From 9c8ae5ca9b0e578cc9b893c11f38a2c3e8b842d6 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Fri, 20 Sep 2024 23:57:11 -0500 Subject: [PATCH] WIP trying to figure out traces. --- .../test/v0/definition/TestDefinition.scala | 7 +-- .../gs/test/v0/definition/TestGroup.scala | 47 ++++++++++++---- .../v0/definition/TestGroupDefinition.scala | 3 +- .../gs/test/v0/definition/UnitOfWork.scala | 18 +++++++ .../gs/test/v0/execution/SuiteExecution.scala | 8 +-- .../gs/test/v0/execution/TestExecution.scala | 7 --- .../engine/EngineConfiguration.scala | 10 ++++ .../v0/execution/engine/EngineResult.scala | 5 +- .../v0/execution/engine/EngineStats.scala | 34 ++++++++++++ .../test/v0/execution/engine/TestEngine.scala | 53 ++++++++++++++++--- 10 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala create mode 100644 modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineStats.scala 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 f1699db..36cbdc1 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,8 +1,9 @@ package gs.test.v0.definition import cats.Show +import cats.data.Kleisli import gs.test.v0.definition.pos.SourcePosition -import natchez.Trace +import natchez.Span /** Each instance of this class indicates the _definition_ of some test. * @@ -23,14 +24,14 @@ import natchez.Trace * @param sourcePosition * The location of this test in source code. */ -final class TestDefinition[F[_]: Trace]( +final class TestDefinition[F[_]]( val name: TestDefinition.Name, val permanentId: PermanentId, val documentation: Option[String], val tags: List[Tag], val markers: List[Marker], val iterations: TestIterations, - val unitOfWork: F[Either[TestFailure, Unit]], + val unitOfWork: UnitOfWork[[A] =>> Kleisli[F, Span[F], A]], 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 879efae..afe3ce0 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,10 +1,14 @@ package gs.test.v0.definition +import cats.~> +import cats.arrow.FunctionK 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 natchez.Trace import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters.* @@ -27,7 +31,7 @@ import scala.jdk.CollectionConverters.* * } * }}} */ -abstract class TestGroup[F[_]: Async: Trace]: +abstract class TestGroup[F[_]: Async]: /** @return * The display name for this group. */ @@ -272,15 +276,8 @@ object TestGroup: * @param unitOfWork * The function this test will execute. */ - def effectful(unitOfWork: => F[Either[TestFailure, Unit]]): Unit = - apply(EitherT(unitOfWork)) - - /** Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork - * The function this test will execute. - */ - def apply(unitOfWork: => EitherT[F, TestFailure, Unit]): Unit = + def effectful(unitOfWork: => UnitOfWork[[A] =>> Kleisli[F, Span[F], A]]) + : Unit = registry.register( new TestDefinition[F]( name = name, @@ -289,7 +286,35 @@ object TestGroup: tags = tags.distinct.toList, markers = markers.distinct.toList, iterations = iterations, - unitOfWork = unitOfWork.value, + unitOfWork = 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 + } + + /** 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 = + registry.register( + new TestDefinition[F]( + name = name, + permanentId = permanentId, + documentation = documentation, + tags = tags.distinct.toList, + markers = markers.distinct.toList, + iterations = iterations, + unitOfWork = UnitOfWork[F](unitOfWork.work.mapK(FooToBar)), sourcePosition = pos ) ) diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala index 28cad31..eaedab6 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala @@ -2,7 +2,6 @@ package gs.test.v0.definition import cats.Show import cats.effect.Async -import natchez.Trace /** Each group is comprised of a list of [[Test]]. This list may be empty. * @@ -19,7 +18,7 @@ import natchez.Trace * @param tests * The list of tests in this group. */ -final class TestGroupDefinition[F[_]: Async: Trace]( +final class TestGroupDefinition[F[_]: Async]( val name: TestGroupDefinition.Name, val documentation: Option[String], val testTags: List[Tag], 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 new file mode 100644 index 0000000..5eb2d1a --- /dev/null +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/UnitOfWork.scala @@ -0,0 +1,18 @@ +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]] + +object UnitOfWork: + + def apply[F[_]](uow: Kleisli[F, Span[F], Either[TestFailure, Unit]]) = uow + +end UnitOfWork diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/SuiteExecution.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/SuiteExecution.scala index c0a2e52..a691ecd 100644 --- a/modules/api-execution/src/main/scala/gs/test/v0/execution/SuiteExecution.scala +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/SuiteExecution.scala @@ -9,9 +9,9 @@ case class SuiteExecution( name: String, documentation: Option[String], duration: FiniteDuration, - countSeen: Int, - countSucceeded: Int, - countFailed: Int, - countIgnored: Int, + countSeen: Long, + countSucceeded: Long, + countFailed: Long, + countIgnored: Long, executedAt: Instant ) 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 eb34d24..d4a8044 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 @@ -63,15 +63,8 @@ object TestExecution: */ def apply(value: UUID): Id = value - given UUID.Generator = UUID.Generator.version7() - given CanEqual[Id, Id] = CanEqual.derived - /** @return - * New ID based on a UUIDv7. - */ - def generate(): Id = UUID.generate() - extension (id: Id) /** @return * The underlying UUID. diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineConfiguration.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineConfiguration.scala index e85254e..e7d4f49 100644 --- a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineConfiguration.scala +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineConfiguration.scala @@ -1,5 +1,15 @@ package gs.test.v0.execution.engine +/** Used to control the behavior of some [[TestEngine]] + * + * @param groupConcurrency + * [[ConcurrencySetting]] for groups; the number of groups allowed to execute + * at the same time. + * @param testConcurrency + * [[ConcurrencySetting]] for tests; the number of tests allowed to execute + * at the same time within some group. + */ case class EngineConfiguration( + groupConcurrency: ConcurrencySetting, testConcurrency: ConcurrencySetting ) 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 1115468..7846b8a 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 @@ -3,7 +3,6 @@ package gs.test.v0.execution.engine import gs.test.v0.execution.SuiteExecution import gs.test.v0.execution.TestExecution -final class EngineResult[F[_]]( - val suiteExecution: SuiteExecution, - val testExecutions: fs2.Stream[F, TestExecution] +final class EngineResult( + val suiteExecution: SuiteExecution ) diff --git a/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineStats.scala b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineStats.scala new file mode 100644 index 0000000..1156823 --- /dev/null +++ b/modules/api-execution/src/main/scala/gs/test/v0/execution/engine/EngineStats.scala @@ -0,0 +1,34 @@ +package gs.test.v0.execution.engine + +import cats.effect.Async +import cats.effect.Ref +import cats.syntax.all.* +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration + +final class EngineStats[F[_]: Async]( + val overallDuration: Ref[F, FiniteDuration], + val countSeen: Ref[F, Long], + val countSucceeded: Ref[F, Long], + val countFailed: Ref[F, Long], + val countIgnored: Ref[F, Long] +) + +object EngineStats: + + def initialize[F[_]: Async]: F[EngineStats[F]] = + for + duration <- Ref.of(FiniteDuration(0L, TimeUnit.NANOSECONDS)) + seen <- Ref.of(0L) + succeeded <- Ref.of(0L) + failed <- Ref.of(0L) + ignored <- Ref.of(0L) + yield new EngineStats[F]( + overallDuration = duration, + countSeen = seen, + countSucceeded = succeeded, + countFailed = failed, + countIgnored = ignored + ) + +end EngineStats 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 c6ff1f9..691eb74 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,28 +1,69 @@ package gs.test.v0.execution.engine +import cats.data.Kleisli import cats.effect.Async import cats.syntax.all.* import gs.test.v0.definition.TestGroupDefinition import gs.test.v0.definition.TestSuite +import gs.test.v0.execution.SuiteExecution import gs.test.v0.execution.TestExecution import gs.timing.v0.Timing +import gs.uuid.v0.UUID +import java.time.Clock +import java.time.Instant import natchez.EntryPoint +import natchez.Span final class TestEngine[F[_]: Async]( val configuration: EngineConfiguration, timing: Timing[F], + suiteExecutionIdGenerator: UUID.Generator, + testExecutionIdGenerator: UUID.Generator, + clock: Clock, val entryPoint: EntryPoint[F] ): def runSuite( suite: TestSuite, tests: fs2.Stream[F, TestGroupDefinition[F]] - ): EngineResult[F] = - EngineResult[F]( - suiteExecution = ???, - testExecutions = tests.mapAsync(4)(group => runGroup(group)).map(_ => ???) + ): F[SuiteExecution] = + for + executedAt <- Async[F].delay(Instant.now(clock)) + stats <- EngineStats.initialize[F] + _ <- tests + .mapAsync(configuration.groupConcurrency.toInt())(runGroup) + .evalTap(updateGroupStats) + .evalTap(reportGroup) + .flatMap(groupResult => fs2.Stream.emits(groupResult.testExecutions)) + .evalTap(updateTestStats) + .evalMap(reportTestExecution) + .compile + .drain + overallDuration <- stats.overallDuration.get + countSeen <- stats.countSeen.get + countSucceeded <- stats.countSucceeded.get + countFailed <- stats.countFailed.get + countIgnored <- stats.countIgnored.get + yield SuiteExecution( + id = suiteExecutionIdGenerator.next(), + name = suite.name, + documentation = suite.documentation, + duration = overallDuration, + countSeen = countSeen, + countSucceeded = countSucceeded, + countFailed = countFailed, + countIgnored = countIgnored, + executedAt = executedAt ) + private def updateGroupStats(groupResult: GroupResult): F[Unit] = ??? + + private def updateTestStats(testExecution: TestExecution): F[Unit] = ??? + + private def reportGroup(groupResult: GroupResult): F[Unit] = ??? + + private def reportTestExecution(testExecution: TestExecution): F[Unit] = ??? + def runGroup( group: TestGroupDefinition[F] ): F[GroupResult] = @@ -52,14 +93,14 @@ final class TestEngine[F[_]: Async]( .emits(group.tests) .mapAsync(configuration.testConcurrency.toInt()) { test => for - testExecutionId <- Async[F].delay(TestExecution.Id.generate()) + 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() yield TestExecution( - id = testExecutionId, + id = TestExecution.Id(testExecutionId), permanentId = test.permanentId, documentation = test.documentation, tags = test.tags,