diff --git a/build.sbt b/build.sbt index 751f1b6..d1ebb4a 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -val scala3: String = "3.5.0" +val scala3: String = "3.5.1" ThisBuild / scalaVersion := scala3 ThisBuild / versionScheme := Some("semver-spec") diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala index 36cbdc1..88f2325 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala @@ -1,9 +1,7 @@ package gs.test.v0.definition import cats.Show -import cats.data.Kleisli import gs.test.v0.definition.pos.SourcePosition -import natchez.Span /** Each instance of this class indicates the _definition_ of some test. * @@ -31,7 +29,7 @@ final class TestDefinition[F[_]]( val tags: List[Tag], val markers: List[Marker], val iterations: TestIterations, - val unitOfWork: UnitOfWork[[A] =>> Kleisli[F, Span[F], A]], + val unitOfWork: UnitOfWork[F], val sourcePosition: SourcePosition ) 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 afe3ce0..7b78f93 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,7 +1,5 @@ package gs.test.v0.definition -import cats.~> -import cats.arrow.FunctionK import cats.data.EitherT import cats.data.Kleisli import cats.effect.Async @@ -9,7 +7,6 @@ import cats.syntax.all.* import gs.test.v0.definition.pos.SourcePosition import java.util.concurrent.ConcurrentHashMap import natchez.Span -import natchez.Trace import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters.* @@ -178,7 +175,7 @@ object TestGroup: * @param iterations * Number of iterations to run this test. */ - final protected class TestBuilder[F[_]: Async: Trace]( + final protected class TestBuilder[F[_]: Async]( val registry: Registry[F], val name: TestDefinition.Name, val permanentId: PermanentId, @@ -269,14 +266,14 @@ object TestGroup: * The function this test will execute. */ def pure(unitOfWork: => Either[TestFailure, Unit]): Unit = - apply(EitherT.fromEither[F](unitOfWork)) + effectful(Kleisli(_ => 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: => UnitOfWork[[A] =>> Kleisli[F, Span[F], A]]) + def effectful(unitOfWork: => Kleisli[F, Span[F], Either[TestFailure, Any]]) : Unit = registry.register( new TestDefinition[F]( @@ -286,26 +283,21 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = unitOfWork, + unitOfWork = UnitOfWork.apply(unitOfWork), sourcePosition = pos ) ) - type Foo[A] = EitherT[F, TestFailure, A] - - type Bar[A] = F[Either[TestFailure, A]] - - val FooToBar: FunctionK[Foo, Bar] = new FunctionK[Foo, Bar] { - def apply[A](fa: Foo[A]): Bar[A] = fa.value - } + /** 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: => UnitOfWork[[A] =>> Kleisli[Foo, Span[Foo], A]]) - : Unit = + def apply(unitOfWork: => Kleisli[ET, Span[F], Any]): Unit = registry.register( new TestDefinition[F]( name = name, @@ -314,7 +306,7 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = UnitOfWork[F](unitOfWork.work.mapK(FooToBar)), + unitOfWork = UnitOfWork[F].apply(unitOfWork.mapF(_.value)), sourcePosition = pos ) ) @@ -342,7 +334,7 @@ object TestGroup: * @param iterations * Number of iterations to run this test. */ - final protected class InputTestBuilder[F[_]: Async: Trace, Input]( + final protected class InputTestBuilder[F[_]: Async, Input]( val registry: Registry[F], val name: TestDefinition.Name, val permanentId: PermanentId, @@ -414,22 +406,16 @@ object TestGroup: * The function this test will execute. */ def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit = - apply(input => EitherT(Async[F].delay(unitOfWork(input)))) + effectful(input => Kleisli(_ => Async[F].pure(unitOfWork(input)))) /** Finalize and register this test with an effectful unit of work. * * @param unitOfWork * The function this test will execute. */ - def effectful(unitOfWork: Input => F[Either[TestFailure, Unit]]): Unit = - apply(input => EitherT(unitOfWork(input))) - - /** Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork - * The function this test will execute. - */ - def apply(unitOfWork: Input => EitherT[F, TestFailure, Unit]): Unit = + def effectful( + unitOfWork: Input => Kleisli[F, Span[F], Either[TestFailure, Any]] + ): Unit = registry.register( new TestDefinition[F]( name = name, @@ -438,7 +424,40 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = EitherT.right(inputFunction).flatMap(unitOfWork).value, + unitOfWork = UnitOfWork.apply( + Kleisli(span => + inputFunction.flatMap(input => unitOfWork(input).run(span)) + ) + ), + 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 = + registry.register( + new TestDefinition[F]( + name = name, + permanentId = permanentId, + documentation = documentation, + 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) + } + ) + ), 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 5eb2d1a..715179b 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 @@ -2,17 +2,30 @@ package gs.test.v0.definition import cats.data.Kleisli import natchez.Span -import natchez.Trace trait UnitOfWork[F[_]]: def work( - using - Trace[F] - ): F[Either[TestFailure, Unit]] + span: Span[F] + ): F[Either[TestFailure, Any]] object UnitOfWork: - def apply[F[_]](uow: Kleisli[F, Span[F], Either[TestFailure, Unit]]) = uow + /** Instantiate a new [[UnitOfWork]] with the given function that requires a + * `Span[F]` as input. + * + * @param uow + * The unit of work implementation. + * @return + * The new [[UnitOfWork]] instance. + */ + def apply[F[_]]( + uow: Kleisli[F, Span[F], Either[TestFailure, Any]] + ): UnitOfWork[F] = new UnitOfWork[F] { + + override def work(span: Span[F]): F[Either[TestFailure, Any]] = + uow.apply(span) + + } end UnitOfWork diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/TestExecution.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/TestExecution.scala index d4a8044..8cce39f 100644 --- a/modules/api-execution/src/main/scala/gs/test/v0/execution/TestExecution.scala +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/TestExecution.scala @@ -1,5 +1,6 @@ package gs.test.v0.execution +import cats.Show import gs.test.v0.definition.Marker import gs.test.v0.definition.PermanentId import gs.test.v0.definition.Tag @@ -40,7 +41,7 @@ case class TestExecution( documentation: Option[String], tags: List[Tag], markers: List[Marker], - result: Either[TestFailure, Unit], + result: Either[TestFailure, Any], traceId: UUID, sourcePosition: SourcePosition, duration: FiniteDuration @@ -65,6 +66,8 @@ object TestExecution: given CanEqual[Id, Id] = CanEqual.derived + given Show[Id] = id => id.toUUID().withoutDashes() + extension (id: Id) /** @return * The underlying UUID. diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineResult.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineResult.scala index 7846b8a..c6dc613 100644 --- a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineResult.scala +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineResult.scala @@ -1,7 +1,6 @@ package gs.test.v0.execution.engine import gs.test.v0.execution.SuiteExecution -import gs.test.v0.execution.TestExecution final class EngineResult( val suiteExecution: SuiteExecution diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/TestEngine.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/TestEngine.scala index 691eb74..0a36455 100644 --- a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/TestEngine.scala +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/TestEngine.scala @@ -1,8 +1,9 @@ package gs.test.v0.execution.engine -import cats.data.Kleisli import cats.effect.Async import cats.syntax.all.* +import gs.test.v0.definition.TestDefinition +import gs.test.v0.definition.TestFailure import gs.test.v0.definition.TestGroupDefinition import gs.test.v0.definition.TestSuite import gs.test.v0.execution.SuiteExecution @@ -64,43 +65,69 @@ final class TestEngine[F[_]: Async]( private def reportTestExecution(testExecution: TestExecution): F[Unit] = ??? + private def runSpan[A]( + name: String, + root: Span[F], + f: F[A] + ): F[A] = + root.span(name).use(_ => f) + def runGroup( group: TestGroupDefinition[F] ): F[GroupResult] = - for - _ <- group.beforeGroup.getOrElse(Async[F].unit) - stream <- executeGroupTests(group) - _ <- group.afterGroup.getOrElse(Async[F].unit) - yield stream + entryPoint.root("test-group").use { rootSpan => + for + _ <- rootSpan.put("test_group_name" -> group.name.show) + _ <- runSpan( + "before-group", + rootSpan, + group.beforeGroup.getOrElse(Async[F].unit) + ) + stream <- executeGroupTests(group, rootSpan) + _ <- runSpan( + "after-group", + rootSpan, + group.afterGroup.getOrElse(Async[F].unit) + ) + yield stream + } - private def executeGroupTests(group: TestGroupDefinition[F]): F[GroupResult] = - for - timer <- timing.start() - _ <- group.beforeGroup.getOrElse(Async[F].unit) - executions <- streamGroupTests(group).compile.toList - _ <- group.afterGroup.getOrElse(Async[F].unit) - elapsed <- timer.checkpoint() - yield new GroupResult( - name = group.name, - documentation = group.documentation, - duration = elapsed.duration, - testExecutions = executions - ) + private def executeGroupTests( + group: TestGroupDefinition[F], + rootSpan: Span[F] + ): F[GroupResult] = + rootSpan.span("group").use { groupSpan => + for + traceId <- rootSpan.traceId.map(parseTraceId) + timer <- timing.start() + executions <- streamGroupTests(group, groupSpan).compile.toList + elapsed <- timer.checkpoint() + yield new GroupResult( + name = group.name, + documentation = group.documentation, + duration = elapsed.duration, + testExecutions = executions + ) + } - private def streamGroupTests(group: TestGroupDefinition[F]) - : fs2.Stream[F, TestExecution] = + private def streamGroupTests( + group: TestGroupDefinition[F], + groupSpan: Span[F] + ): fs2.Stream[F, TestExecution] = fs2.Stream .emits(group.tests) .mapAsync(configuration.testConcurrency.toInt()) { test => for - testExecutionId <- Async[F].delay(testExecutionIdGenerator.next()) - timer <- timing.start() - _ <- group.beforeEachTest.getOrElse(Async[F].unit) - result <- test.unitOfWork - _ <- group.afterEachTest.getOrElse(Async[F].unit) - elapsed <- timer.checkpoint() + testExecutionId <- Async[F].delay( + TestExecution.Id(testExecutionIdGenerator.next()) + ) + timer <- timing.start() + _ <- group.beforeEachTest.getOrElse(Async[F].unit) + result <- runSingleTest(testExecutionId, test, groupSpan) + _ <- group.afterEachTest.getOrElse(Async[F].unit) + elapsed <- timer.checkpoint() yield TestExecution( - id = TestExecution.Id(testExecutionId), + id = testExecutionId, permanentId = test.permanentId, documentation = test.documentation, tags = test.tags, @@ -111,3 +138,19 @@ final class TestEngine[F[_]: Async]( duration = elapsed.duration ) } + + private def runSingleTest( + testExecutionId: TestExecution.Id, + test: TestDefinition[F], + groupSpan: Span[F] + ): F[Either[TestFailure, Any]] = + groupSpan.span("test").use { span => + for + // TODO: Constants + _ <- span.put("test_execution_id" -> testExecutionId.show) + _ <- span.put("test_name" -> test.name.show) + result <- test.unitOfWork.work(span) + yield result + } + + private def parseTraceId(candidate: Option[String]): UUID = ??? diff --git a/project/build.properties b/project/build.properties index ee4c672..0b699c3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.1 +sbt.version=1.10.2