Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Local[F, Span[F]] instance where F is Kleisli[F, Span[F], *] #713

Merged
merged 5 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)

lazy val coreTests = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("modules/core-tests"))
.dependsOn(core, testkit)
.dependsOn(core, mtl, testkit)
.enablePlugins(AutomateHeaderPlugin, NoPublishPlugin)
.settings(commonSettings)

Expand Down Expand Up @@ -283,7 +283,10 @@ lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform)
name := "natchez-mtl",
description := "cats-mtl bindings for Natchez.",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-mtl" % "1.3.0"
"org.typelevel" %%% "cats-mtl" % "1.3.0",
"org.typelevel" %%% "cats-mtl-laws" % "1.3.0" % Test,
"org.typelevel" %%% "discipline-munit" % "2.0.0-M3" % Test,
"org.typelevel" %%% "cats-effect-testkit" % "3.4.5" % Test
)
)
.nativeSettings(
Expand Down
147 changes: 111 additions & 36 deletions modules/core-tests/shared/src/test/scala/InMemorySuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,131 @@

package natchez

import cats.arrow.FunctionK
import cats.{Applicative, ~>}
import cats.syntax.all._
import cats.data.Kleisli
import cats.effect.{IO, MonadCancelThrow}
import cats.effect.{Concurrent, IO, IOLocal, MonadCancelThrow}
import cats.mtl.Local
import munit.CatsEffectSuite
import natchez.InMemory.Lineage.defaultRootName
import natchez.mtl._

trait InMemorySuite extends CatsEffectSuite {
type Lineage = InMemory.Lineage
val Lineage = InMemory.Lineage
type NatchezCommand = InMemory.NatchezCommand
val NatchezCommand = InMemory.NatchezCommand
}

trait TraceTest {
def program[F[_]: MonadCancelThrow: Trace]: F[Unit]
def expectedHistory: List[(Lineage, NatchezCommand)]
}
object InMemorySuite {
trait TraceSuite extends InMemorySuite {
trait TraceTest {
def program[F[_]: MonadCancelThrow: Trace]: F[Unit]

def expectedHistory: List[(Lineage, NatchezCommand)]
}

def traceTest(name: String, tt: TraceTest): Unit = {
test(s"$name - Kleisli")(
testTraceKleisli(tt.program[Kleisli[IO, Span[IO], *]](implicitly, _), tt.expectedHistory)
)
test(s"$name - IOLocal")(testTraceIoLocal(tt.program[IO](implicitly, _), tt.expectedHistory))
}

def traceTest(name: String, tt: TraceTest): Unit = {
test(s"$name - Kleisli")(
testTraceKleisli(tt.program[Kleisli[IO, Span[IO], *]](implicitly, _), tt.expectedHistory)
def testTraceKleisli(
traceProgram: Trace[Kleisli[IO, Span[IO], *]] => Kleisli[IO, Span[IO], Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] = testTrace[Kleisli[IO, Span[IO], *]](
traceProgram,
root => IO.pure(Trace[Kleisli[IO, Span[IO], *]] -> (k => k.run(root))),
expectedHistory
)
test(s"$name - IOLocal")(testTraceIoLocal(tt.program[IO](implicitly, _), tt.expectedHistory))
}

def testTraceKleisli(
traceProgram: Trace[Kleisli[IO, Span[IO], *]] => Kleisli[IO, Span[IO], Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] = testTrace[Kleisli[IO, Span[IO], *]](
traceProgram,
root => IO.pure(Trace[Kleisli[IO, Span[IO], *]] -> (k => k.run(root))),
expectedHistory
)

def testTraceIoLocal(
traceProgram: Trace[IO] => IO[Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)

def testTrace[F[_]](
traceProgram: Trace[F] => F[Unit],
makeTraceAndResolver: Span[IO] => IO[(Trace[F], F[Unit] => IO[Unit])],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] =
InMemory.EntryPoint.create[IO].flatMap { ep =>
val traced = ep.root(defaultRootName).use { r =>
makeTraceAndResolver(r).flatMap { case (traceInstance, resolve) =>
resolve(traceProgram(traceInstance))
def testTraceIoLocal(
traceProgram: Trace[IO] => IO[Unit],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] = testTrace[IO](traceProgram, Trace.ioTrace(_).map(_ -> identity), expectedHistory)

def testTrace[F[_]](
traceProgram: Trace[F] => F[Unit],
makeTraceAndResolver: Span[IO] => IO[(Trace[F], F[Unit] => IO[Unit])],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] =
InMemory.EntryPoint.create[IO].flatMap { ep =>
val traced = ep.root(defaultRootName).use { r =>
makeTraceAndResolver(r).flatMap { case (traceInstance, resolve) =>
resolve(traceProgram(traceInstance))
}
}
traced *> ep.ref.get.map { history =>
assertEquals(history.toList, expectedHistory)
}
}
traced *> ep.ref.get.map { history =>
assertEquals(history.toList, expectedHistory)
}
}

trait LocalSuite extends InMemorySuite {
type LocalProgram[F[_]] = (EntryPoint[F], Local[F, Span[F]]) => F[Unit]

trait LocalTest {
def program[F[_]: MonadCancelThrow](entryPoint: EntryPoint[F])(implicit
L: Local[F, Span[F]]
): F[Unit]
def expectedHistory: List[(Lineage, NatchezCommand)]
}

def localTest(name: String, tt: LocalTest): Unit = {
test(s"$name - Kleisli")(
testLocalKleisli(tt.program[Kleisli[IO, Span[IO], *]](_)(implicitly, _), tt.expectedHistory)
)
test(s"$name - IOLocal")(
testLocalIoLocal(tt.program[IO](_)(implicitly, _), tt.expectedHistory)
)
}

private def testLocalKleisli(
localProgram: LocalProgram[Kleisli[IO, Span[IO], *]],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] = testProgramGivenEntryPoint[Kleisli[IO, Span[IO], *]](
localProgram(_, implicitly),
Kleisli.applyK(Span.noop[IO]),
expectedHistory
)

private def testLocalIoLocal(
localProgram: LocalProgram[IO],
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] =
testProgramGivenEntryPoint[IO](
ep =>
IOLocal(Span.noop[IO])
.map { ioLocal =>
new Local[IO, Span[IO]] {
override def local[A](fa: IO[A])(f: Span[IO] => Span[IO]): IO[A] =
ioLocal.get.flatMap { initial =>
ioLocal.set(f(initial)) >> fa.guarantee(ioLocal.set(initial))
}

override def applicative: Applicative[IO] = implicitly

override def ask[E2 >: Span[IO]]: IO[E2] = ioLocal.get
}
}
.flatMap(localProgram(ep, _)),
FunctionK.id,
expectedHistory
)

private def testProgramGivenEntryPoint[F[_]: Concurrent](
localProgram: EntryPoint[F] => F[Unit],
fk: F ~> IO,
expectedHistory: List[(Lineage, NatchezCommand)]
): IO[Unit] =
fk(InMemory.EntryPoint.create[F].flatMap { ep =>
localProgram(ep) *> ep.ref.get.map { history =>
assertEquals(history.toList, expectedHistory)
}
})
}

}
33 changes: 33 additions & 0 deletions modules/core-tests/shared/src/test/scala/LocalTraceSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2019-2020 by Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez
package mtl

import cats.effect.{MonadCancelThrow, Trace => _}
import cats.mtl._

class LocalTraceSpec extends InMemorySuite.LocalSuite {

private def useTrace[F[_]: Trace]: F[Unit] = Trace[F].log("hello world")

localTest(
"Trace[F] should use root span generated by provided EntryPoint[F] via Local[F, Span[F]]",
new LocalTest {
override def program[F[_]: MonadCancelThrow](entryPoint: EntryPoint[F])(implicit
L: Local[F, Span[F]]
): F[Unit] =
entryPoint
.root("my root")
.use(Local[F, Span[F]].scope(useTrace[F]))

override def expectedHistory: List[(Lineage, NatchezCommand)] = List(
Lineage.Root -> NatchezCommand
.CreateRootSpan("my root", Kernel(Map()), Span.Options.Defaults),
Lineage.Root("my root") -> NatchezCommand.LogEvent("hello world"),
Lineage.Root -> NatchezCommand.ReleaseRootSpan("my root")
)
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package natchez

import cats.effect.MonadCancelThrow

class SpanCoalesceTest extends InMemorySuite {
class SpanCoalesceTest extends InMemorySuite.TraceSuite {

traceTest(
"suppress - nominal",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package natchez
import cats.effect.MonadCancelThrow
import cats.syntax.all._

class SpanPropagationTest extends InMemorySuite {
class SpanPropagationTest extends InMemorySuite.TraceSuite {

traceTest(
"propagation",
Expand Down
23 changes: 22 additions & 1 deletion modules/mtl/shared/src/main/scala/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@

package natchez

import cats.mtl.Local
import cats._
import cats.data.Kleisli
import cats.effect.{Trace => _, _}
import cats.mtl.{Local, MonadPartialOrder}

package object mtl {
implicit def natchezMtlTraceForLocal[F[_]](implicit
ev: Local[F, Span[F]],
eb: MonadCancel[F, Throwable]
): Trace[F] =
new LocalTrace(ev)

implicit def localSpanForKleisli[F[_]](implicit
F: MonadCancel[F, _]
): Local[Kleisli[F, Span[F], *], Span[Kleisli[F, Span[F], *]]] =
new Local[Kleisli[F, Span[F], *], Span[Kleisli[F, Span[F], *]]] {
override def local[A](
fa: Kleisli[F, Span[F], A]
)(f: Span[Kleisli[F, Span[F], *]] => Span[Kleisli[F, Span[F], *]]): Kleisli[F, Span[F], A] =
fa.local {
f.andThen(_.mapK(Kleisli.applyK(Span.noop[F])))
.compose(_.mapK(MonadPartialOrder[F, Kleisli[F, Span[F], *]]))
}

override def applicative: Applicative[Kleisli[F, Span[F], *]] =
MonadPartialOrder[F, Kleisli[F, Span[F], *]].monadG

override def ask[E2 >: Span[Kleisli[F, Span[F], *]]]: Kleisli[F, Span[F], E2] =
Kleisli.ask[F, Span[F]].map(_.mapK(MonadPartialOrder[F, Kleisli[F, Span[F], *]]))
}
}
109 changes: 109 additions & 0 deletions modules/mtl/shared/src/test/scala/LocalInstancesSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2019-2020 by Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package natchez
package mtl

import cats._
import cats.data._
import cats.effect.testkit.TestInstances
import cats.effect.{Trace => _, _}
import cats.laws.discipline.MiniInt.allValues
import cats.laws.discipline.{ExhaustiveCheck, MiniInt}
import cats.laws.discipline.arbitrary._
import cats.laws.discipline.eq._
import cats.mtl.laws.discipline.LocalTests
import cats.syntax.all._
import munit.DisciplineSuite
import natchez.mtl.SeededSpan.{seedKey, spanToSeed}
import org.scalacheck.rng.Seed
import org.scalacheck.{Arbitrary, Cogen, Gen}
import org.typelevel.ci.CIStringSyntax

import java.net.URI
import scala.util.Try

class LocalInstancesSpec extends DisciplineSuite with TestInstances {
implicit val ticker: Ticker = Ticker()

private implicit def exhaustiveCheckSpan[F[_]: Applicative]: ExhaustiveCheck[Span[F]] =
ExhaustiveCheck.instance(allValues.map(new SeededSpan[F](_)))

private def genFromExhaustiveCheck[A: ExhaustiveCheck]: Gen[A] =
Gen.oneOf(ExhaustiveCheck[A].allValues)

private implicit def arbFromExhaustiveCheck[A: ExhaustiveCheck]: Arbitrary[A] =
Arbitrary(genFromExhaustiveCheck)

implicit def comonadIO: Comonad[IO] = new Comonad[IO] {
override def extract[A](x: IO[A]): A =
unsafeRun(x).fold(
throw new RuntimeException("canceled"),
throw _,
_.get
)

override def coflatMap[A, B](fa: IO[A])(f: IO[A] => B): IO[B] =
f(fa).pure[IO]

override def map[A, B](fa: IO[A])(f: A => B): IO[B] = fa.map(f)
}

private implicit def cogenSpan[F[_]: Comonad]: Cogen[Span[F]] =
Cogen(spanToSeed(_))

private implicit def cogenSpanK[F[_]: Comonad](implicit
F: MonadCancel[F, _]
): Cogen[Span[Kleisli[F, Span[F], *]]] =
Cogen { (seed: Seed, span: Span[Kleisli[F, Span[F], *]]) =>
seed.reseed(
spanToSeed(
span.mapK(
Kleisli.applyK[F, Span[F]](
genFromExhaustiveCheck[Span[F]].apply(Gen.Parameters.default, seed).get
)
)
)
)
}

private implicit def eqKleisli[F[_], A: ExhaustiveCheck, B](implicit
ev: Eq[F[B]]
): Eq[Kleisli[F, A, B]] =
Eq.by((x: Kleisli[F, A, B]) => x.run)

private implicit def kernelEq: Eq[Kernel] = Eq.by(_.toHeaders)
private implicit def spanKEq: Eq[Span[Kleisli[IO, Span[IO], *]]] = Eq.by(_.kernel)

checkAll(
"Local[Kleisli[F, Span[F], *], Span[Kleisli[F, Span[F], *]]]",
LocalTests[Kleisli[IO, Span[IO], *], Span[Kleisli[IO, Span[IO], *]]](localSpanForKleisli)
.local[Int, Int]
)
}

object SeededSpan {
val seedKey = ci"seed"

def spanToSeed[F[_]: Comonad](span: Span[F]): Long =
span.kernel.extract.toHeaders
.get(seedKey)
.flatMap { s =>
Try(java.lang.Long.parseUnsignedLong(s, 16)).toOption
}
.get
}

private class SeededSpan[F[_]: Applicative](seed: MiniInt) extends Span[F] {
override def put(fields: (String, TraceValue)*): F[Unit] = ().pure[F]
override def log(fields: (String, TraceValue)*): F[Unit] = ().pure[F]
override def log(event: String): F[Unit] = ().pure[F]
override def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = ().pure[F]
override def kernel: F[Kernel] = Kernel(Map(seedKey -> seed.toInt.toHexString)).pure[F]
override def span(name: String, options: Span.Options): Resource[F, Span[F]] =
Resource.pure(this)
override def traceId: F[Option[String]] = Seed(seed.toInt.toLong).toBase64.some.pure[F]
override def spanId: F[Option[String]] = Seed(seed.toInt.toLong).toBase64.some.pure[F]
override def traceUri: F[Option[URI]] = none[URI].pure[F]
}