Figured out trace syntax and passing context bounds to lambdas.

This commit is contained in:
Pat Garrity 2024-10-08 22:24:25 -05:00
parent 3f41e23478
commit ddb977b80c
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
4 changed files with 151 additions and 51 deletions

View file

@ -1,12 +1,10 @@
package gs.test.v0.definition
import cats.data.EitherT
import cats.data.Kleisli
import cats.effect.Async
import cats.syntax.all.*
import gs.test.v0.definition.pos.SourcePosition
import java.util.concurrent.ConcurrentHashMap
import natchez.Span
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.*
@ -266,15 +264,16 @@ object TestGroup:
* The function this test will execute.
*/
def pure(unitOfWork: => Either[TestFailure, Unit]): Unit =
effectful(Kleisli(_ => Async[F].pure(unitOfWork)))
effectful(Async[F].pure(unitOfWork))
/** Finalize and register this test with an effectful unit of work.
*
* @param unitOfWork
* The function this test will execute.
*/
def effectful(unitOfWork: => Kleisli[F, Span[F], Either[TestFailure, Any]])
: Unit =
def effectful(
unitOfWork: natchez.Trace[F] ?=> F[Either[TestFailure, Any]]
): Unit =
registry.register(
new TestDefinition[F](
name = name,
@ -288,16 +287,14 @@ object TestGroup:
)
)
/** Helper type for representing `span => EitherT[F, TestFailure, Any]`
*/
type ET[A] = EitherT[F, TestFailure, A]
/** Finalize and register this test with an effectful unit of work.
*
* @param unitOfWork
* The function this test will execute.
*/
def apply(unitOfWork: => Kleisli[ET, Span[F], Any]): Unit =
def apply(
unitOfWork: natchez.Trace[F] ?=> EitherT[F, TestFailure, Any]
): Unit =
registry.register(
new TestDefinition[F](
name = name,
@ -306,7 +303,7 @@ object TestGroup:
tags = tags.distinct.toList,
markers = markers.distinct.toList,
iterations = iterations,
unitOfWork = UnitOfWork[F].apply(unitOfWork.mapF(_.value)),
unitOfWork = UnitOfWork.applyT(unitOfWork),
sourcePosition = pos
)
)
@ -406,7 +403,20 @@ object TestGroup:
* The function this test will execute.
*/
def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit =
effectful(input => Kleisli(_ => Async[F].pure(unitOfWork(input))))
registry.register(
new TestDefinition[F](
name = name,
permanentId = permanentId,
documentation = documentation,
tags = tags.distinct.toList,
markers = markers.distinct.toList,
iterations = iterations,
unitOfWork = UnitOfWork.apply(
inputFunction.map(input => unitOfWork(input))
),
sourcePosition = pos
)
)
/** Finalize and register this test with an effectful unit of work.
*
@ -414,7 +424,7 @@ object TestGroup:
* The function this test will execute.
*/
def effectful(
unitOfWork: Input => Kleisli[F, Span[F], Either[TestFailure, Any]]
unitOfWork: natchez.Trace[F] ?=> Input => F[Either[TestFailure, Any]]
): Unit =
registry.register(
new TestDefinition[F](
@ -425,24 +435,20 @@ object TestGroup:
markers = markers.distinct.toList,
iterations = iterations,
unitOfWork = UnitOfWork.apply(
Kleisli(span =>
inputFunction.flatMap(input => unitOfWork(input).run(span))
)
inputFunction.flatMap(input => unitOfWork(input))
),
sourcePosition = pos
)
)
/** Helper type for representing `span => EitherT[F, TestFailure, Any]`
*/
type ET[A] = EitherT[F, TestFailure, A]
/** Finalize and register this test with an effectful unit of work.
*
* @param unitOfWork
* The function this test will execute.
*/
def apply(unitOfWork: => Input => Kleisli[ET, Span[F], Any]): Unit =
def apply(
unitOfWork: natchez.Trace[F] ?=> Input => EitherT[F, TestFailure, Any]
): Unit =
registry.register(
new TestDefinition[F](
name = name,
@ -451,12 +457,8 @@ object TestGroup:
tags = tags.distinct.toList,
markers = markers.distinct.toList,
iterations = iterations,
unitOfWork = UnitOfWork[F].apply(
Kleisli(span =>
inputFunction.flatMap { input =>
unitOfWork(input).mapF(_.value).run(span)
}
)
unitOfWork = UnitOfWork.applyT(
EitherT.liftF(inputFunction).flatMap(unitOfWork)
),
sourcePosition = pos
)

View file

@ -1,9 +1,17 @@
package gs.test.v0.definition
import cats.data.Kleisli
import natchez.Span
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 natchez.*
trait UnitOfWork[F[_]]:
sealed trait UnitOfWork[F[_]]:
def work(
span: Span[F]
@ -11,6 +19,10 @@ trait UnitOfWork[F[_]]:
object UnitOfWork:
type Traced[F[_]] = natchez.Trace[F] ?=> F[Either[TestFailure, Any]]
type TracedT[F[_]] = natchez.Trace[F] ?=> EitherT[F, TestFailure, Any]
/** Instantiate a new [[UnitOfWork]] with the given function that requires a
* `Span[F]` as input.
*
@ -19,13 +31,96 @@ object UnitOfWork:
* @return
* The new [[UnitOfWork]] instance.
*/
def apply[F[_]](
uow: Kleisli[F, Span[F], Either[TestFailure, Any]]
): UnitOfWork[F] = new UnitOfWork[F] {
def apply[F[_]: Async](
unitOfWork: Traced[F]
): UnitOfWork[F] =
new UnitOfWork[F] {
override def work(span: Span[F]): F[Either[TestFailure, Any]] =
uow.apply(span)
makeInternalTrace[F](span).flatMap { trace =>
given Trace[F] = trace
unitOfWork
}
}
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 =>
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

@ -6,7 +6,6 @@ import cats.effect.IO
import gs.test.v0.definition.{Tag => GsTag}
import munit.*
import natchez.Span
import natchez.Trace
class GroupImplementationTests extends FunSuite:
import GroupImplementationTests.*
@ -90,15 +89,16 @@ object GroupImplementationTests:
val T1: PermanentId = pid"t1"
val T2: PermanentId = pid"t2"
val T3: PermanentId = pid"t3"
val T4: PermanentId = pid"t4"
end Ids
class G1[F[_]: Async: Trace] extends TestGroup[F]:
class G1[F[_]: Async] extends TestGroup[F]:
override def name: String = "G1"
test(Ids.T1, "simple").pure(Right(()))
end G1
class G2[F[_]: Async: Trace] extends TestGroup[F]:
class G2[F[_]: Async] extends TestGroup[F]:
override def name: String =
"G2"
@ -120,7 +120,7 @@ object GroupImplementationTests:
test(Ids.T2, "inherit from group").pure(Right(()))
end G2
class G3[F[_]: Async: Trace] extends TestGroup[F]:
class G3[F[_]: Async] extends TestGroup[F]:
override def name: String = "G3"
test(Ids.T3, "configure test")
@ -132,4 +132,12 @@ object GroupImplementationTests:
end G3
class G4[F[_]: Async] extends TestGroup[F]:
override def name: String = "G4"
// TODO: Make test entrypoint and test Trace[F]
test(Ids.T4, "Effectful test").effectful {
???
}
end GroupImplementationTests

View file

@ -1,6 +1,5 @@
package gs.test.v0.definition.pos
import cats.data.Kleisli
import cats.effect.IO
import cats.effect.kernel.Resource
import gs.test.v0.IOSuite
@ -10,8 +9,6 @@ import natchez.EntryPoint
import natchez.Kernel
import natchez.Span
import natchez.Span.Options
import natchez.Trace
import natchez.Trace.Implicits.noop
/** These tests are sensitive to changes, even in formatting! They are looking
* for specific line numbers in this source code, so any sort of newline that
@ -45,15 +42,15 @@ class SourcePositionTests extends IOSuite:
}
test("should provide the source position of a failed check") {
lookForSourcePosition(new G1, 73)
lookForSourcePosition(new G1, 81)
}
test("should provide the source position of an explicit failure") {
lookForSourcePosition(new G2, 82)
lookForSourcePosition(new G2, 90)
}
private def lookForSourcePosition(
groupDef: TestGroup[TracedIO],
groupDef: TestGroup[IO],
line: Int
): Unit =
val group = groupDef.compile()
@ -61,7 +58,7 @@ class SourcePositionTests extends IOSuite:
case t1 :: Nil =>
ep.root("unit-test")
.use { span =>
t1.unitOfWork.run(span).map {
t1.unitOfWork.work(span).map {
case Left(TestFailure.AssertionFailed(_, _, _, pos)) =>
assertEquals(pos.file.endsWith(SourceFileName), true)
assertEquals(pos.line, line)
@ -77,9 +74,7 @@ class SourcePositionTests extends IOSuite:
object SourcePositionTests:
type TracedIO[A] = Kleisli[IO, Span[IO], A]
class G1 extends TestGroup[TracedIO]:
class G1 extends TestGroup[IO]:
override def name: String = "G1"
test(pid"t1", "pos").pure {
@ -88,7 +83,7 @@ object SourcePositionTests:
end G1
class G2 extends TestGroup[TracedIO]:
class G2 extends TestGroup[IO]:
override def name: String = "G2"
test(pid"t2", "pos").pure {