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 / versionScheme := Some("semver-spec")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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