Small amount of updates for docs and trace support.

This commit is contained in:
Pat Garrity 2024-10-09 21:39:25 -05:00
parent f4f2462d15
commit 96f0fab473
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
6 changed files with 125 additions and 94 deletions

View file

@ -1,19 +1,25 @@
package gs.test.v0.api package gs.test.v0.api
import cats.~>
import cats.Applicative
import cats.data.EitherT import cats.data.EitherT
import cats.effect.kernel.Async 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 cats.syntax.all.*
import java.net.URI import gs.test.v0.api.trace.GsTestTrace
import natchez.* import natchez.*
/** Represents some function that may produce an error, intended to run within a
* traced context.
*/
sealed trait UnitOfWork[F[_]]: 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] span: Span[F]
): F[Either[TestFailure, Any]] ): F[Either[TestFailure, Any]]
@ -36,91 +42,33 @@ object UnitOfWork:
): UnitOfWork[F] = ): UnitOfWork[F] =
new UnitOfWork[F] { new UnitOfWork[F] {
override def work(span: Span[F]): F[Either[TestFailure, Any]] = override def doWork(span: Span[F]): F[Either[TestFailure, Any]] =
makeInternalTrace[F](span).flatMap { trace => GsTestTrace.initialize[F](span).flatMap { trace =>
given Trace[F] = trace given Trace[F] = trace
unitOfWork 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]( def applyT[F[_]: Async](
unitOfWork: TracedT[F] unitOfWork: TracedT[F]
): UnitOfWork[F] = ): UnitOfWork[F] =
new UnitOfWork[F] { new UnitOfWork[F] {
override def work(span: Span[F]): F[Either[TestFailure, Any]] = override def doWork(span: Span[F]): F[Either[TestFailure, Any]] =
makeInternalTrace[F](span).flatMap { trace => GsTestTrace.initialize[F](span).flatMap { trace =>
given Trace[F] = trace given Trace[F] = trace
unitOfWork.value 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 end UnitOfWork

View file

@ -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

View file

@ -58,7 +58,7 @@ class SourcePositionTests extends IOSuite:
case t1 :: Nil => case t1 :: Nil =>
ep.root("unit-test") ep.root("unit-test")
.use { span => .use { span =>
t1.unitOfWork.work(span).map { t1.unitOfWork.doWork(span).map {
case Left(TestFailure.AssertionFailed(_, _, _, pos)) => case Left(TestFailure.AssertionFailed(_, _, _, pos)) =>
assertEquals(pos.file.endsWith(SourceFileName), true) assertEquals(pos.file.endsWith(SourceFileName), true)
assertEquals(pos.line, line) assertEquals(pos.line, line)

View file

@ -1,24 +1,23 @@
package gs.test.v0.runtime package gs.test.v0.runtime
import cats.Show import cats.Show
import gs.test.v0.definition.Marker import gs.test.v0.api.Marker
import gs.test.v0.definition.PermanentId import gs.test.v0.api.PermanentId
import gs.test.v0.definition.Tag import gs.test.v0.api.SourcePosition
import gs.test.v0.definition.TestFailure import gs.test.v0.api.Tag
import gs.test.v0.definition.pos.SourcePosition import gs.test.v0.api.TestFailure
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
/** Represents a single _Test Execution_. Each _Test Execution_ represents /** Represents a single _Test Execution_. Each _Test Execution_ represents
* evaluating the unit of work for some * evaluating the unit of work for some [[gs.test.v0.api.TestDefinition]]
* [[gs.test.v0.definition.TestDefinition]] exactly once. It describes the * exactly once. It describes the result of the test.
* result of the test.
* *
* @param id * @param id
* The unique identifier for this execution. * The unique identifier for this execution.
* @param permanentId * @param permanentId
* The [[gs.test.v0.definition.PermanentId]] for the * The [[gs.test.v0.api.PermanentId]] for the
* [[gs.test.v0.definition.TestDefinition]] that was executed. * [[gs.test.v0.api.TestDefinition]] that was executed.
* @param documentation * @param documentation
* Documentation for the test that was executed. * Documentation for the test that was executed.
* @param tags * @param tags

View file

@ -1,6 +1,6 @@
package gs.test.v0.runtime.engine 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 gs.test.v0.runtime.TestExecution
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration

View file

@ -2,10 +2,10 @@ package gs.test.v0.runtime.engine
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.api.TestDefinition
import gs.test.v0.definition.TestFailure import gs.test.v0.api.TestFailure
import gs.test.v0.definition.TestGroupDefinition import gs.test.v0.api.TestGroupDefinition
import gs.test.v0.definition.TestSuite import gs.test.v0.api.TestSuite
import gs.test.v0.runtime.SuiteExecution import gs.test.v0.runtime.SuiteExecution
import gs.test.v0.runtime.TestExecution import gs.test.v0.runtime.TestExecution
import gs.timing.v0.Timing import gs.timing.v0.Timing
@ -149,7 +149,7 @@ final class TestEngine[F[_]: Async](
// TODO: Constants // TODO: Constants
_ <- span.put("test_execution_id" -> testExecutionId.show) _ <- span.put("test_execution_id" -> testExecutionId.show)
_ <- span.put("test_name" -> test.name.show) _ <- span.put("test_name" -> test.name.show)
result <- test.unitOfWork.work(span) result <- test.unitOfWork.doWork(span)
yield result yield result
} }