diff --git a/modules/api/src/main/scala/gs/test/v0/api/UnitOfWork.scala b/modules/api/src/main/scala/gs/test/v0/api/UnitOfWork.scala index d64ce31..f347d05 100644 --- a/modules/api/src/main/scala/gs/test/v0/api/UnitOfWork.scala +++ b/modules/api/src/main/scala/gs/test/v0/api/UnitOfWork.scala @@ -1,19 +1,25 @@ package gs.test.v0.api -import cats.~> -import cats.Applicative import cats.data.EitherT import cats.effect.kernel.Async -import cats.effect.kernel.Ref -import cats.effect.kernel.Resource -import cats.effect.syntax.all.* import cats.syntax.all.* -import java.net.URI +import gs.test.v0.api.trace.GsTestTrace import natchez.* +/** Represents some function that may produce an error, intended to run within a + * traced context. + */ sealed trait UnitOfWork[F[_]]: - def work( + /** Execute the work defined by this function in the context of the given + * span. + * + * @param span + * The root span for tracing this work. + * @return + * An effect producing either a failure, or any arbitrary value. + */ + def doWork( span: Span[F] ): F[Either[TestFailure, Any]] @@ -36,91 +42,33 @@ object UnitOfWork: ): UnitOfWork[F] = new UnitOfWork[F] { - override def work(span: Span[F]): F[Either[TestFailure, Any]] = - makeInternalTrace[F](span).flatMap { trace => + override def doWork(span: Span[F]): F[Either[TestFailure, Any]] = + GsTestTrace.initialize[F](span).flatMap { trace => given Trace[F] = trace unitOfWork } } + /** Instantiate a new [[UnitOfWork]] with the given function that requires a + * `Span[F]` as input (EitherT support). + * + * @param uow + * The unit of work implementation. + * @return + * The new [[UnitOfWork]] instance. + */ def applyT[F[_]: Async]( unitOfWork: TracedT[F] ): UnitOfWork[F] = new UnitOfWork[F] { - override def work(span: Span[F]): F[Either[TestFailure, Any]] = - makeInternalTrace[F](span).flatMap { trace => + override def doWork(span: Span[F]): F[Either[TestFailure, Any]] = + GsTestTrace.initialize[F](span).flatMap { trace => given Trace[F] = trace unitOfWork.value } } - private def makeInternalTrace[F[_]: Async](sourceSpan: Span[F]) - : F[natchez.Trace[F]] = - Ref.of[F, Span[F]](sourceSpan).map(src => new InternalTrace[F](src)) - - // Copied from the Natchez ioTrace - private class InternalTrace[F[_]: Async]( - val src: Ref[F, Span[F]] - ) extends natchez.Trace[F]: - - def put(fields: (String, TraceValue)*): F[Unit] = - src.get.flatMap(_.put(fields*)) - - def log(fields: (String, TraceValue)*): F[Unit] = - src.get.flatMap(_.log(fields*)) - - def log(event: String): F[Unit] = - src.get.flatMap(_.log(event)) - - def attachError( - err: Throwable, - fields: (String, TraceValue)* - ): F[Unit] = - src.get.flatMap(_.attachError(err, fields*)) - - def kernel: F[Kernel] = src.get.flatMap(_.kernel) - - def spanR( - name: String, - options: Span.Options = Span.Options.Defaults - ): Resource[F, F ~> F] = - for { - parent <- Resource.eval(src.get) - child <- parent.span(name, options) - } yield new (F ~> F) { - def apply[A](fa: F[A]): F[A] = - src.get.flatMap { old => - src - .set(child) - .bracket(_ => fa.onError { case e => child.attachError(e) })(_ => - src.set(old) - ) - } - - } - - def span[A]( - name: String, - options: Span.Options = Span.Options.Defaults - )( - k: F[A] - ): F[A] = - spanR(name, options).use(_(k)) - - def traceId: F[Option[String]] = - src.get.flatMap(_.traceId) - - override def spanId( - implicit - F: Applicative[F] - ): F[Option[String]] = - src.get.flatMap(_.spanId) - - override def traceUri: F[Option[URI]] = src.get.flatMap(_.traceUri) - - end InternalTrace - end UnitOfWork diff --git a/modules/api/src/main/scala/gs/test/v0/api/trace/GsTestTrace.scala b/modules/api/src/main/scala/gs/test/v0/api/trace/GsTestTrace.scala new file mode 100644 index 0000000..cefc434 --- /dev/null +++ b/modules/api/src/main/scala/gs/test/v0/api/trace/GsTestTrace.scala @@ -0,0 +1,84 @@ +package gs.test.v0.api.trace + +import cats.~> +import cats.Applicative +import cats.effect.kernel.Async +import cats.effect.kernel.Ref +import cats.effect.kernel.Resource +import cats.effect.syntax.all.* +import cats.syntax.all.* +import java.net.URI +import natchez.* + +/** Essentially copied from the "IOTrace" implementation within the excellent + * Natchez, but updated to work with `Ref`. + */ +final class GsTestTrace[F[_]: Async] private ( + src: Ref[F, Span[F]] +) extends natchez.Trace[F]: + + def put(fields: (String, TraceValue)*): F[Unit] = + src.get.flatMap(_.put(fields*)) + + def log(fields: (String, TraceValue)*): F[Unit] = + src.get.flatMap(_.log(fields*)) + + def log(event: String): F[Unit] = + src.get.flatMap(_.log(event)) + + def attachError( + err: Throwable, + fields: (String, TraceValue)* + ): F[Unit] = + src.get.flatMap(_.attachError(err, fields*)) + + def kernel: F[Kernel] = src.get.flatMap(_.kernel) + + def spanR( + name: String, + options: Span.Options = Span.Options.Defaults + ): Resource[F, F ~> F] = + for { + parent <- Resource.eval(src.get) + child <- parent.span(name, options) + } yield new (F ~> F) { + def apply[A](fa: F[A]): F[A] = + src.get.flatMap { old => + src + .set(child) + .bracket(_ => fa.onError { case e => child.attachError(e) })(_ => + src.set(old) + ) + } + + } + + def span[A]( + name: String, + options: Span.Options = Span.Options.Defaults + )( + k: F[A] + ): F[A] = + spanR(name, options).use(_(k)) + + def traceId: F[Option[String]] = + src.get.flatMap(_.traceId) + + override def spanId( + implicit + F: Applicative[F] + ): F[Option[String]] = + src.get.flatMap(_.spanId) + + override def traceUri: F[Option[URI]] = src.get.flatMap(_.traceUri) + +object GsTestTrace: + + /** Initialize a new `natchez.Trace` instance based on a given Span. + */ + def initialize[F[_]: Async]( + sourceSpan: Span[F] + ): F[natchez.Trace[F]] = + Ref.of[F, Span[F]](sourceSpan).map(src => new GsTestTrace[F](src)) + +end GsTestTrace diff --git a/modules/api/src/test/scala/gs/test/v0/api/SourcePositionTests.scala b/modules/api/src/test/scala/gs/test/v0/api/SourcePositionTests.scala index 638c37a..b9bb78d 100644 --- a/modules/api/src/test/scala/gs/test/v0/api/SourcePositionTests.scala +++ b/modules/api/src/test/scala/gs/test/v0/api/SourcePositionTests.scala @@ -58,7 +58,7 @@ class SourcePositionTests extends IOSuite: case t1 :: Nil => ep.root("unit-test") .use { span => - t1.unitOfWork.work(span).map { + t1.unitOfWork.doWork(span).map { case Left(TestFailure.AssertionFailed(_, _, _, pos)) => assertEquals(pos.file.endsWith(SourceFileName), true) assertEquals(pos.line, line) diff --git a/modules/runtime/src/main/scala/gs/test/v0/runtime/TestExecution.scala b/modules/runtime/src/main/scala/gs/test/v0/runtime/TestExecution.scala index 61ceafd..2d779d0 100644 --- a/modules/runtime/src/main/scala/gs/test/v0/runtime/TestExecution.scala +++ b/modules/runtime/src/main/scala/gs/test/v0/runtime/TestExecution.scala @@ -1,24 +1,23 @@ package gs.test.v0.runtime import cats.Show -import gs.test.v0.definition.Marker -import gs.test.v0.definition.PermanentId -import gs.test.v0.definition.Tag -import gs.test.v0.definition.TestFailure -import gs.test.v0.definition.pos.SourcePosition +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 /** Represents a single _Test Execution_. Each _Test Execution_ represents - * evaluating the unit of work for some - * [[gs.test.v0.definition.TestDefinition]] exactly once. It describes the - * result of the test. + * evaluating the unit of work for some [[gs.test.v0.api.TestDefinition]] + * exactly once. It describes the result of the test. * * @param id * The unique identifier for this execution. * @param permanentId - * The [[gs.test.v0.definition.PermanentId]] for the - * [[gs.test.v0.definition.TestDefinition]] that was executed. + * The [[gs.test.v0.api.PermanentId]] for the + * [[gs.test.v0.api.TestDefinition]] that was executed. * @param documentation * Documentation for the test that was executed. * @param tags diff --git a/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/GroupResult.scala b/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/GroupResult.scala index 049b91f..f21efd8 100644 --- a/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/GroupResult.scala +++ b/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/GroupResult.scala @@ -1,6 +1,6 @@ package gs.test.v0.runtime.engine -import gs.test.v0.definition.TestGroupDefinition +import gs.test.v0.api.TestGroupDefinition import gs.test.v0.runtime.TestExecution import scala.concurrent.duration.FiniteDuration diff --git a/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/TestEngine.scala b/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/TestEngine.scala index c07a1fa..2157d1c 100644 --- a/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/TestEngine.scala +++ b/modules/runtime/src/main/scala/gs/test/v0/runtime/engine/TestEngine.scala @@ -2,10 +2,10 @@ package gs.test.v0.runtime.engine 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.api.TestDefinition +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.timing.v0.Timing @@ -149,7 +149,7 @@ final class TestEngine[F[_]: Async]( // TODO: Constants _ <- span.put("test_execution_id" -> testExecutionId.show) _ <- span.put("test_name" -> test.name.show) - result <- test.unitOfWork.work(span) + result <- test.unitOfWork.doWork(span) yield result }