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

View file

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

View file

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

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,
documentation: Option[String],
duration: FiniteDuration,
countSeen: Int,
countSucceeded: Int,
countFailed: Int,
countIgnored: Int,
countSeen: Long,
countSucceeded: Long,
countFailed: Long,
countIgnored: Long,
executedAt: Instant
)

View file

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

View file

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

View file

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

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