Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
830105af3a | |||
fb831ea7d3 | |||
c2a155ceab | |||
2b905d3fb2 | |||
4d0bef4d4b | |||
b23b6cfdea |
23 changed files with 877 additions and 188 deletions
|
@ -1,5 +1,5 @@
|
|||
// See: https://github.com/scalameta/scalafmt/tags for the latest tags.
|
||||
version = 3.8.1
|
||||
version = 3.9.9
|
||||
runner.dialect = scala3
|
||||
maxColumn = 80
|
||||
|
||||
|
|
48
build.sbt
48
build.sbt
|
@ -1,4 +1,4 @@
|
|||
val scala3: String = "3.5.1"
|
||||
val scala3: String = "3.7.1"
|
||||
|
||||
ThisBuild / scalaVersion := scala3
|
||||
ThisBuild / versionScheme := Some("semver-spec")
|
||||
|
@ -10,7 +10,7 @@ ThisBuild / externalResolvers := Seq(
|
|||
)
|
||||
|
||||
ThisBuild / licenses := Seq(
|
||||
"MIT" -> url("https://garrity.co/MIT.html")
|
||||
"MIT" -> url("https://git.garrity.co/garrity-software/gs-test/LICENSE")
|
||||
)
|
||||
|
||||
val noPublishSettings = Seq(
|
||||
|
@ -26,25 +26,25 @@ val sharedSettings = Seq(
|
|||
|
||||
val Deps = new {
|
||||
val Cats = new {
|
||||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.12.0"
|
||||
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.5.4"
|
||||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
|
||||
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.6.3"
|
||||
}
|
||||
|
||||
val Fs2 = new {
|
||||
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.10.2"
|
||||
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.12.0"
|
||||
}
|
||||
|
||||
val Natchez = new {
|
||||
val Core: ModuleID = "org.tpolecat" %% "natchez-core" % "0.3.6"
|
||||
val Core: ModuleID = "org.tpolecat" %% "natchez-core" % "0.3.8"
|
||||
}
|
||||
|
||||
val Gs = new {
|
||||
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.3.0"
|
||||
val Timing: ModuleID = "gs" %% "gs-timing-v0" % "0.1.1"
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.2.0"
|
||||
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 MUnit: ModuleID = "org.scalameta" %% "munit" % "1.0.1"
|
||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.1.1"
|
||||
}
|
||||
|
||||
lazy val testSettings = Seq(
|
||||
|
@ -59,11 +59,14 @@ lazy val `gs-test` = project
|
|||
.aggregate(
|
||||
`test-support`,
|
||||
api,
|
||||
reporting,
|
||||
runtime
|
||||
)
|
||||
.settings(noPublishSettings)
|
||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||
|
||||
/** Internal project used for unit tests.
|
||||
*/
|
||||
lazy val `test-support` = project
|
||||
.in(file("modules/test-support"))
|
||||
.settings(sharedSettings)
|
||||
|
@ -79,6 +82,8 @@ lazy val `test-support` = project
|
|||
)
|
||||
)
|
||||
|
||||
/** Core API - the only dependency needed to write tests.
|
||||
*/
|
||||
lazy val api = project
|
||||
.in(file("modules/api"))
|
||||
.dependsOn(`test-support` % "test->test")
|
||||
|
@ -89,16 +94,36 @@ lazy val api = project
|
|||
)
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.Gs.Uuid,
|
||||
Deps.Cats.Core,
|
||||
Deps.Cats.Effect,
|
||||
Deps.Natchez.Core
|
||||
)
|
||||
)
|
||||
|
||||
/** Reporting API and implementations.
|
||||
*/
|
||||
lazy val reporting = project
|
||||
.in(file("modules/reporting"))
|
||||
.dependsOn(`test-support` % "test->test")
|
||||
.dependsOn(api)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(
|
||||
name := s"${gsProjectName.value}-reporting-v${semVerMajor.value}"
|
||||
)
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.Fs2.Core
|
||||
)
|
||||
)
|
||||
|
||||
/** Runtime - the dependency needed to _run_ tests.
|
||||
*/
|
||||
lazy val runtime = project
|
||||
.in(file("modules/runtime"))
|
||||
.dependsOn(`test-support` % "test->test")
|
||||
.dependsOn(api)
|
||||
.dependsOn(api, reporting)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(
|
||||
|
@ -106,7 +131,6 @@ lazy val runtime = project
|
|||
)
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.Gs.Uuid,
|
||||
Deps.Gs.Timing,
|
||||
Deps.Cats.Core,
|
||||
Deps.Cats.Effect,
|
||||
|
|
|
@ -18,7 +18,7 @@ object Check:
|
|||
*/
|
||||
def apply[A](candidate: A): Check[A] = candidate
|
||||
|
||||
extension [A: ClassTag](candidate: Check[A])
|
||||
extension [A](candidate: Check[A])
|
||||
/** @return
|
||||
* The unwrapped value of this [[Check]].
|
||||
*/
|
||||
|
@ -42,6 +42,7 @@ object Check:
|
|||
)(
|
||||
using
|
||||
CanEqual[A, A],
|
||||
ClassTag[A],
|
||||
SourcePosition
|
||||
): TestResult =
|
||||
Assertion.IsEqualTo.evaluate(candidate, expected)
|
||||
|
@ -67,6 +68,7 @@ object Check:
|
|||
)(
|
||||
using
|
||||
CanEqual[A, A],
|
||||
ClassTag[A],
|
||||
SourcePosition
|
||||
): F[TestResult] =
|
||||
Sync[F].delay(isEqualTo(expected))
|
||||
|
|
28
modules/api/src/main/scala/gs/test/v0/api/GroupResult.scala
Normal file
28
modules/api/src/main/scala/gs/test/v0/api/GroupResult.scala
Normal file
|
@ -0,0 +1,28 @@
|
|||
package gs.test.v0.api
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** Represents the results of executing an entire group of tests.
|
||||
*
|
||||
* @param name
|
||||
* The name of the executed group.
|
||||
* @param documentation
|
||||
* The documentation for the group.
|
||||
* @param duration
|
||||
* The overall duration of execution.
|
||||
* @param seen
|
||||
* The number of tests seen.
|
||||
* @param passed
|
||||
* The number of tests which passed.
|
||||
* @param failed
|
||||
* The number of tests which failed.
|
||||
*/
|
||||
final class GroupResult(
|
||||
val name: TestGroupDefinition.Name,
|
||||
val documentation: Option[String],
|
||||
val duration: FiniteDuration,
|
||||
val seen: Long,
|
||||
val passed: Long,
|
||||
val failed: Long
|
||||
):
|
||||
def millis: Long = duration.toMillis
|
|
@ -0,0 +1,36 @@
|
|||
package gs.test.v0.api
|
||||
|
||||
import gs.uuid.v0.UUID
|
||||
import java.time.Instant
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** Describes the overall result of execution a suite of tests.
|
||||
*
|
||||
* @param id
|
||||
* Unique identifier for this execution.
|
||||
* @param testSuite
|
||||
* Suite-level identifiers and metadata.
|
||||
* @param traceId
|
||||
* The 128-bit trace identifier used for this suite.
|
||||
* @param duration
|
||||
* Overall amount of time it took to execute the suite.
|
||||
* @param seen
|
||||
* Overall number of tests seen.
|
||||
* @param passed
|
||||
* Overall number of passed tests.
|
||||
* @param failed
|
||||
* Overall number of failed tests.
|
||||
* @param executedAt
|
||||
* Timestamp at which this suite was executed.
|
||||
*/
|
||||
case class SuiteExecution(
|
||||
id: UUID,
|
||||
testSuite: TestSuite,
|
||||
traceId: String,
|
||||
duration: FiniteDuration,
|
||||
seen: Long,
|
||||
passed: Long,
|
||||
failed: Long,
|
||||
executedAt: Instant
|
||||
):
|
||||
def millis: Long = duration.toMillis
|
|
@ -1,11 +1,6 @@
|
|||
package gs.test.v0.runtime
|
||||
package gs.test.v0.api
|
||||
|
||||
import cats.Show
|
||||
import gs.test.v0.api.Marker
|
||||
import gs.test.v0.api.PermanentId
|
||||
import gs.test.v0.api.SourcePosition
|
||||
import gs.test.v0.api.Tag
|
||||
import gs.test.v0.api.TestFailure
|
||||
import gs.uuid.v0.UUID
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
|
@ -26,8 +21,8 @@ import scala.concurrent.duration.FiniteDuration
|
|||
* Markers for the test that was executed.
|
||||
* @param result
|
||||
* The result of the test.
|
||||
* @param traceId
|
||||
* The 128-bit trace identifier used for this test.
|
||||
* @param spanId
|
||||
* The 64-bit span identifier used for this test (including before/after).
|
||||
* @param sourcePosition
|
||||
* The position, in source code, of the test that was executed.
|
||||
* @param duration
|
||||
|
@ -41,10 +36,23 @@ case class TestExecution(
|
|||
tags: List[Tag],
|
||||
markers: List[Marker],
|
||||
result: Either[TestFailure, Any],
|
||||
traceId: UUID,
|
||||
spanId: String,
|
||||
sourcePosition: SourcePosition,
|
||||
duration: FiniteDuration
|
||||
)
|
||||
):
|
||||
|
||||
/** @return
|
||||
* The string "passed" if the test passed, and "failed" otherwise.
|
||||
*/
|
||||
def textResult: String = result match {
|
||||
case Left(_) => "failed"
|
||||
case Right(_) => "passed"
|
||||
}
|
||||
|
||||
/** @return
|
||||
* The duration, in milliseconds, it took this test to execute.
|
||||
*/
|
||||
def millis: Long = duration.toMillis
|
||||
|
||||
object TestExecution:
|
||||
|
|
@ -2,7 +2,8 @@ package gs.test.v0.api
|
|||
|
||||
/** Base trait for all failures recognized by gs-test.
|
||||
*/
|
||||
sealed trait TestFailure
|
||||
sealed trait TestFailure:
|
||||
def message: String
|
||||
|
||||
object TestFailure:
|
||||
|
||||
|
@ -44,6 +45,7 @@ object TestFailure:
|
|||
*/
|
||||
case class ExceptionThrown(
|
||||
cause: Throwable
|
||||
) extends TestFailure
|
||||
) extends TestFailure:
|
||||
override def message: String = cause.getMessage()
|
||||
|
||||
end TestFailure
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package gs.test.v0.api
|
||||
|
||||
import cats.Show
|
||||
import cats.effect.Async
|
||||
|
||||
/** Each group is comprised of a list of [[Test]]. This list may be empty.
|
||||
*
|
||||
|
@ -18,7 +17,7 @@ import cats.effect.Async
|
|||
* @param tests
|
||||
* The list of tests in this group.
|
||||
*/
|
||||
final class TestGroupDefinition[F[_]: Async](
|
||||
final class TestGroupDefinition[F[_]](
|
||||
val name: TestGroupDefinition.Name,
|
||||
val documentation: Option[String],
|
||||
val testTags: List[Tag],
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import cats.Applicative
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
|
||||
/** No-op implementation of [[Reporter]].
|
||||
*/
|
||||
final class NoopReporter[F[_]: Applicative] extends Reporter[F]:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def startReport(): F[Unit] = Applicative[F].unit
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def reportGroup(
|
||||
groupResult: GroupResult,
|
||||
testExecutions: List[TestExecution]
|
||||
): F[Unit] = Applicative[F].unit
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def reportSuite(suiteExecution: SuiteExecution): F[Unit] =
|
||||
Applicative[F].unit
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def endReport(): F[Unit] = Applicative[F].unit
|
|
@ -0,0 +1,27 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
|
||||
final class NoopResultFormatter extends ResultFormatter:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def prefix: String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def suffix: String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatGroupResult(groupResult: GroupResult): String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatTestExecution(testExecution: TestExecution): String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatSuiteExecution(suiteExecution: SuiteExecution): String = ""
|
|
@ -0,0 +1,118 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import cats.effect.Async
|
||||
import cats.effect.Concurrent
|
||||
import cats.effect.Resource
|
||||
import cats.effect.kernel.Fiber
|
||||
import cats.effect.std.Queue
|
||||
import cats.effect.syntax.all.*
|
||||
import cats.syntax.all.*
|
||||
import fs2.text
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
import java.io.OutputStream
|
||||
|
||||
/** Implementation of [[Reporter]] that writes bytes to an `OutputStream`.
|
||||
*
|
||||
* @param formatter
|
||||
* The [[ResultFormatter]] used to render test results.
|
||||
* @param state
|
||||
* The internal state of the reporter.
|
||||
*/
|
||||
final class OutputStreamReporter[F[_]: Async] private (
|
||||
formatter: ResultFormatter,
|
||||
state: OutputStreamReporter.State[F]
|
||||
) extends Reporter[F]:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def startReport(): F[Unit] =
|
||||
write(formatter.prefix)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def reportGroup(
|
||||
groupResult: GroupResult,
|
||||
testExecutions: List[TestExecution]
|
||||
): F[Unit] =
|
||||
write(formatter.formatGroupResult(groupResult)) *>
|
||||
testExecutions
|
||||
.map(formatter.formatTestExecution)
|
||||
.map(write)
|
||||
.sequence
|
||||
.as(())
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def reportSuite(suiteExecution: SuiteExecution): F[Unit] =
|
||||
write(formatter.formatSuiteExecution(suiteExecution))
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def endReport(): F[Unit] =
|
||||
write(formatter.suffix)
|
||||
|
||||
private def write(output: String): F[Unit] =
|
||||
state.queue.offer(Some(output))
|
||||
|
||||
/** Produce an effect that, when executed, will cause the underlying stream to
|
||||
* terminate. After executing this effect, the `OutputStreamReporter` will no
|
||||
* longer be capable of writing more output.
|
||||
*
|
||||
* @return
|
||||
* The effect that describes the stop operation.
|
||||
*/
|
||||
def stop(): F[Unit] = state.queue.offer(None)
|
||||
|
||||
object OutputStreamReporter:
|
||||
|
||||
/** Provision a new [[OutputStreamReporter]].
|
||||
*
|
||||
* @param formatter
|
||||
* The [[ResultFormatter]] this reporter should use to render test results.
|
||||
* @param output
|
||||
* Resource which manages the `OutputStream` where bytes will be written.
|
||||
* @return
|
||||
* Resource which manages the [[OutputStreamReporter]].
|
||||
*/
|
||||
def provision[F[_]: Concurrent: Async](
|
||||
formatter: ResultFormatter,
|
||||
output: Resource[F, OutputStream]
|
||||
): Resource[F, OutputStreamReporter[F]] =
|
||||
output.flatMap { os =>
|
||||
Resource.make(acquireReporter(formatter, os))(_.stop())
|
||||
}
|
||||
|
||||
private def acquireReporter[F[_]: Concurrent: Async](
|
||||
formatter: ResultFormatter,
|
||||
output: OutputStream
|
||||
): F[OutputStreamReporter[F]] =
|
||||
for
|
||||
queue <- Queue.unbounded[F, Option[String]]
|
||||
process <- startProcess[F](queue, output)
|
||||
yield new OutputStreamReporter[F](
|
||||
formatter = formatter,
|
||||
state = new State[F](queue, process)
|
||||
)
|
||||
|
||||
private def startProcess[F[_]: Concurrent: Async](
|
||||
queue: Queue[F, Option[String]],
|
||||
output: OutputStream
|
||||
): F[Fiber[F, Throwable, Unit]] =
|
||||
fs2.Stream
|
||||
.fromQueueNoneTerminated(queue)
|
||||
.through(text.utf8.encode)
|
||||
.through(
|
||||
fs2.io.writeOutputStream(Async[F].delay(output), closeAfterUse = false)
|
||||
)
|
||||
.compile
|
||||
.drain
|
||||
.start
|
||||
|
||||
private class State[F[_]](
|
||||
val queue: Queue[F, Option[String]],
|
||||
val process: Fiber[F, Throwable, Unit]
|
||||
)
|
||||
|
||||
end OutputStreamReporter
|
|
@ -0,0 +1,60 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import cats.syntax.all.*
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
import gs.test.v0.api.TestFailure
|
||||
|
||||
/** Implmentation of [[ResultFormatter]] that uses an unstructured text format.
|
||||
*/
|
||||
final class PlainResultFormatter extends ResultFormatter:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def prefix: String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def suffix: String = ""
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatGroupResult(groupResult: GroupResult): String =
|
||||
def gr = groupResult
|
||||
s"""
|
||||
Group: '${gr.name.show}'
|
||||
Stats: Seen=${gr.seen} Passed=${gr.passed} Failed=${gr.failed}
|
||||
Duration: ${gr.millis}ms
|
||||
Docs: ${gr.documentation.getOrElse("None")}
|
||||
""".stripMargin
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatTestExecution(testExecution: TestExecution): String =
|
||||
def te = testExecution
|
||||
s"""
|
||||
Test: ${te.permanentId.show} (id=${te.id.show}) (span=${te.spanId})
|
||||
Result: *${te.textResult}* in ${te.millis}ms
|
||||
Tags: ${te.tags.mkString(", ")}
|
||||
Docs: ${te.documentation.getOrElse("None")}${makeFailure(te.result)}
|
||||
""".stripMargin
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def formatSuiteExecution(suiteExecution: SuiteExecution): String =
|
||||
def se = suiteExecution
|
||||
s"""
|
||||
Suite: '${se.testSuite.permanentId.show}' (id=${se.id.str}) (trace=${se.traceId})
|
||||
Name: ${se.testSuite.name}
|
||||
Stats: Seen=${se.seen} Passed=${se.passed} Failed=${se.failed}
|
||||
Duration: ${se.millis}ms
|
||||
""".stripMargin
|
||||
|
||||
private def makeFailure(result: Either[TestFailure, Any]): String =
|
||||
result match
|
||||
case Right(_) => ""
|
||||
case Left(f) =>
|
||||
s"""\n------
|
||||
${f.message}
|
||||
""".stripMargin
|
|
@ -0,0 +1,63 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import cats.Applicative
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
|
||||
/** Interface for reporters - implementations that report on test results.
|
||||
*
|
||||
* Example implementations include writing to standard output or writing to
|
||||
* some JSON-formatted file.
|
||||
*
|
||||
* ## Order of Operations
|
||||
*
|
||||
* - `beginReporting()`
|
||||
* - `reportGroup` for each group executed
|
||||
* - `reportSuite`
|
||||
* - `endReporting()`
|
||||
*/
|
||||
trait Reporter[F[_]]:
|
||||
/** Hook for the beginning of the reporting lifecycle. This allows
|
||||
* implementations to perform "setup" actions, such as opening a JSON object
|
||||
* or writing a header.
|
||||
*/
|
||||
def startReport(): F[Unit]
|
||||
|
||||
/** Report the results of a single group.
|
||||
*
|
||||
* @param groupResult
|
||||
* The [[GroupResult]] that describes the group level summary.
|
||||
* @param testExecutions
|
||||
* The list of [[TestExecution]] describing the result of each test.
|
||||
* @return
|
||||
* Side-effect that describes the reporting operation.
|
||||
*/
|
||||
def reportGroup(
|
||||
groupResult: GroupResult,
|
||||
testExecutions: List[TestExecution]
|
||||
): F[Unit]
|
||||
|
||||
/** Report the results of an entire suite.
|
||||
*
|
||||
* @param suiteExecution
|
||||
* The [[SuiteExecution]] that describes results.
|
||||
* @return
|
||||
* Side-effect that describes the reporting operation.
|
||||
*/
|
||||
def reportSuite(suiteExecution: SuiteExecution): F[Unit]
|
||||
|
||||
/** Hook for the end of the reporting lifecycle. This allows implementations
|
||||
* to perform "finish" actions, such as closing a JSON object or writing a
|
||||
* footer.
|
||||
*/
|
||||
def endReport(): F[Unit]
|
||||
|
||||
object Reporter:
|
||||
|
||||
/** @return
|
||||
* New instance of the no-op Reporter implementation.
|
||||
*/
|
||||
def noop[F[_]: Applicative]: Reporter[F] = new NoopReporter[F]
|
||||
|
||||
end Reporter
|
|
@ -0,0 +1,49 @@
|
|||
package gs.test.v0.reporting
|
||||
|
||||
import gs.test.v0.api.GroupResult
|
||||
import gs.test.v0.api.SuiteExecution
|
||||
import gs.test.v0.api.TestExecution
|
||||
|
||||
/** Interface for formatters - implementations that transform test results into
|
||||
* string representations.
|
||||
*
|
||||
* Example implementations include producing plain text or JSON
|
||||
* representations.
|
||||
*/
|
||||
trait ResultFormatter:
|
||||
/** @return
|
||||
* The prefix for the format (if any).
|
||||
*/
|
||||
def prefix: String
|
||||
|
||||
/** @return
|
||||
* The suffix for the format (if any).
|
||||
*/
|
||||
def suffix: String
|
||||
|
||||
/** Format a single [[GroupResult]] as a string.
|
||||
*
|
||||
* @param groupResult
|
||||
* The result to format.
|
||||
* @return
|
||||
* The string rendition.
|
||||
*/
|
||||
def formatGroupResult(groupResult: GroupResult): String
|
||||
|
||||
/** Format a single [[TestExecution]] as a string.
|
||||
*
|
||||
* @param testExecution
|
||||
* The result to format.
|
||||
* @return
|
||||
* The string rendition.
|
||||
*/
|
||||
def formatTestExecution(testExecution: TestExecution): String
|
||||
|
||||
/** Format a single [[SuiteExecution]] as a string.
|
||||
*
|
||||
* @param suiteExecution
|
||||
* The result to format.
|
||||
* @return
|
||||
* The string rendition.
|
||||
*/
|
||||
def formatSuiteExecution(suiteExecution: SuiteExecution): String
|
|
@ -1,17 +0,0 @@
|
|||
package gs.test.v0.runtime
|
||||
|
||||
import gs.uuid.v0.UUID
|
||||
import java.time.Instant
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
case class SuiteExecution(
|
||||
id: UUID,
|
||||
name: String,
|
||||
documentation: Option[String],
|
||||
duration: FiniteDuration,
|
||||
countSeen: Long,
|
||||
countSucceeded: Long,
|
||||
countFailed: Long,
|
||||
countIgnored: Long,
|
||||
executedAt: Instant
|
||||
)
|
|
@ -1,5 +1,8 @@
|
|||
package gs.test.v0.runtime.engine
|
||||
|
||||
import gs.uuid.v0.UUID
|
||||
import java.time.Clock
|
||||
|
||||
/** Used to control the behavior of some [[TestEngine]]
|
||||
*
|
||||
* @param groupConcurrency
|
||||
|
@ -8,8 +11,17 @@ package gs.test.v0.runtime.engine
|
|||
* @param testConcurrency
|
||||
* [[ConcurrencySetting]] for tests; the number of tests allowed to execute
|
||||
* at the same time within some group.
|
||||
* @param clock
|
||||
* The `Clock` instance used to inform all date/time operations.
|
||||
* @param suiteIdGenerator
|
||||
* UUID provider that is used at the suite level.
|
||||
* @param testIdGenerator
|
||||
* UUID provider that is used at the test level.
|
||||
*/
|
||||
case class EngineConfiguration(
|
||||
groupConcurrency: ConcurrencySetting,
|
||||
testConcurrency: ConcurrencySetting
|
||||
testConcurrency: ConcurrencySetting,
|
||||
clock: Clock,
|
||||
suiteIdGenerator: UUID.Generator,
|
||||
testIdGenerator: UUID.Generator
|
||||
)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package gs.test.v0.runtime.engine
|
||||
|
||||
object EngineConstants:
|
||||
|
||||
object Tracing:
|
||||
|
||||
val RootSpan: String = "suite"
|
||||
val FullGroup: String = "full-group"
|
||||
val BeforeGroup: String = "before-group"
|
||||
val AfterGroup: String = "after-group"
|
||||
val FullTest: String = "full-test"
|
||||
val BeforeTest: String = "before-test"
|
||||
val AfterTest: String = "after-test"
|
||||
val InGroup: String = "in-group"
|
||||
val TestSpan: String = "test"
|
||||
|
||||
end Tracing
|
||||
|
||||
object MetaData:
|
||||
|
||||
val TestGroupName: String = "test_group_name"
|
||||
val TestExecutionId: String = "test_execution_id"
|
||||
val TestName: String = "test_name"
|
||||
|
||||
end MetaData
|
||||
|
||||
end EngineConstants
|
|
@ -1,7 +0,0 @@
|
|||
package gs.test.v0.runtime.engine
|
||||
|
||||
import gs.test.v0.runtime.SuiteExecution
|
||||
|
||||
final class EngineResult(
|
||||
val suiteExecution: SuiteExecution
|
||||
)
|
|
@ -3,32 +3,98 @@ package gs.test.v0.runtime.engine
|
|||
import cats.effect.Async
|
||||
import cats.effect.Ref
|
||||
import cats.syntax.all.*
|
||||
import gs.test.v0.api.TestExecution
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
final class EngineStats[F[_]: Async](
|
||||
val overallDuration: Ref[F, FiniteDuration],
|
||||
val countSeen: Ref[F, Long],
|
||||
val countSucceeded: Ref[F, Long],
|
||||
val countFailed: Ref[F, Long],
|
||||
val countIgnored: Ref[F, Long]
|
||||
)
|
||||
/** Statistics for executed tests. Used by the [[TestEngine]].
|
||||
*
|
||||
* @param overallDuration
|
||||
* Duration of all recorded tests.
|
||||
* @param countSeen
|
||||
* Number of tests encountered.
|
||||
* @param countPassed
|
||||
* Number of tests that passed.
|
||||
* @param countFailed
|
||||
* Number of tests that failed.
|
||||
*/
|
||||
final class EngineStats[F[_]: Async] private (
|
||||
overallDuration: Ref[F, FiniteDuration],
|
||||
countSeen: Ref[F, Long],
|
||||
countPassed: Ref[F, Long],
|
||||
countFailed: Ref[F, Long]
|
||||
):
|
||||
/** @return
|
||||
* The accumulated duration of test executions.
|
||||
*/
|
||||
def duration: F[FiniteDuration] = overallDuration.get
|
||||
|
||||
/** @return
|
||||
* Number of tests encountered.
|
||||
*/
|
||||
def seen: F[Long] = countSeen.get
|
||||
|
||||
/** @return
|
||||
* Number of tests that passed.
|
||||
*/
|
||||
def passed: F[Long] = countPassed.get
|
||||
|
||||
/** @return
|
||||
* Number of tests that failed.
|
||||
*/
|
||||
def failed: F[Long] = countFailed.get
|
||||
|
||||
/** Update the stats based on the results of an entire group.
|
||||
*
|
||||
* @param duration
|
||||
* The length of time it took to execute the group.
|
||||
* @param testExecutions
|
||||
* The list of all [[TestExecution]] produced by the group.
|
||||
* @return
|
||||
* Side-effect which updates statistic values.
|
||||
*/
|
||||
def updateForGroup(
|
||||
duration: FiniteDuration,
|
||||
testExecutions: List[TestExecution]
|
||||
): F[Unit] =
|
||||
for
|
||||
_ <- overallDuration.update(base => base + duration)
|
||||
_ <- testExecutions.map(updateForTest).sequence
|
||||
yield ()
|
||||
|
||||
/** Update the stats based on the results of a single test.
|
||||
*
|
||||
* @param testExecution
|
||||
* The [[TestExecution]] representing the test.
|
||||
* @return
|
||||
* Side-effect which updates statistic values.
|
||||
*/
|
||||
def updateForTest(testExecution: TestExecution): F[Unit] =
|
||||
for
|
||||
_ <- countSeen.update(_ + 1L)
|
||||
_ <- testExecution.result match
|
||||
case Left(_) => countFailed.update(_ + 1L)
|
||||
case Right(_) => countPassed.update(_ + 1L)
|
||||
yield ()
|
||||
|
||||
object EngineStats:
|
||||
|
||||
/** Initialize a new [[EngineStats]] instance with all values set to 0.
|
||||
*
|
||||
* @return
|
||||
* The new [[EngineStats]] instance.
|
||||
*/
|
||||
def initialize[F[_]: Async]: F[EngineStats[F]] =
|
||||
for
|
||||
duration <- Ref.of(FiniteDuration(0L, TimeUnit.NANOSECONDS))
|
||||
seen <- Ref.of(0L)
|
||||
succeeded <- Ref.of(0L)
|
||||
passed <- Ref.of(0L)
|
||||
failed <- Ref.of(0L)
|
||||
ignored <- Ref.of(0L)
|
||||
yield new EngineStats[F](
|
||||
overallDuration = duration,
|
||||
countSeen = seen,
|
||||
countSucceeded = succeeded,
|
||||
countFailed = failed,
|
||||
countIgnored = ignored
|
||||
countPassed = passed,
|
||||
countFailed = failed
|
||||
)
|
||||
|
||||
end EngineStats
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package gs.test.v0.runtime.engine
|
||||
|
||||
import gs.test.v0.api.TestGroupDefinition
|
||||
import gs.test.v0.runtime.TestExecution
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
final class GroupResult(
|
||||
val name: TestGroupDefinition.Name,
|
||||
val documentation: Option[String],
|
||||
val duration: FiniteDuration,
|
||||
val testExecutions: List[TestExecution]
|
||||
)
|
|
@ -1,113 +1,179 @@
|
|||
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
|
||||
import gs.test.v0.api.TestDefinition
|
||||
import gs.test.v0.api.TestExecution
|
||||
import gs.test.v0.api.TestFailure
|
||||
import gs.test.v0.api.TestGroupDefinition
|
||||
import gs.test.v0.api.TestSuite
|
||||
import gs.test.v0.runtime.SuiteExecution
|
||||
import gs.test.v0.runtime.TestExecution
|
||||
import gs.test.v0.reporting.Reporter
|
||||
import gs.timing.v0.Timing
|
||||
import gs.uuid.v0.UUID
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import natchez.EntryPoint
|
||||
import natchez.Span
|
||||
|
||||
/** This class is responsible for executing suites of tests.
|
||||
*
|
||||
* ## How Execution Works
|
||||
*
|
||||
* Test execution starts at the group level, via a stream of
|
||||
* [[TestGroupDefinition]]. Each group of tests is executed concurrently based
|
||||
* on the [[EngineConfiguration]].
|
||||
*
|
||||
* ### Executing a Single Group
|
||||
*
|
||||
* Each [[TestGroupDefinition]] is executed by executing, in order:
|
||||
*
|
||||
* - The `beforeGroup` effect.
|
||||
* - Each test (configurable concurrency).
|
||||
* - The `afterGroup` effect.
|
||||
*
|
||||
* The before/after effects are described at the group level.
|
||||
*
|
||||
* ### Executing a Single Test
|
||||
*
|
||||
* Each [[TestDefinition]] is executed by executing, in order:
|
||||
*
|
||||
* - The `beforeEachTest` effect.
|
||||
* - The test code.
|
||||
* - The `afterEachTest` effect.
|
||||
*
|
||||
* The before/after effects are described at the group level.
|
||||
*
|
||||
* ## OpenTelemetry Support
|
||||
*
|
||||
* Each [[SuiteExecution]] produces a single trace that encompasses all tests.
|
||||
* Spans are used to designate different related portions of work.
|
||||
*/
|
||||
final class TestEngine[F[_]: Async](
|
||||
val configuration: EngineConfiguration,
|
||||
timing: Timing[F],
|
||||
suiteExecutionIdGenerator: UUID.Generator,
|
||||
testExecutionIdGenerator: UUID.Generator,
|
||||
clock: Clock,
|
||||
val entryPoint: EntryPoint[F]
|
||||
reporter: Reporter[F],
|
||||
entryPoint: EntryPoint[F],
|
||||
timing: Timing[F]
|
||||
):
|
||||
|
||||
private def clock = configuration.clock
|
||||
private def testIdGen = configuration.testIdGenerator
|
||||
private def suiteIdGen = configuration.suiteIdGenerator
|
||||
|
||||
def runSuite(
|
||||
suite: TestSuite,
|
||||
tests: fs2.Stream[F, TestGroupDefinition[F]]
|
||||
): F[SuiteExecution] =
|
||||
entryPoint.root(EngineConstants.Tracing.RootSpan).use { rootSpan =>
|
||||
for
|
||||
executedAt <- Async[F].delay(Instant.now(clock))
|
||||
stats <- EngineStats.initialize[F]
|
||||
|
||||
// Start reporting
|
||||
_ <- reporter.startReport()
|
||||
|
||||
// Run all tests, group by group.
|
||||
_ <- tests
|
||||
.mapAsync(configuration.groupConcurrency.toInt())(runGroup)
|
||||
.evalTap(updateGroupStats)
|
||||
.evalTap(reportGroup)
|
||||
.flatMap(groupResult => fs2.Stream.emits(groupResult.testExecutions))
|
||||
.evalTap(updateTestStats)
|
||||
.evalMap(reportTestExecution)
|
||||
.mapAsync(configuration.groupConcurrency.toInt())(
|
||||
runGroup(rootSpan, _)
|
||||
)
|
||||
.evalTap(
|
||||
(
|
||||
groupResult,
|
||||
testExecutions
|
||||
) =>
|
||||
for
|
||||
// Update the overall statistics based on this group.
|
||||
_ <- stats.updateForGroup(
|
||||
duration = groupResult.duration,
|
||||
testExecutions = testExecutions
|
||||
)
|
||||
|
||||
// Report group level results for this group.
|
||||
_ <- reporter.reportGroup(
|
||||
groupResult = groupResult,
|
||||
testExecutions = testExecutions
|
||||
)
|
||||
yield ()
|
||||
)
|
||||
.compile
|
||||
.drain
|
||||
overallDuration <- stats.overallDuration.get
|
||||
countSeen <- stats.countSeen.get
|
||||
countSucceeded <- stats.countSucceeded.get
|
||||
countFailed <- stats.countFailed.get
|
||||
countIgnored <- stats.countIgnored.get
|
||||
yield SuiteExecution(
|
||||
id = suiteExecutionIdGenerator.next(),
|
||||
name = suite.name,
|
||||
documentation = suite.documentation,
|
||||
duration = overallDuration,
|
||||
countSeen = countSeen,
|
||||
countSucceeded = countSucceeded,
|
||||
countFailed = countFailed,
|
||||
countIgnored = countIgnored,
|
||||
executedAt = executedAt
|
||||
)
|
||||
|
||||
private def updateGroupStats(groupResult: GroupResult): F[Unit] = ???
|
||||
// Calculate the final summary of execution at the suite level.
|
||||
suiteExecution <- makeSuiteExecution(rootSpan, suite, stats, executedAt)
|
||||
|
||||
private def updateTestStats(testExecution: TestExecution): F[Unit] = ???
|
||||
// Report suite level results.
|
||||
_ <- reporter.reportSuite(suiteExecution)
|
||||
|
||||
private def reportGroup(groupResult: GroupResult): 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(
|
||||
group: TestGroupDefinition[F]
|
||||
): F[GroupResult] =
|
||||
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
|
||||
// Finish reporting.
|
||||
_ <- reporter.endReport()
|
||||
yield suiteExecution
|
||||
}
|
||||
|
||||
private def executeGroupTests(
|
||||
group: TestGroupDefinition[F],
|
||||
rootSpan: Span[F]
|
||||
): F[GroupResult] =
|
||||
rootSpan.span("group").use { groupSpan =>
|
||||
def runGroup(
|
||||
suiteSpan: Span[F],
|
||||
group: TestGroupDefinition[F]
|
||||
): F[(GroupResult, List[TestExecution])] =
|
||||
suiteSpan.span(EngineConstants.Tracing.FullGroup).use { fullGroupSpan =>
|
||||
for
|
||||
traceId <- rootSpan.traceId.map(parseTraceId)
|
||||
groupStats <- EngineStats.initialize[F]
|
||||
// Augment the span with all group-level metadata.
|
||||
_ <- fullGroupSpan
|
||||
.put(EngineConstants.MetaData.TestGroupName -> group.name.show)
|
||||
|
||||
// Start the timer for the entire group.
|
||||
timer <- timing.start()
|
||||
executions <- streamGroupTests(group, groupSpan).compile.toList
|
||||
|
||||
// Run the before-group logic (in its own span).
|
||||
_ <- runSpan(
|
||||
EngineConstants.Tracing.BeforeGroup,
|
||||
fullGroupSpan,
|
||||
group.beforeGroup.getOrElse(Async[F].unit)
|
||||
)
|
||||
|
||||
// Execute all tests within this group.
|
||||
testExecutions <- executeGroupTests(group, fullGroupSpan)
|
||||
|
||||
// Run the after-group logic (in its own span).
|
||||
_ <- runSpan(
|
||||
EngineConstants.Tracing.AfterGroup,
|
||||
fullGroupSpan,
|
||||
group.afterGroup.getOrElse(Async[F].unit)
|
||||
)
|
||||
|
||||
// Calculate the overall elapsed time for this group.
|
||||
elapsed <- timer.checkpoint()
|
||||
|
||||
// Calculate group-level statistics.
|
||||
_ <- groupStats.updateForGroup(
|
||||
duration = elapsed.duration,
|
||||
testExecutions = testExecutions
|
||||
)
|
||||
|
||||
// Extract the group statistic values for inclusion in the result..
|
||||
seen <- groupStats.seen
|
||||
passed <- groupStats.passed
|
||||
failed <- groupStats.failed
|
||||
yield new GroupResult(
|
||||
name = group.name,
|
||||
documentation = group.documentation,
|
||||
duration = elapsed.duration,
|
||||
testExecutions = executions
|
||||
)
|
||||
seen = seen,
|
||||
passed = passed,
|
||||
failed = failed
|
||||
) -> testExecutions
|
||||
}
|
||||
|
||||
private def executeGroupTests(
|
||||
group: TestGroupDefinition[F],
|
||||
fullGroupSpan: Span[F]
|
||||
): F[List[TestExecution]] =
|
||||
fullGroupSpan.span(EngineConstants.Tracing.InGroup).use { groupSpan =>
|
||||
for
|
||||
// If, for some reason, the generated span has no Trace ID, this will
|
||||
// throw an exception.
|
||||
executions <- streamGroupTests(group, groupSpan).compile.toList
|
||||
yield executions
|
||||
}
|
||||
|
||||
private def streamGroupTests(
|
||||
|
@ -117,14 +183,37 @@ final class TestEngine[F[_]: Async](
|
|||
fs2.Stream
|
||||
.emits(group.tests)
|
||||
.mapAsync(configuration.testConcurrency.toInt()) { test =>
|
||||
groupSpan.span(EngineConstants.Tracing.FullTest).use { fullSpan =>
|
||||
for
|
||||
// Generate a unique TestExecutionId for this execution.
|
||||
testExecutionId <- Async[F].delay(
|
||||
TestExecution.Id(testExecutionIdGenerator.next())
|
||||
TestExecution.Id(testIdGen.next())
|
||||
)
|
||||
|
||||
testSpanId <- fullSpan.spanId.map(parseSpanId)
|
||||
|
||||
// Start the timer for the test, including the before/after
|
||||
// components.
|
||||
timer <- timing.start()
|
||||
_ <- group.beforeEachTest.getOrElse(Async[F].unit)
|
||||
|
||||
// Run the before-test logic (in its own span).
|
||||
_ <- runSpan(
|
||||
EngineConstants.Tracing.BeforeTest,
|
||||
groupSpan,
|
||||
group.beforeEachTest.getOrElse(Async[F].unit)
|
||||
)
|
||||
|
||||
// Run the test (in its own span).
|
||||
result <- runSingleTest(testExecutionId, test, groupSpan)
|
||||
_ <- group.afterEachTest.getOrElse(Async[F].unit)
|
||||
|
||||
// Run the after-test logic (in its own span).
|
||||
_ <- runSpan(
|
||||
EngineConstants.Tracing.AfterTest,
|
||||
groupSpan,
|
||||
group.afterEachTest.getOrElse(Async[F].unit)
|
||||
)
|
||||
|
||||
// Calculate the overall elapsed time for this single test.
|
||||
elapsed <- timer.checkpoint()
|
||||
yield TestExecution(
|
||||
id = testExecutionId,
|
||||
|
@ -133,24 +222,109 @@ final class TestEngine[F[_]: Async](
|
|||
tags = test.tags,
|
||||
markers = test.markers,
|
||||
result = result,
|
||||
traceId = ???,
|
||||
spanId = testSpanId,
|
||||
sourcePosition = test.sourcePosition,
|
||||
duration = elapsed.duration
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def runSingleTest(
|
||||
testExecutionId: TestExecution.Id,
|
||||
test: TestDefinition[F],
|
||||
groupSpan: Span[F]
|
||||
): F[Either[TestFailure, Any]] =
|
||||
groupSpan.span("test").use { span =>
|
||||
groupSpan.span(EngineConstants.Tracing.TestSpan).use { span =>
|
||||
for
|
||||
// TODO: Constants
|
||||
_ <- span.put("test_execution_id" -> testExecutionId.show)
|
||||
_ <- span.put("test_name" -> test.name.show)
|
||||
_ <- span
|
||||
.put(EngineConstants.MetaData.TestExecutionId -> testExecutionId.show)
|
||||
_ <- span.put(EngineConstants.MetaData.TestName -> test.name.show)
|
||||
result <- test.unitOfWork.doWork(span)
|
||||
yield result
|
||||
}
|
||||
|
||||
private def parseTraceId(candidate: Option[String]): UUID = ???
|
||||
private def parseTraceId(candidate: Option[String]): String =
|
||||
candidate match
|
||||
case Some(traceId) => traceId
|
||||
case None =>
|
||||
throw new IllegalArgumentException("Created a span without a Trace ID!")
|
||||
|
||||
private def parseSpanId(candidate: Option[String]): String =
|
||||
candidate match
|
||||
case Some(spanId) => spanId
|
||||
case None =>
|
||||
throw new IllegalArgumentException("Created a span without a Span ID!")
|
||||
|
||||
private def makeSuiteExecution(
|
||||
rootSpan: Span[F],
|
||||
suite: TestSuite,
|
||||
stats: EngineStats[F],
|
||||
executedAt: Instant
|
||||
): F[SuiteExecution] =
|
||||
for
|
||||
traceId <- rootSpan.traceId.map(parseTraceId)
|
||||
overallDuration <- stats.duration
|
||||
seen <- stats.seen
|
||||
passed <- stats.passed
|
||||
failed <- stats.failed
|
||||
yield SuiteExecution(
|
||||
id = suiteIdGen.next(),
|
||||
testSuite = suite,
|
||||
traceId = traceId,
|
||||
duration = overallDuration,
|
||||
seen = seen,
|
||||
passed = passed,
|
||||
failed = failed,
|
||||
executedAt = executedAt
|
||||
)
|
||||
|
||||
/** Run some effect as a child span for some root span.
|
||||
*
|
||||
* @param name
|
||||
* The name of the span.
|
||||
* @param root
|
||||
* The root span.
|
||||
* @param f
|
||||
* The effect to execute in a child span.
|
||||
* @return
|
||||
* The contextualized effect.
|
||||
*/
|
||||
private def runSpan[A](
|
||||
name: String,
|
||||
root: Span[F],
|
||||
f: F[A]
|
||||
): F[A] =
|
||||
root.span(name).use(_ => f)
|
||||
|
||||
object TestEngine:
|
||||
|
||||
/** Provision a new [[TestEngine]].
|
||||
*
|
||||
* @param configuration
|
||||
* The [[EngineConfiguration]] used for this instance.
|
||||
* @param reporter
|
||||
* Resource which manages the [[Reporter]].
|
||||
* @param entryPoint
|
||||
* Resource which manages the telemetry entry point.
|
||||
* @param timing
|
||||
* Timing controller.
|
||||
* @return
|
||||
* Resource which manages the [[TestEngine]].
|
||||
*/
|
||||
def provision[F[_]: Async](
|
||||
configuration: EngineConfiguration,
|
||||
reporter: Resource[F, Reporter[F]],
|
||||
entryPoint: Resource[F, EntryPoint[F]],
|
||||
timing: Timing[F]
|
||||
): Resource[F, TestEngine[F]] =
|
||||
for
|
||||
r <- reporter
|
||||
ep <- entryPoint
|
||||
yield new TestEngine(
|
||||
configuration = configuration,
|
||||
reporter = r,
|
||||
entryPoint = ep,
|
||||
timing = timing
|
||||
)
|
||||
|
||||
end TestEngine
|
||||
|
|
|
@ -1 +1 @@
|
|||
sbt.version=1.10.2
|
||||
sbt.version=1.11.2
|
||||
|
|
|
@ -28,6 +28,6 @@ externalResolvers := Seq(
|
|||
"Garrity Software Releases" at "https://maven.garrity.co/gs"
|
||||
)
|
||||
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0")
|
||||
addSbtPlugin("gs" % "sbt-garrity-software" % "0.4.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
|
||||
addSbtPlugin("gs" % "sbt-garrity-software" % "0.6.0")
|
||||
addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0")
|
||||
|
|
Loading…
Add table
Reference in a new issue