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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package gs.test.v0.definition.pos package gs.test.v0.definition.pos
import cats.data.Kleisli
import cats.effect.IO import cats.effect.IO
import cats.effect.kernel.Resource import cats.effect.kernel.Resource
import gs.test.v0.IOSuite import gs.test.v0.IOSuite
@ -10,8 +9,6 @@ import natchez.EntryPoint
import natchez.Kernel import natchez.Kernel
import natchez.Span import natchez.Span
import natchez.Span.Options import natchez.Span.Options
import natchez.Trace
import natchez.Trace.Implicits.noop
/** These tests are sensitive to changes, even in formatting! They are looking /** 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 * 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") { 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") { test("should provide the source position of an explicit failure") {
lookForSourcePosition(new G2, 82) lookForSourcePosition(new G2, 90)
} }
private def lookForSourcePosition( private def lookForSourcePosition(
groupDef: TestGroup[TracedIO], groupDef: TestGroup[IO],
line: Int line: Int
): Unit = ): Unit =
val group = groupDef.compile() val group = groupDef.compile()
@ -61,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.run(span).map { t1.unitOfWork.work(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)
@ -77,9 +74,7 @@ class SourcePositionTests extends IOSuite:
object SourcePositionTests: object SourcePositionTests:
type TracedIO[A] = Kleisli[IO, Span[IO], A] class G1 extends TestGroup[IO]:
class G1 extends TestGroup[TracedIO]:
override def name: String = "G1" override def name: String = "G1"
test(pid"t1", "pos").pure { test(pid"t1", "pos").pure {
@ -88,7 +83,7 @@ object SourcePositionTests:
end G1 end G1
class G2 extends TestGroup[TracedIO]: class G2 extends TestGroup[IO]:
override def name: String = "G2" override def name: String = "G2"
test(pid"t2", "pos").pure { test(pid"t2", "pos").pure {