WIP - More engine work, tying everything together.
This commit is contained in:
parent
9c8ae5ca9b
commit
3f41e23478
8 changed files with 144 additions and 69 deletions
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,21 +65,42 @@ 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] =
|
||||||
|
entryPoint.root("test-group").use { rootSpan =>
|
||||||
for
|
for
|
||||||
_ <- group.beforeGroup.getOrElse(Async[F].unit)
|
_ <- rootSpan.put("test_group_name" -> group.name.show)
|
||||||
stream <- executeGroupTests(group)
|
_ <- runSpan(
|
||||||
_ <- group.afterGroup.getOrElse(Async[F].unit)
|
"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
|
yield stream
|
||||||
|
}
|
||||||
|
|
||||||
private def executeGroupTests(group: TestGroupDefinition[F]): F[GroupResult] =
|
private def executeGroupTests(
|
||||||
|
group: TestGroupDefinition[F],
|
||||||
|
rootSpan: Span[F]
|
||||||
|
): F[GroupResult] =
|
||||||
|
rootSpan.span("group").use { groupSpan =>
|
||||||
for
|
for
|
||||||
|
traceId <- rootSpan.traceId.map(parseTraceId)
|
||||||
timer <- timing.start()
|
timer <- timing.start()
|
||||||
_ <- group.beforeGroup.getOrElse(Async[F].unit)
|
executions <- streamGroupTests(group, groupSpan).compile.toList
|
||||||
executions <- streamGroupTests(group).compile.toList
|
|
||||||
_ <- group.afterGroup.getOrElse(Async[F].unit)
|
|
||||||
elapsed <- timer.checkpoint()
|
elapsed <- timer.checkpoint()
|
||||||
yield new GroupResult(
|
yield new GroupResult(
|
||||||
name = group.name,
|
name = group.name,
|
||||||
|
@ -86,21 +108,26 @@ final class TestEngine[F[_]: Async](
|
||||||
duration = elapsed.duration,
|
duration = elapsed.duration,
|
||||||
testExecutions = executions
|
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(
|
||||||
|
TestExecution.Id(testExecutionIdGenerator.next())
|
||||||
|
)
|
||||||
timer <- timing.start()
|
timer <- timing.start()
|
||||||
_ <- group.beforeEachTest.getOrElse(Async[F].unit)
|
_ <- group.beforeEachTest.getOrElse(Async[F].unit)
|
||||||
result <- test.unitOfWork
|
result <- runSingleTest(testExecutionId, test, groupSpan)
|
||||||
_ <- group.afterEachTest.getOrElse(Async[F].unit)
|
_ <- group.afterEachTest.getOrElse(Async[F].unit)
|
||||||
elapsed <- timer.checkpoint()
|
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 = ???
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
sbt.version=1.10.1
|
sbt.version=1.10.2
|
||||||
|
|
Loading…
Add table
Reference in a new issue