Successfully run an engine with no tests.
This commit is contained in:
parent
311ab17d5f
commit
3137fe4005
7 changed files with 266 additions and 34 deletions
|
@ -41,7 +41,7 @@ val Deps = new {
|
||||||
val Gs = new {
|
val Gs = new {
|
||||||
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.1"
|
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.1"
|
||||||
val Timing: ModuleID = "gs" %% "gs-timing-v0" % "0.1.2"
|
val Timing: ModuleID = "gs" %% "gs-timing-v0" % "0.1.2"
|
||||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.1"
|
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.1.1"
|
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.1.1"
|
||||||
|
@ -78,7 +78,8 @@ lazy val `test-support` = project
|
||||||
.settings(
|
.settings(
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
Deps.Cats.Core,
|
Deps.Cats.Core,
|
||||||
Deps.Cats.Effect
|
Deps.Cats.Effect,
|
||||||
|
Deps.Natchez.Core
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package gs.test.v0.reporting
|
||||||
|
|
||||||
import cats.effect.Async
|
import cats.effect.Async
|
||||||
import cats.effect.Ref
|
import cats.effect.Ref
|
||||||
|
import cats.effect.Resource
|
||||||
import cats.effect.std.Queue
|
import cats.effect.std.Queue
|
||||||
import cats.syntax.all.*
|
import cats.syntax.all.*
|
||||||
import gs.test.v0.api.GroupResult
|
import gs.test.v0.api.GroupResult
|
||||||
|
@ -10,7 +11,7 @@ import gs.test.v0.api.TestExecution
|
||||||
|
|
||||||
final class InMemoryReporter[F[_]: Async] private (
|
final class InMemoryReporter[F[_]: Async] private (
|
||||||
suiteExecution: Ref[F, Option[SuiteExecution]],
|
suiteExecution: Ref[F, Option[SuiteExecution]],
|
||||||
groupResults: Queue[F, (GroupResult, List[TestExecution])]
|
groupResults: Queue[F, Option[(GroupResult, List[TestExecution])]]
|
||||||
) extends Reporter[F]:
|
) extends Reporter[F]:
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
|
@ -22,7 +23,7 @@ final class InMemoryReporter[F[_]: Async] private (
|
||||||
override def reportGroup(
|
override def reportGroup(
|
||||||
groupResult: GroupResult,
|
groupResult: GroupResult,
|
||||||
testExecutions: List[TestExecution]
|
testExecutions: List[TestExecution]
|
||||||
): F[Unit] = groupResults.offer(groupResult -> testExecutions)
|
): F[Unit] = groupResults.offer(Some(groupResult -> testExecutions))
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
|
@ -31,20 +32,22 @@ final class InMemoryReporter[F[_]: Async] private (
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def endReport(): F[Unit] = Async[F].unit
|
override def endReport(): F[Unit] = groupResults.offer(None)
|
||||||
|
|
||||||
def getSuiteExecution(): F[Option[SuiteExecution]] = suiteExecution.get
|
def getSuiteExecution(): F[Option[SuiteExecution]] = suiteExecution.get
|
||||||
|
|
||||||
// TODO: make stream to consume and reify to list
|
def terminateAndGetResults(): F[List[(GroupResult, List[TestExecution])]] =
|
||||||
def getGroupResults(): F[List[(GroupResult, List[TestExecution])]] =
|
endReport() *>
|
||||||
???
|
fs2.Stream.fromQueueNoneTerminated(groupResults).compile.toList
|
||||||
|
|
||||||
object InMemoryReporter:
|
object InMemoryReporter:
|
||||||
|
|
||||||
def initialize[F[_]: Async]: F[InMemoryReporter[F]] =
|
def provision[F[_]: Async]: Resource[F, InMemoryReporter[F]] =
|
||||||
for
|
Resource.make(
|
||||||
se <- Ref.of[F, Option[SuiteExecution]](None)
|
for
|
||||||
gr <- Queue.unbounded[F, (GroupResult, List[TestExecution])]
|
se <- Ref.of[F, Option[SuiteExecution]](None)
|
||||||
yield new InMemoryReporter(se, gr)
|
gr <- Queue.unbounded[F, Option[(GroupResult, List[TestExecution])]]
|
||||||
|
yield new InMemoryReporter(se, gr)
|
||||||
|
)(_ => Async[F].unit)
|
||||||
|
|
||||||
end InMemoryReporter
|
end InMemoryReporter
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package gs.test.v0.runtime.engine
|
package gs.test.v0.runtime.engine
|
||||||
|
|
||||||
import cats.effect.Async
|
import cats.effect.Async
|
||||||
import cats.effect.Resource
|
|
||||||
import cats.syntax.all.*
|
import cats.syntax.all.*
|
||||||
import gs.test.v0.api.GroupResult
|
import gs.test.v0.api.GroupResult
|
||||||
import gs.test.v0.api.SuiteExecution
|
import gs.test.v0.api.SuiteExecution
|
||||||
|
@ -51,8 +50,8 @@ import natchez.Span
|
||||||
*/
|
*/
|
||||||
final class TestEngine[F[_]: Async](
|
final class TestEngine[F[_]: Async](
|
||||||
val configuration: EngineConfiguration,
|
val configuration: EngineConfiguration,
|
||||||
reporter: Reporter[F],
|
val reporter: Reporter[F],
|
||||||
entryPoint: EntryPoint[F],
|
val entryPoint: EntryPoint[F],
|
||||||
timing: Timing[F]
|
timing: Timing[F]
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -310,32 +309,29 @@ final class TestEngine[F[_]: Async](
|
||||||
|
|
||||||
object TestEngine:
|
object TestEngine:
|
||||||
|
|
||||||
/** Provision a new [[TestEngine]].
|
/** Initialize a new [[TestEngine]].
|
||||||
*
|
*
|
||||||
* @param configuration
|
* @param configuration
|
||||||
* The [[EngineConfiguration]] used for this instance.
|
* The [[EngineConfiguration]] used for this instance.
|
||||||
* @param reporter
|
* @param reporter
|
||||||
* Resource which manages the [[Reporter]].
|
* Reports test results.
|
||||||
* @param entryPoint
|
* @param entryPoint
|
||||||
* Resource which manages the telemetry entry point.
|
* Entry point for OpenTelemetry support.
|
||||||
* @param timing
|
* @param timing
|
||||||
* Timing controller.
|
* Timing controller.
|
||||||
* @return
|
* @return
|
||||||
* Resource which manages the [[TestEngine]].
|
* Resource which manages the [[TestEngine]].
|
||||||
*/
|
*/
|
||||||
def provision[F[_]: Async](
|
def initialize[F[_]: Async](
|
||||||
configuration: EngineConfiguration,
|
configuration: EngineConfiguration,
|
||||||
reporter: Resource[F, Reporter[F]],
|
reporter: Reporter[F],
|
||||||
entryPoint: Resource[F, EntryPoint[F]],
|
entryPoint: EntryPoint[F],
|
||||||
timing: Timing[F]
|
timing: Timing[F]
|
||||||
): Resource[F, TestEngine[F]] =
|
): TestEngine[F] =
|
||||||
for
|
new TestEngine(
|
||||||
r <- reporter
|
|
||||||
ep <- entryPoint
|
|
||||||
yield new TestEngine(
|
|
||||||
configuration = configuration,
|
configuration = configuration,
|
||||||
reporter = r,
|
reporter = reporter,
|
||||||
entryPoint = ep,
|
entryPoint = entryPoint,
|
||||||
timing = timing
|
timing = timing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package gs.test.v0.runtime.engine
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.effect.Resource
|
||||||
|
import gs.test.v0.api.TestGroupDefinition
|
||||||
|
import gs.test.v0.reporting.InMemoryReporter
|
||||||
|
import gs.test.v0.runtime.engine.TestEngineTests.EngineObservation
|
||||||
|
import gs.timing.v0.MonotonicProvider.ManualTickProvider
|
||||||
|
import gs.timing.v0.Timing
|
||||||
|
import gs.uuid.v0.UUID
|
||||||
|
import java.time.Clock
|
||||||
|
import munit.*
|
||||||
|
import support.*
|
||||||
|
|
||||||
|
class TestEngineTests extends IOSuite:
|
||||||
|
|
||||||
|
import TestEngineTests.TestData
|
||||||
|
|
||||||
|
iotest("should run an engine with no tests") {
|
||||||
|
newEngine().use { obs =>
|
||||||
|
for
|
||||||
|
suiteExecution <- obs.engine.runSuite(
|
||||||
|
suite = Generators.testSuite(),
|
||||||
|
tests = emptyStream[TestGroupDefinition[IO]]
|
||||||
|
)
|
||||||
|
rootSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.RootSpan)
|
||||||
|
results <- obs.reporter.terminateAndGetResults()
|
||||||
|
yield
|
||||||
|
assertEquals(rootSpan.isDefined, true)
|
||||||
|
assertEquals(results.isEmpty, true)
|
||||||
|
assertEquals(suiteExecution.seen, 0L)
|
||||||
|
assertEquals(suiteExecution.passed, 0L)
|
||||||
|
assertEquals(suiteExecution.failed, 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def emptyStream[A]: fs2.Stream[IO, A] =
|
||||||
|
fs2.Stream.empty
|
||||||
|
|
||||||
|
private def liftToResource[A](io: IO[A]): Resource[IO, A] =
|
||||||
|
Resource.make(io)(_ => IO.unit)
|
||||||
|
|
||||||
|
def newEngine(): Resource[IO, EngineObservation] =
|
||||||
|
for
|
||||||
|
(tickProvider, timing) <- liftToResource(Timing.manual[IO])
|
||||||
|
reporter <- InMemoryReporter.provision[IO]
|
||||||
|
entryPoint <- TestEntryPoint.provision()
|
||||||
|
yield EngineObservation(
|
||||||
|
tickProvider = tickProvider,
|
||||||
|
reporter = reporter,
|
||||||
|
entryPoint = entryPoint,
|
||||||
|
engine = TestEngine.initialize[IO](
|
||||||
|
configuration = TestData.Config,
|
||||||
|
reporter = reporter,
|
||||||
|
entryPoint = entryPoint,
|
||||||
|
timing = timing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
object TestEngineTests:
|
||||||
|
|
||||||
|
private object TestData:
|
||||||
|
|
||||||
|
val Config: EngineConfiguration = EngineConfiguration(
|
||||||
|
groupConcurrency = ConcurrencySetting.Serial,
|
||||||
|
testConcurrency = ConcurrencySetting.Serial,
|
||||||
|
clock = Clock.systemUTC(),
|
||||||
|
suiteIdGenerator = UUID.Generator.version7,
|
||||||
|
testIdGenerator = UUID.Generator.version7
|
||||||
|
)
|
||||||
|
|
||||||
|
end TestData
|
||||||
|
|
||||||
|
case class EngineObservation(
|
||||||
|
tickProvider: ManualTickProvider[IO],
|
||||||
|
reporter: InMemoryReporter[IO],
|
||||||
|
entryPoint: TestEntryPoint,
|
||||||
|
engine: TestEngine[IO]
|
||||||
|
)
|
||||||
|
|
||||||
|
end TestEngineTests
|
|
@ -64,16 +64,24 @@ object Generators:
|
||||||
)
|
)
|
||||||
|
|
||||||
val GenTestExecution: Gen[TestExecution] =
|
val GenTestExecution: Gen[TestExecution] =
|
||||||
Gen.boolean().map(passed => InputGenTestExecution.generate(passed))
|
Gen.boolean().satisfy(InputGenTestExecution)
|
||||||
|
|
||||||
val GenTestExecutionPassed: Gen[TestExecution] =
|
val GenTestExecutionPassed: Gen[TestExecution] =
|
||||||
Gen.single(true).map(InputGenTestExecution.generate)
|
InputGenTestExecution.toGen(true)
|
||||||
|
|
||||||
val GenTestExecutionFailed: Gen[TestExecution] =
|
val GenTestExecutionFailed: Gen[TestExecution] =
|
||||||
Gen.single(false).map(InputGenTestExecution.generate)
|
InputGenTestExecution.toGen(false)
|
||||||
|
|
||||||
given Generated[TestExecution] = Generated.of(GenTestExecution)
|
given Generated[TestExecution] = Generated.of(GenTestExecution)
|
||||||
|
|
||||||
|
val GenTestSuite: Gen[TestSuite] =
|
||||||
|
for
|
||||||
|
pid <- GenPermanentId
|
||||||
|
name <- Gen.string.alphaNumeric(Size.fixed(8))
|
||||||
|
yield TestSuite(pid, name, None)
|
||||||
|
|
||||||
|
given Generated[TestSuite] = Generated.of(GenTestSuite)
|
||||||
|
|
||||||
def testExecutionId(): TestExecution.Id = GenTestExecutionId.gen()
|
def testExecutionId(): TestExecution.Id = GenTestExecutionId.gen()
|
||||||
|
|
||||||
def permanentId(): PermanentId = GenPermanentId.gen()
|
def permanentId(): PermanentId = GenPermanentId.gen()
|
||||||
|
@ -91,9 +99,11 @@ object Generators:
|
||||||
def testExecution(): TestExecution = GenTestExecution.gen()
|
def testExecution(): TestExecution = GenTestExecution.gen()
|
||||||
|
|
||||||
def testExecutionPassed(): TestExecution =
|
def testExecutionPassed(): TestExecution =
|
||||||
InputGenTestExecution.generate(true)
|
GenTestExecutionPassed.gen()
|
||||||
|
|
||||||
def testExecutionFailed(): TestExecution =
|
def testExecutionFailed(): TestExecution =
|
||||||
InputGenTestExecution.generate(false)
|
GenTestExecutionFailed.gen()
|
||||||
|
|
||||||
|
def testSuite(): TestSuite = GenTestSuite.gen()
|
||||||
|
|
||||||
end Generators
|
end Generators
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package support
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.effect.kernel.Resource
|
||||||
|
import cats.effect.std.MapRef
|
||||||
|
import natchez.EntryPoint
|
||||||
|
import natchez.Kernel
|
||||||
|
import natchez.Span
|
||||||
|
import natchez.Span.Options
|
||||||
|
|
||||||
|
final class TestEntryPoint private (
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
) extends EntryPoint[IO]:
|
||||||
|
|
||||||
|
def getSpan(name: String): IO[Option[TestSpan]] =
|
||||||
|
spans(name).get
|
||||||
|
|
||||||
|
override def root(
|
||||||
|
name: String,
|
||||||
|
options: Options
|
||||||
|
): Resource[IO, Span[IO]] =
|
||||||
|
TestSpan
|
||||||
|
.provisionRoot(name, spans)
|
||||||
|
.evalTap(span => spans.setKeyValue(name, span))
|
||||||
|
|
||||||
|
override def continue(
|
||||||
|
name: String,
|
||||||
|
kernel: Kernel,
|
||||||
|
options: Options
|
||||||
|
): Resource[IO, Span[IO]] =
|
||||||
|
throw new IllegalStateException("Not allowed for testing.")
|
||||||
|
|
||||||
|
override def continueOrElseRoot(
|
||||||
|
name: String,
|
||||||
|
kernel: Kernel,
|
||||||
|
options: Options
|
||||||
|
): Resource[IO, Span[IO]] =
|
||||||
|
throw new IllegalStateException("Not allowed for testing.")
|
||||||
|
|
||||||
|
object TestEntryPoint:
|
||||||
|
|
||||||
|
def initialize(): IO[TestEntryPoint] =
|
||||||
|
MapRef.apply[IO, String, TestSpan].map(spans => new TestEntryPoint(spans))
|
||||||
|
|
||||||
|
def provision(): Resource[IO, TestEntryPoint] =
|
||||||
|
Resource.make(initialize())(_ => IO.unit)
|
||||||
|
|
||||||
|
end TestEntryPoint
|
93
modules/test-support/src/test/scala/support/TestSpan.scala
Normal file
93
modules/test-support/src/test/scala/support/TestSpan.scala
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package support
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.effect.kernel.Resource
|
||||||
|
import cats.effect.std.MapRef
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.UUID
|
||||||
|
import natchez.Kernel
|
||||||
|
import natchez.Span
|
||||||
|
import natchez.Span.Options
|
||||||
|
import natchez.TraceValue
|
||||||
|
|
||||||
|
final class TestSpan private (
|
||||||
|
val name: String,
|
||||||
|
val rawTraceId: String,
|
||||||
|
val rawSpanId: String,
|
||||||
|
baggage: MapRef[IO, String, Option[TraceValue]],
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
) extends Span[IO]:
|
||||||
|
|
||||||
|
override def put(fields: (String, TraceValue)*): IO[Unit] =
|
||||||
|
fields.map { case (k, v) => baggage.setKeyValue(k, v) }.sequence.as(())
|
||||||
|
|
||||||
|
override def log(fields: (String, TraceValue)*): IO[Unit] = IO.unit
|
||||||
|
|
||||||
|
override def log(event: String): IO[Unit] = IO.unit
|
||||||
|
|
||||||
|
override def attachError(
|
||||||
|
err: Throwable,
|
||||||
|
fields: (String, TraceValue)*
|
||||||
|
): IO[Unit] = IO.unit
|
||||||
|
|
||||||
|
override def kernel: IO[Kernel] = IO(Kernel(Map.empty))
|
||||||
|
|
||||||
|
override def span(
|
||||||
|
name: String,
|
||||||
|
options: Options
|
||||||
|
): Resource[IO, Span[IO]] =
|
||||||
|
TestSpan
|
||||||
|
.provision(name, rawTraceId, TestSpan.makeSpanId(), spans)
|
||||||
|
.evalTap(span => spans.setKeyValue(name, span))
|
||||||
|
|
||||||
|
override def traceId: IO[Option[String]] = IO(Some(rawTraceId))
|
||||||
|
|
||||||
|
override def spanId: IO[Option[String]] = IO(Some(rawSpanId))
|
||||||
|
|
||||||
|
override def traceUri: IO[Option[URI]] = IO(None)
|
||||||
|
|
||||||
|
object TestSpan:
|
||||||
|
|
||||||
|
def initializeRoot(
|
||||||
|
name: String,
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
): IO[TestSpan] =
|
||||||
|
initialize(name, makeTraceId(), makeSpanId(), spans)
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
name: String,
|
||||||
|
traceId: String,
|
||||||
|
spanId: String,
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
): IO[TestSpan] =
|
||||||
|
MapRef.apply[IO, String, TraceValue].map { baggage =>
|
||||||
|
new TestSpan(
|
||||||
|
name = name,
|
||||||
|
rawTraceId = traceId,
|
||||||
|
rawSpanId = spanId,
|
||||||
|
baggage = baggage,
|
||||||
|
spans = spans
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def provisionRoot(
|
||||||
|
name: String,
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
): Resource[IO, TestSpan] =
|
||||||
|
provision(name, makeTraceId(), makeSpanId(), spans)
|
||||||
|
|
||||||
|
def provision(
|
||||||
|
name: String,
|
||||||
|
traceId: String,
|
||||||
|
spanId: String,
|
||||||
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
|
): Resource[IO, TestSpan] =
|
||||||
|
Resource.make(initialize(name, traceId, spanId, spans))(_ => IO.unit)
|
||||||
|
|
||||||
|
private def makeTraceId(): String =
|
||||||
|
UUID.randomUUID().toString().filterNot(_ == '-')
|
||||||
|
|
||||||
|
private def makeSpanId(): String =
|
||||||
|
makeTraceId().take(16)
|
||||||
|
|
||||||
|
end TestSpan
|
Loading…
Add table
Reference in a new issue