diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala index 7b78f93..3caaace 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala @@ -1,12 +1,10 @@ package gs.test.v0.definition import cats.data.EitherT -import cats.data.Kleisli import cats.effect.Async import cats.syntax.all.* import gs.test.v0.definition.pos.SourcePosition import java.util.concurrent.ConcurrentHashMap -import natchez.Span import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters.* @@ -266,15 +264,16 @@ object TestGroup: * The function this test will execute. */ def pure(unitOfWork: => Either[TestFailure, Unit]): Unit = - effectful(Kleisli(_ => Async[F].pure(unitOfWork))) + effectful(Async[F].pure(unitOfWork)) /** Finalize and register this test with an effectful unit of work. * * @param unitOfWork * The function this test will execute. */ - def effectful(unitOfWork: => Kleisli[F, Span[F], Either[TestFailure, Any]]) - : Unit = + def effectful( + unitOfWork: natchez.Trace[F] ?=> F[Either[TestFailure, Any]] + ): Unit = registry.register( new TestDefinition[F]( name = name, @@ -288,16 +287,14 @@ object TestGroup: ) ) - /** Helper type for representing `span => EitherT[F, TestFailure, Any]` - */ - type ET[A] = EitherT[F, TestFailure, A] - /** Finalize and register this test with an effectful unit of work. * * @param unitOfWork * The function this test will execute. */ - def apply(unitOfWork: => Kleisli[ET, Span[F], Any]): Unit = + def apply( + unitOfWork: natchez.Trace[F] ?=> EitherT[F, TestFailure, Any] + ): Unit = registry.register( new TestDefinition[F]( name = name, @@ -306,7 +303,7 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = UnitOfWork[F].apply(unitOfWork.mapF(_.value)), + unitOfWork = UnitOfWork.applyT(unitOfWork), sourcePosition = pos ) ) @@ -406,7 +403,20 @@ object TestGroup: * The function this test will execute. */ def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit = - effectful(input => Kleisli(_ => Async[F].pure(unitOfWork(input)))) + registry.register( + new TestDefinition[F]( + name = name, + permanentId = permanentId, + documentation = documentation, + tags = tags.distinct.toList, + markers = markers.distinct.toList, + iterations = iterations, + unitOfWork = UnitOfWork.apply( + inputFunction.map(input => unitOfWork(input)) + ), + sourcePosition = pos + ) + ) /** Finalize and register this test with an effectful unit of work. * @@ -414,7 +424,7 @@ object TestGroup: * The function this test will execute. */ def effectful( - unitOfWork: Input => Kleisli[F, Span[F], Either[TestFailure, Any]] + unitOfWork: natchez.Trace[F] ?=> Input => F[Either[TestFailure, Any]] ): Unit = registry.register( new TestDefinition[F]( @@ -425,24 +435,20 @@ object TestGroup: markers = markers.distinct.toList, iterations = iterations, unitOfWork = UnitOfWork.apply( - Kleisli(span => - inputFunction.flatMap(input => unitOfWork(input).run(span)) - ) + inputFunction.flatMap(input => unitOfWork(input)) ), sourcePosition = pos ) ) - /** Helper type for representing `span => EitherT[F, TestFailure, Any]` - */ - type ET[A] = EitherT[F, TestFailure, A] - /** Finalize and register this test with an effectful unit of work. * * @param unitOfWork * The function this test will execute. */ - def apply(unitOfWork: => Input => Kleisli[ET, Span[F], Any]): Unit = + def apply( + unitOfWork: natchez.Trace[F] ?=> Input => EitherT[F, TestFailure, Any] + ): Unit = registry.register( new TestDefinition[F]( name = name, @@ -451,12 +457,8 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = UnitOfWork[F].apply( - Kleisli(span => - inputFunction.flatMap { input => - unitOfWork(input).mapF(_.value).run(span) - } - ) + unitOfWork = UnitOfWork.applyT( + EitherT.liftF(inputFunction).flatMap(unitOfWork) ), sourcePosition = pos ) diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala index 715179b..6911b00 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala @@ -1,9 +1,17 @@ package gs.test.v0.definition -import cats.data.Kleisli -import natchez.Span +import cats.~> +import cats.Applicative +import cats.data.EitherT +import cats.effect.kernel.Async +import cats.effect.kernel.Ref +import cats.effect.kernel.Resource +import cats.effect.syntax.all.* +import cats.syntax.all.* +import java.net.URI +import natchez.* -trait UnitOfWork[F[_]]: +sealed trait UnitOfWork[F[_]]: def work( span: Span[F] @@ -11,6 +19,10 @@ trait UnitOfWork[F[_]]: object UnitOfWork: + type Traced[F[_]] = natchez.Trace[F] ?=> F[Either[TestFailure, Any]] + + type TracedT[F[_]] = natchez.Trace[F] ?=> EitherT[F, TestFailure, Any] + /** Instantiate a new [[UnitOfWork]] with the given function that requires a * `Span[F]` as input. * @@ -19,13 +31,96 @@ object UnitOfWork: * @return * The new [[UnitOfWork]] instance. */ - def apply[F[_]]( - uow: Kleisli[F, Span[F], Either[TestFailure, Any]] - ): UnitOfWork[F] = new UnitOfWork[F] { + def apply[F[_]: Async]( + unitOfWork: Traced[F] + ): UnitOfWork[F] = + new UnitOfWork[F] { - override def work(span: Span[F]): F[Either[TestFailure, Any]] = - uow.apply(span) + override def work(span: Span[F]): F[Either[TestFailure, Any]] = + makeInternalTrace[F](span).flatMap { trace => + given Trace[F] = trace + unitOfWork + } - } + } + + def applyT[F[_]: Async]( + unitOfWork: TracedT[F] + ): UnitOfWork[F] = + new UnitOfWork[F] { + + override def work(span: Span[F]): F[Either[TestFailure, Any]] = + makeInternalTrace[F](span).flatMap { trace => + given Trace[F] = trace + unitOfWork.value + } + + } + + private def makeInternalTrace[F[_]: Async](sourceSpan: Span[F]) + : F[natchez.Trace[F]] = + Ref.of[F, Span[F]](sourceSpan).map(src => new InternalTrace[F](src)) + + // Copied from the Natchez ioTrace + private class InternalTrace[F[_]: Async]( + val src: Ref[F, Span[F]] + ) extends natchez.Trace[F]: + + def put(fields: (String, TraceValue)*): F[Unit] = + src.get.flatMap(_.put(fields*)) + + def log(fields: (String, TraceValue)*): F[Unit] = + src.get.flatMap(_.log(fields*)) + + def log(event: String): F[Unit] = + src.get.flatMap(_.log(event)) + + def attachError( + err: Throwable, + fields: (String, TraceValue)* + ): F[Unit] = + src.get.flatMap(_.attachError(err, fields*)) + + def kernel: F[Kernel] = src.get.flatMap(_.kernel) + + def spanR( + name: String, + options: Span.Options = Span.Options.Defaults + ): Resource[F, F ~> F] = + for { + parent <- Resource.eval(src.get) + child <- parent.span(name, options) + } yield new (F ~> F) { + def apply[A](fa: F[A]): F[A] = + src.get.flatMap { old => + src + .set(child) + .bracket(_ => fa.onError { case e => child.attachError(e) })(_ => + src.set(old) + ) + } + + } + + def span[A]( + name: String, + options: Span.Options = Span.Options.Defaults + )( + k: F[A] + ): F[A] = + spanR(name, options).use(_(k)) + + def traceId: F[Option[String]] = + src.get.flatMap(_.traceId) + + override def spanId( + implicit + F: Applicative[F] + ): F[Option[String]] = + src.get.flatMap(_.spanId) + + override def traceUri: F[Option[URI]] = src.get.flatMap(_.traceUri) + + end InternalTrace end UnitOfWork diff --git a/modules/api-definition/src/test/scala/gs/test/v0/definition/GroupImplementationTests.scala b/modules/api-definition/src/test/scala/gs/test/v0/definition/GroupImplementationTests.scala index dc7088b..8274105 100644 --- a/modules/api-definition/src/test/scala/gs/test/v0/definition/GroupImplementationTests.scala +++ b/modules/api-definition/src/test/scala/gs/test/v0/definition/GroupImplementationTests.scala @@ -6,7 +6,6 @@ import cats.effect.IO import gs.test.v0.definition.{Tag => GsTag} import munit.* import natchez.Span -import natchez.Trace class GroupImplementationTests extends FunSuite: import GroupImplementationTests.* @@ -90,15 +89,16 @@ object GroupImplementationTests: val T1: PermanentId = pid"t1" val T2: PermanentId = pid"t2" val T3: PermanentId = pid"t3" + val T4: PermanentId = pid"t4" end Ids - class G1[F[_]: Async: Trace] extends TestGroup[F]: + class G1[F[_]: Async] extends TestGroup[F]: override def name: String = "G1" test(Ids.T1, "simple").pure(Right(())) end G1 - class G2[F[_]: Async: Trace] extends TestGroup[F]: + class G2[F[_]: Async] extends TestGroup[F]: override def name: String = "G2" @@ -120,7 +120,7 @@ object GroupImplementationTests: test(Ids.T2, "inherit from group").pure(Right(())) end G2 - class G3[F[_]: Async: Trace] extends TestGroup[F]: + class G3[F[_]: Async] extends TestGroup[F]: override def name: String = "G3" test(Ids.T3, "configure test") @@ -132,4 +132,12 @@ object GroupImplementationTests: end G3 + class G4[F[_]: Async] extends TestGroup[F]: + override def name: String = "G4" + + // TODO: Make test entrypoint and test Trace[F] + test(Ids.T4, "Effectful test").effectful { + ??? + } + end GroupImplementationTests diff --git a/modules/api-definition/src/test/scala/gs/test/v0/definition/pos/SourcePositionTests.scala b/modules/api-definition/src/test/scala/gs/test/v0/definition/pos/SourcePositionTests.scala index bcc57fc..e85dfe6 100644 --- a/modules/api-definition/src/test/scala/gs/test/v0/definition/pos/SourcePositionTests.scala +++ b/modules/api-definition/src/test/scala/gs/test/v0/definition/pos/SourcePositionTests.scala @@ -1,6 +1,5 @@ package gs.test.v0.definition.pos -import cats.data.Kleisli import cats.effect.IO import cats.effect.kernel.Resource import gs.test.v0.IOSuite @@ -10,8 +9,6 @@ import natchez.EntryPoint import natchez.Kernel import natchez.Span import natchez.Span.Options -import natchez.Trace -import natchez.Trace.Implicits.noop /** These tests are sensitive to changes, even in formatting! They are looking * for specific line numbers in this source code, so any sort of newline that @@ -45,15 +42,15 @@ class SourcePositionTests extends IOSuite: } test("should provide the source position of a failed check") { - lookForSourcePosition(new G1, 73) + lookForSourcePosition(new G1, 81) } test("should provide the source position of an explicit failure") { - lookForSourcePosition(new G2, 82) + lookForSourcePosition(new G2, 90) } private def lookForSourcePosition( - groupDef: TestGroup[TracedIO], + groupDef: TestGroup[IO], line: Int ): Unit = val group = groupDef.compile() @@ -61,7 +58,7 @@ class SourcePositionTests extends IOSuite: case t1 :: Nil => ep.root("unit-test") .use { span => - t1.unitOfWork.run(span).map { + t1.unitOfWork.work(span).map { case Left(TestFailure.AssertionFailed(_, _, _, pos)) => assertEquals(pos.file.endsWith(SourceFileName), true) assertEquals(pos.line, line) @@ -77,9 +74,7 @@ class SourcePositionTests extends IOSuite: object SourcePositionTests: - type TracedIO[A] = Kleisli[IO, Span[IO], A] - - class G1 extends TestGroup[TracedIO]: + class G1 extends TestGroup[IO]: override def name: String = "G1" test(pid"t1", "pos").pure { @@ -88,7 +83,7 @@ object SourcePositionTests: end G1 - class G2 extends TestGroup[TracedIO]: + class G2 extends TestGroup[IO]: override def name: String = "G2" test(pid"t2", "pos").pure {