WIP - More engine work, tying everything together.

This commit is contained in:
Pat Garrity 2024-10-06 21:38:39 -05:00
parent 9c8ae5ca9b
commit 3f41e23478
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
8 changed files with 144 additions and 69 deletions

View file

@ -1,4 +1,4 @@
val scala3: String = "3.5.0" val scala3: String = "3.5.1"
ThisBuild / scalaVersion := scala3 ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec") ThisBuild / versionScheme := Some("semver-spec")

View file

@ -1,9 +1,7 @@
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.Span
/** Each instance of this class indicates the _definition_ of some test. /** Each instance of this class indicates the _definition_ of some test.
* *
@ -31,7 +29,7 @@ final class TestDefinition[F[_]](
val tags: List[Tag], val tags: List[Tag],
val markers: List[Marker], val markers: List[Marker],
val iterations: TestIterations, val iterations: TestIterations,
val unitOfWork: UnitOfWork[[A] =>> Kleisli[F, Span[F], A]], val unitOfWork: UnitOfWork[F],
val sourcePosition: SourcePosition val sourcePosition: SourcePosition
) )

View file

@ -1,7 +1,5 @@
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.data.Kleisli
import cats.effect.Async import cats.effect.Async
@ -9,7 +7,6 @@ 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.Span
import natchez.Trace
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.* import scala.jdk.CollectionConverters.*
@ -178,7 +175,7 @@ object TestGroup:
* @param iterations * @param iterations
* Number of iterations to run this test. * 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 registry: Registry[F],
val name: TestDefinition.Name, val name: TestDefinition.Name,
val permanentId: PermanentId, val permanentId: PermanentId,
@ -269,14 +266,14 @@ object TestGroup:
* The function this test will execute. * The function this test will execute.
*/ */
def pure(unitOfWork: => Either[TestFailure, Unit]): Unit = 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. /** Finalize and register this test with an effectful unit of work.
* *
* @param unitOfWork * @param unitOfWork
* The function this test will execute. * 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 = : Unit =
registry.register( registry.register(
new TestDefinition[F]( new TestDefinition[F](
@ -286,26 +283,21 @@ object TestGroup:
tags = tags.distinct.toList, tags = tags.distinct.toList,
markers = markers.distinct.toList, markers = markers.distinct.toList,
iterations = iterations, iterations = iterations,
unitOfWork = unitOfWork, unitOfWork = UnitOfWork.apply(unitOfWork),
sourcePosition = pos sourcePosition = pos
) )
) )
type Foo[A] = EitherT[F, TestFailure, A] /** Helper type for representing `span => EitherT[F, TestFailure, Any]`
*/
type Bar[A] = F[Either[TestFailure, A]] type ET[A] = EitherT[F, 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. /** Finalize and register this test with an effectful unit of work.
* *
* @param unitOfWork * @param unitOfWork
* The function this test will execute. * The function this test will execute.
*/ */
def apply(unitOfWork: => UnitOfWork[[A] =>> Kleisli[Foo, Span[Foo], A]]) def apply(unitOfWork: => Kleisli[ET, Span[F], Any]): Unit =
: Unit =
registry.register( registry.register(
new TestDefinition[F]( new TestDefinition[F](
name = name, name = name,
@ -314,7 +306,7 @@ object TestGroup:
tags = tags.distinct.toList, tags = tags.distinct.toList,
markers = markers.distinct.toList, markers = markers.distinct.toList,
iterations = iterations, iterations = iterations,
unitOfWork = UnitOfWork[F](unitOfWork.work.mapK(FooToBar)), unitOfWork = UnitOfWork[F].apply(unitOfWork.mapF(_.value)),
sourcePosition = pos sourcePosition = pos
) )
) )
@ -342,7 +334,7 @@ object TestGroup:
* @param iterations * @param iterations
* Number of iterations to run this test. * 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 registry: Registry[F],
val name: TestDefinition.Name, val name: TestDefinition.Name,
val permanentId: PermanentId, val permanentId: PermanentId,
@ -414,22 +406,16 @@ object TestGroup:
* The function this test will execute. * The function this test will execute.
*/ */
def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit = 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. /** Finalize and register this test with an effectful unit of work.
* *
* @param unitOfWork * @param unitOfWork
* The function this test will execute. * The function this test will execute.
*/ */
def effectful(unitOfWork: Input => F[Either[TestFailure, Unit]]): Unit = def effectful(
apply(input => EitherT(unitOfWork(input))) unitOfWork: Input => Kleisli[F, Span[F], Either[TestFailure, Any]]
): Unit =
/** 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 =
registry.register( registry.register(
new TestDefinition[F]( new TestDefinition[F](
name = name, name = name,
@ -438,7 +424,40 @@ object TestGroup:
tags = tags.distinct.toList, tags = tags.distinct.toList,
markers = markers.distinct.toList, markers = markers.distinct.toList,
iterations = iterations, 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 sourcePosition = pos
) )
) )

View file

@ -2,17 +2,30 @@ package gs.test.v0.definition
import cats.data.Kleisli import cats.data.Kleisli
import natchez.Span import natchez.Span
import natchez.Trace
trait UnitOfWork[F[_]]: trait UnitOfWork[F[_]]:
def work( def work(
using span: Span[F]
Trace[F] ): F[Either[TestFailure, Any]]
): F[Either[TestFailure, Unit]]
object UnitOfWork: 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 end UnitOfWork

View file

@ -1,5 +1,6 @@
package gs.test.v0.execution package gs.test.v0.execution
import cats.Show
import gs.test.v0.definition.Marker import gs.test.v0.definition.Marker
import gs.test.v0.definition.PermanentId import gs.test.v0.definition.PermanentId
import gs.test.v0.definition.Tag import gs.test.v0.definition.Tag
@ -40,7 +41,7 @@ case class TestExecution(
documentation: Option[String], documentation: Option[String],
tags: List[Tag], tags: List[Tag],
markers: List[Marker], markers: List[Marker],
result: Either[TestFailure, Unit], result: Either[TestFailure, Any],
traceId: UUID, traceId: UUID,
sourcePosition: SourcePosition, sourcePosition: SourcePosition,
duration: FiniteDuration duration: FiniteDuration
@ -65,6 +66,8 @@ object TestExecution:
given CanEqual[Id, Id] = CanEqual.derived given CanEqual[Id, Id] = CanEqual.derived
given Show[Id] = id => id.toUUID().withoutDashes()
extension (id: Id) extension (id: Id)
/** @return /** @return
* The underlying UUID. * The underlying UUID.

View file

@ -1,7 +1,6 @@
package gs.test.v0.execution.engine package gs.test.v0.execution.engine
import gs.test.v0.execution.SuiteExecution import gs.test.v0.execution.SuiteExecution
import gs.test.v0.execution.TestExecution
final class EngineResult( final class EngineResult(
val suiteExecution: SuiteExecution val suiteExecution: SuiteExecution

View file

@ -1,8 +1,9 @@
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.TestDefinition
import gs.test.v0.definition.TestFailure
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.SuiteExecution
@ -64,43 +65,69 @@ final class TestEngine[F[_]: Async](
private def reportTestExecution(testExecution: TestExecution): F[Unit] = ??? 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( def runGroup(
group: TestGroupDefinition[F] group: TestGroupDefinition[F]
): F[GroupResult] = ): F[GroupResult] =
for entryPoint.root("test-group").use { rootSpan =>
_ <- group.beforeGroup.getOrElse(Async[F].unit) for
stream <- executeGroupTests(group) _ <- rootSpan.put("test_group_name" -> group.name.show)
_ <- group.afterGroup.getOrElse(Async[F].unit) _ <- runSpan(
yield stream "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] = private def executeGroupTests(
for group: TestGroupDefinition[F],
timer <- timing.start() rootSpan: Span[F]
_ <- group.beforeGroup.getOrElse(Async[F].unit) ): F[GroupResult] =
executions <- streamGroupTests(group).compile.toList rootSpan.span("group").use { groupSpan =>
_ <- group.afterGroup.getOrElse(Async[F].unit) for
elapsed <- timer.checkpoint() traceId <- rootSpan.traceId.map(parseTraceId)
yield new GroupResult( timer <- timing.start()
name = group.name, executions <- streamGroupTests(group, groupSpan).compile.toList
documentation = group.documentation, elapsed <- timer.checkpoint()
duration = elapsed.duration, yield new GroupResult(
testExecutions = executions name = group.name,
) documentation = group.documentation,
duration = elapsed.duration,
testExecutions = executions
)
}
private def streamGroupTests(group: TestGroupDefinition[F]) private def streamGroupTests(
: fs2.Stream[F, TestExecution] = group: TestGroupDefinition[F],
groupSpan: Span[F]
): fs2.Stream[F, TestExecution] =
fs2.Stream fs2.Stream
.emits(group.tests) .emits(group.tests)
.mapAsync(configuration.testConcurrency.toInt()) { test => .mapAsync(configuration.testConcurrency.toInt()) { test =>
for for
testExecutionId <- Async[F].delay(testExecutionIdGenerator.next()) testExecutionId <- Async[F].delay(
timer <- timing.start() TestExecution.Id(testExecutionIdGenerator.next())
_ <- group.beforeEachTest.getOrElse(Async[F].unit) )
result <- test.unitOfWork timer <- timing.start()
_ <- group.afterEachTest.getOrElse(Async[F].unit) _ <- group.beforeEachTest.getOrElse(Async[F].unit)
elapsed <- timer.checkpoint() result <- runSingleTest(testExecutionId, test, groupSpan)
_ <- group.afterEachTest.getOrElse(Async[F].unit)
elapsed <- timer.checkpoint()
yield TestExecution( yield TestExecution(
id = TestExecution.Id(testExecutionId), id = testExecutionId,
permanentId = test.permanentId, permanentId = test.permanentId,
documentation = test.documentation, documentation = test.documentation,
tags = test.tags, tags = test.tags,
@ -111,3 +138,19 @@ final class TestEngine[F[_]: Async](
duration = elapsed.duration 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 = ???

View file

@ -1 +1 @@
sbt.version=1.10.1 sbt.version=1.10.2