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

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 =>
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)

View file

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

View file

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

View file

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