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 Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.1"
|
||||
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"
|
||||
|
@ -78,7 +78,8 @@ lazy val `test-support` = project
|
|||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
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.Ref
|
||||
import cats.effect.Resource
|
||||
import cats.effect.std.Queue
|
||||
import cats.syntax.all.*
|
||||
import gs.test.v0.api.GroupResult
|
||||
|
@ -10,7 +11,7 @@ import gs.test.v0.api.TestExecution
|
|||
|
||||
final class InMemoryReporter[F[_]: Async] private (
|
||||
suiteExecution: Ref[F, Option[SuiteExecution]],
|
||||
groupResults: Queue[F, (GroupResult, List[TestExecution])]
|
||||
groupResults: Queue[F, Option[(GroupResult, List[TestExecution])]]
|
||||
) extends Reporter[F]:
|
||||
|
||||
/** @inheritDocs
|
||||
|
@ -22,7 +23,7 @@ final class InMemoryReporter[F[_]: Async] private (
|
|||
override def reportGroup(
|
||||
groupResult: GroupResult,
|
||||
testExecutions: List[TestExecution]
|
||||
): F[Unit] = groupResults.offer(groupResult -> testExecutions)
|
||||
): F[Unit] = groupResults.offer(Some(groupResult -> testExecutions))
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
|
@ -31,20 +32,22 @@ final class InMemoryReporter[F[_]: Async] private (
|
|||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def endReport(): F[Unit] = Async[F].unit
|
||||
override def endReport(): F[Unit] = groupResults.offer(None)
|
||||
|
||||
def getSuiteExecution(): F[Option[SuiteExecution]] = suiteExecution.get
|
||||
|
||||
// TODO: make stream to consume and reify to list
|
||||
def getGroupResults(): F[List[(GroupResult, List[TestExecution])]] =
|
||||
???
|
||||
def terminateAndGetResults(): F[List[(GroupResult, List[TestExecution])]] =
|
||||
endReport() *>
|
||||
fs2.Stream.fromQueueNoneTerminated(groupResults).compile.toList
|
||||
|
||||
object InMemoryReporter:
|
||||
|
||||
def initialize[F[_]: Async]: F[InMemoryReporter[F]] =
|
||||
for
|
||||
se <- Ref.of[F, Option[SuiteExecution]](None)
|
||||
gr <- Queue.unbounded[F, (GroupResult, List[TestExecution])]
|
||||
yield new InMemoryReporter(se, gr)
|
||||
def provision[F[_]: Async]: Resource[F, InMemoryReporter[F]] =
|
||||
Resource.make(
|
||||
for
|
||||
se <- Ref.of[F, Option[SuiteExecution]](None)
|
||||
gr <- Queue.unbounded[F, Option[(GroupResult, List[TestExecution])]]
|
||||
yield new InMemoryReporter(se, gr)
|
||||
)(_ => Async[F].unit)
|
||||
|
||||
end InMemoryReporter
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package gs.test.v0.runtime.engine
|
||||
|
||||
import cats.effect.Async
|
||||
import cats.effect.Resource
|
||||
import cats.syntax.all.*
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
|
@ -51,8 +50,8 @@ import natchez.Span
|
|||
*/
|
||||
final class TestEngine[F[_]: Async](
|
||||
val configuration: EngineConfiguration,
|
||||
reporter: Reporter[F],
|
||||
entryPoint: EntryPoint[F],
|
||||
val reporter: Reporter[F],
|
||||
val entryPoint: EntryPoint[F],
|
||||
timing: Timing[F]
|
||||
):
|
||||
|
||||
|
@ -310,32 +309,29 @@ final class TestEngine[F[_]: Async](
|
|||
|
||||
object TestEngine:
|
||||
|
||||
/** Provision a new [[TestEngine]].
|
||||
/** Initialize a new [[TestEngine]].
|
||||
*
|
||||
* @param configuration
|
||||
* The [[EngineConfiguration]] used for this instance.
|
||||
* @param reporter
|
||||
* Resource which manages the [[Reporter]].
|
||||
* Reports test results.
|
||||
* @param entryPoint
|
||||
* Resource which manages the telemetry entry point.
|
||||
* Entry point for OpenTelemetry support.
|
||||
* @param timing
|
||||
* Timing controller.
|
||||
* @return
|
||||
* Resource which manages the [[TestEngine]].
|
||||
*/
|
||||
def provision[F[_]: Async](
|
||||
def initialize[F[_]: Async](
|
||||
configuration: EngineConfiguration,
|
||||
reporter: Resource[F, Reporter[F]],
|
||||
entryPoint: Resource[F, EntryPoint[F]],
|
||||
reporter: Reporter[F],
|
||||
entryPoint: EntryPoint[F],
|
||||
timing: Timing[F]
|
||||
): Resource[F, TestEngine[F]] =
|
||||
for
|
||||
r <- reporter
|
||||
ep <- entryPoint
|
||||
yield new TestEngine(
|
||||
): TestEngine[F] =
|
||||
new TestEngine(
|
||||
configuration = configuration,
|
||||
reporter = r,
|
||||
entryPoint = ep,
|
||||
reporter = reporter,
|
||||
entryPoint = entryPoint,
|
||||
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] =
|
||||
Gen.boolean().map(passed => InputGenTestExecution.generate(passed))
|
||||
Gen.boolean().satisfy(InputGenTestExecution)
|
||||
|
||||
val GenTestExecutionPassed: Gen[TestExecution] =
|
||||
Gen.single(true).map(InputGenTestExecution.generate)
|
||||
InputGenTestExecution.toGen(true)
|
||||
|
||||
val GenTestExecutionFailed: Gen[TestExecution] =
|
||||
Gen.single(false).map(InputGenTestExecution.generate)
|
||||
InputGenTestExecution.toGen(false)
|
||||
|
||||
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 permanentId(): PermanentId = GenPermanentId.gen()
|
||||
|
@ -91,9 +99,11 @@ object Generators:
|
|||
def testExecution(): TestExecution = GenTestExecution.gen()
|
||||
|
||||
def testExecutionPassed(): TestExecution =
|
||||
InputGenTestExecution.generate(true)
|
||||
GenTestExecutionPassed.gen()
|
||||
|
||||
def testExecutionFailed(): TestExecution =
|
||||
InputGenTestExecution.generate(false)
|
||||
GenTestExecutionFailed.gen()
|
||||
|
||||
def testSuite(): TestSuite = GenTestSuite.gen()
|
||||
|
||||
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