WIP trying to figure out traces.

This commit is contained in:
Pat Garrity 2024-09-20 23:57:11 -05:00
parent 3304d5d341
commit 9c8ae5ca9b
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
10 changed files with 156 additions and 36 deletions

View file

@ -1,8 +1,9 @@
package gs.test.v0.definition package gs.test.v0.definition
import cats.Show import cats.Show
import cats.data.Kleisli
import gs.test.v0.definition.pos.SourcePosition import gs.test.v0.definition.pos.SourcePosition
import natchez.Trace import natchez.Span
/** Each instance of this class indicates the _definition_ of some test. /** Each instance of this class indicates the _definition_ of some test.
* *
@ -23,14 +24,14 @@ import natchez.Trace
* @param sourcePosition * @param sourcePosition
* The location of this test in source code. * The location of this test in source code.
*/ */
final class TestDefinition[F[_]: Trace]( final class TestDefinition[F[_]](
val name: TestDefinition.Name, val name: TestDefinition.Name,
val permanentId: PermanentId, val permanentId: PermanentId,
val documentation: Option[String], val documentation: Option[String],
val tags: List[Tag], val tags: List[Tag],
val markers: List[Marker], val markers: List[Marker],
val iterations: TestIterations, val iterations: TestIterations,
val unitOfWork: F[Either[TestFailure, Unit]], val unitOfWork: UnitOfWork[[A] =>> Kleisli[F, Span[F], A]],
val sourcePosition: SourcePosition val sourcePosition: SourcePosition
) )

View file

@ -1,10 +1,14 @@
package gs.test.v0.definition package gs.test.v0.definition
import cats.~>
import cats.arrow.FunctionK
import cats.data.EitherT import cats.data.EitherT
import cats.data.Kleisli
import cats.effect.Async import cats.effect.Async
import cats.syntax.all.* import cats.syntax.all.*
import gs.test.v0.definition.pos.SourcePosition import gs.test.v0.definition.pos.SourcePosition
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import natchez.Span
import natchez.Trace import natchez.Trace
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.* import scala.jdk.CollectionConverters.*
@ -27,7 +31,7 @@ import scala.jdk.CollectionConverters.*
* } * }
* }}} * }}}
*/ */
abstract class TestGroup[F[_]: Async: Trace]: abstract class TestGroup[F[_]: Async]:
/** @return /** @return
* The display name for this group. * The display name for this group.
*/ */
@ -272,15 +276,8 @@ object TestGroup:
* @param unitOfWork * @param unitOfWork
* The function this test will execute. * The function this test will execute.
*/ */
def effectful(unitOfWork: => F[Either[TestFailure, Unit]]): Unit = def effectful(unitOfWork: => UnitOfWork[[A] =>> Kleisli[F, Span[F], A]])
apply(EitherT(unitOfWork)) : Unit =
/** 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 =
registry.register( registry.register(
new TestDefinition[F]( new TestDefinition[F](
name = name, name = name,
@ -289,7 +286,35 @@ object TestGroup:
tags = tags.distinct.toList, tags = tags.distinct.toList,
markers = markers.distinct.toList, markers = markers.distinct.toList,
iterations = iterations, 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 sourcePosition = pos
) )
) )

View file

@ -2,7 +2,6 @@ package gs.test.v0.definition
import cats.Show import cats.Show
import cats.effect.Async import cats.effect.Async
import natchez.Trace
/** Each group is comprised of a list of [[Test]]. This list may be empty. /** Each group is comprised of a list of [[Test]]. This list may be empty.
* *
@ -19,7 +18,7 @@ import natchez.Trace
* @param tests * @param tests
* The list of tests in this group. * The list of tests in this group.
*/ */
final class TestGroupDefinition[F[_]: Async: Trace]( final class TestGroupDefinition[F[_]: Async](
val name: TestGroupDefinition.Name, val name: TestGroupDefinition.Name,
val documentation: Option[String], val documentation: Option[String],
val testTags: List[Tag], val testTags: List[Tag],

View file

@ -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

View file

@ -9,9 +9,9 @@ case class SuiteExecution(
name: String, name: String,
documentation: Option[String], documentation: Option[String],
duration: FiniteDuration, duration: FiniteDuration,
countSeen: Int, countSeen: Long,
countSucceeded: Int, countSucceeded: Long,
countFailed: Int, countFailed: Long,
countIgnored: Int, countIgnored: Long,
executedAt: Instant executedAt: Instant
) )

View file

@ -63,15 +63,8 @@ object TestExecution:
*/ */
def apply(value: UUID): Id = value def apply(value: UUID): Id = value
given UUID.Generator = UUID.Generator.version7()
given CanEqual[Id, Id] = CanEqual.derived given CanEqual[Id, Id] = CanEqual.derived
/** @return
* New ID based on a UUIDv7.
*/
def generate(): Id = UUID.generate()
extension (id: Id) extension (id: Id)
/** @return /** @return
* The underlying UUID. * The underlying UUID.

View file

@ -1,5 +1,15 @@
package gs.test.v0.execution.engine 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( case class EngineConfiguration(
groupConcurrency: ConcurrencySetting,
testConcurrency: ConcurrencySetting testConcurrency: ConcurrencySetting
) )

View file

@ -3,7 +3,6 @@ package gs.test.v0.execution.engine
import gs.test.v0.execution.SuiteExecution import gs.test.v0.execution.SuiteExecution
import gs.test.v0.execution.TestExecution import gs.test.v0.execution.TestExecution
final class EngineResult[F[_]]( final class EngineResult(
val suiteExecution: SuiteExecution, val suiteExecution: SuiteExecution
val testExecutions: fs2.Stream[F, TestExecution]
) )

View file

@ -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

View file

@ -1,28 +1,69 @@
package gs.test.v0.execution.engine package gs.test.v0.execution.engine
import cats.data.Kleisli
import cats.effect.Async import cats.effect.Async
import cats.syntax.all.* import cats.syntax.all.*
import gs.test.v0.definition.TestGroupDefinition import gs.test.v0.definition.TestGroupDefinition
import gs.test.v0.definition.TestSuite import gs.test.v0.definition.TestSuite
import gs.test.v0.execution.SuiteExecution
import gs.test.v0.execution.TestExecution import gs.test.v0.execution.TestExecution
import gs.timing.v0.Timing import gs.timing.v0.Timing
import gs.uuid.v0.UUID
import java.time.Clock
import java.time.Instant
import natchez.EntryPoint import natchez.EntryPoint
import natchez.Span
final class TestEngine[F[_]: Async]( final class TestEngine[F[_]: Async](
val configuration: EngineConfiguration, val configuration: EngineConfiguration,
timing: Timing[F], timing: Timing[F],
suiteExecutionIdGenerator: UUID.Generator,
testExecutionIdGenerator: UUID.Generator,
clock: Clock,
val entryPoint: EntryPoint[F] val entryPoint: EntryPoint[F]
): ):
def runSuite( def runSuite(
suite: TestSuite, suite: TestSuite,
tests: fs2.Stream[F, TestGroupDefinition[F]] tests: fs2.Stream[F, TestGroupDefinition[F]]
): EngineResult[F] = ): F[SuiteExecution] =
EngineResult[F]( for
suiteExecution = ???, executedAt <- Async[F].delay(Instant.now(clock))
testExecutions = tests.mapAsync(4)(group => runGroup(group)).map(_ => ???) 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( def runGroup(
group: TestGroupDefinition[F] group: TestGroupDefinition[F]
): F[GroupResult] = ): F[GroupResult] =
@ -52,14 +93,14 @@ final class TestEngine[F[_]: Async](
.emits(group.tests) .emits(group.tests)
.mapAsync(configuration.testConcurrency.toInt()) { test => .mapAsync(configuration.testConcurrency.toInt()) { test =>
for for
testExecutionId <- Async[F].delay(TestExecution.Id.generate()) testExecutionId <- Async[F].delay(testExecutionIdGenerator.next())
timer <- timing.start() timer <- timing.start()
_ <- group.beforeEachTest.getOrElse(Async[F].unit) _ <- group.beforeEachTest.getOrElse(Async[F].unit)
result <- test.unitOfWork result <- test.unitOfWork
_ <- group.afterEachTest.getOrElse(Async[F].unit) _ <- group.afterEachTest.getOrElse(Async[F].unit)
elapsed <- timer.checkpoint() elapsed <- timer.checkpoint()
yield TestExecution( yield TestExecution(
id = testExecutionId, id = TestExecution.Id(testExecutionId),
permanentId = test.permanentId, permanentId = test.permanentId,
documentation = test.documentation, documentation = test.documentation,
tags = test.tags, tags = test.tags,