diff --git a/build.sbt b/build.sbt index f935277a..51f12a1d 100644 --- a/build.sbt +++ b/build.sbt @@ -64,7 +64,10 @@ lazy val commonSettings = Seq( "org.scalameta" %%% "munit" % "1.0.0" % Test, "org.scalameta" %%% "munit-scalacheck" % "1.0.0-M11" % Test, "org.typelevel" %%% "munit-cats-effect" % "2.0.0" % Test, - "org.typelevel" %%% "scalacheck-effect-munit" % "2.0.0-M2" % Test + "org.typelevel" %%% "scalacheck-effect-munit" % "2.0.0-M2" % Test, + "org.typelevel" %%% "cats-kernel-laws" % "2.11.0" % Test, + "org.typelevel" %%% "cats-laws" % "2.11.0" % Test, + "org.typelevel" %%% "discipline-munit" % "2.0.0-M3" % Test ) ) @@ -401,7 +404,11 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "natchez-testkit", description := "In-memory Natchez implementation that is useful for testing", - tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.3.1").toMap + tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.3.1").toMap, + libraryDependencies ++= Seq( + "org.scalacheck" %%% "scalacheck" % "1.17.1", + "org.typelevel" %%% "case-insensitive-testing" % "1.4.2" + ) ) lazy val docs = project diff --git a/modules/core-tests/shared/src/test/scala/KernelInstancesLawSuite.scala b/modules/core-tests/shared/src/test/scala/KernelInstancesLawSuite.scala new file mode 100644 index 00000000..773ca805 --- /dev/null +++ b/modules/core-tests/shared/src/test/scala/KernelInstancesLawSuite.scala @@ -0,0 +1,30 @@ +// 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 + +import cats.kernel.laws.discipline.* +import cats.syntax.all.* +import munit.DisciplineSuite +import org.scalacheck.Prop +import org.typelevel.ci.CIString +import org.typelevel.ci.testing.arbitraries.* + +class KernelInstancesLawSuite extends DisciplineSuite with Arbitraries { + checkAll("Kernel.MonoidLaws", MonoidTests[Kernel].monoid) + checkAll("Kernel.EqLaws", EqTests[Kernel].eqv) + + test( + "Don't concatenate values that appear under the same key; instead, prefer the right-hand side" + ) { + Prop.forAll { (key: CIString, valueA: String, valueB: String) => + val kernelA = Kernel(Map(key -> valueA)) + val kernelB = Kernel(Map(key -> valueB)) + + // make sure if the same key exists in both kernels, the right-hand side wins, + // and the values aren't just combined + assertEquals(kernelA |+| kernelB, kernelB) + } + } +} diff --git a/modules/core/shared/src/main/scala/Kernel.scala b/modules/core/shared/src/main/scala/Kernel.scala index 1e39c10f..26eba894 100644 --- a/modules/core/shared/src/main/scala/Kernel.scala +++ b/modules/core/shared/src/main/scala/Kernel.scala @@ -4,10 +4,12 @@ package natchez -import org.typelevel.ci._ +import cats.syntax.all.* +import cats.{Eq, Monoid} +import org.typelevel.ci.* import java.util -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* /** An opaque hunk of data that we can hand off to another system (in the form of HTTP headers), * which can then create new spans as children of this one. By this mechanism we allow our trace @@ -21,4 +23,10 @@ final case class Kernel(toHeaders: Map[CIString, String]) { object Kernel { private[natchez] def fromJava(headers: util.Map[String, String]): Kernel = apply(headers.asScala.map { case (k, v) => CIString(k) -> v }.toMap) + + implicit val kernelMonoid: Monoid[Kernel] = + Monoid.instance(Kernel(Map.empty), (a, b) => Kernel(a.toHeaders ++ b.toHeaders)) + + implicit val kernelEq: Eq[Kernel] = + Eq[Map[CIString, String]].contramap(_.toHeaders) } diff --git a/modules/testkit/shared/src/main/scala/Arbitraries.scala b/modules/testkit/shared/src/main/scala/Arbitraries.scala new file mode 100644 index 00000000..7860b079 --- /dev/null +++ b/modules/testkit/shared/src/main/scala/Arbitraries.scala @@ -0,0 +1,74 @@ +// 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 + +import cats.data.Chain +import natchez.Span.Options.SpanCreationPolicy +import natchez.Span.SpanKind +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.{Arbitrary, Cogen, Gen} +import org.typelevel.ci.CIString +import org.typelevel.ci.testing.arbitraries.* + +trait Arbitraries { + implicit val arbKernel: Arbitrary[Kernel] = Arbitrary { + arbitrary[Map[CIString, String]].map(Kernel(_)) + } + + implicit val cogenKernel: Cogen[Kernel] = + Cogen[Map[CIString, String]].contramap(_.toHeaders) + + implicit val arbKernelToKernel: Arbitrary[Kernel => Kernel] = Arbitrary { + arbitrary[Map[CIString, String] => Map[CIString, String]].map { + (f: Map[CIString, String] => Map[CIString, String]) => + f.compose[Kernel](_.toHeaders).andThen(Kernel(_)) + } + } + + implicit val arbTraceValue: Arbitrary[TraceValue] = Arbitrary { + Gen.oneOf( + arbitrary[String].map(TraceValue.StringValue(_)), + arbitrary[Boolean].map(TraceValue.BooleanValue(_)), + arbitrary[Number].map(TraceValue.NumberValue(_)) + ) + } + + implicit val arbAttribute: Arbitrary[(String, TraceValue)] = Arbitrary { + for { + key <- arbitrary[String] + value <- arbitrary[TraceValue] + } yield key -> value + } + + implicit val arbSpanCreationPolicy: Arbitrary[SpanCreationPolicy] = Arbitrary { + Gen.oneOf(SpanCreationPolicy.Default, SpanCreationPolicy.Coalesce, SpanCreationPolicy.Suppress) + } + + implicit val arbSpanKind: Arbitrary[SpanKind] = Arbitrary { + Gen.oneOf( + SpanKind.Internal, + SpanKind.Client, + SpanKind.Server, + SpanKind.Producer, + SpanKind.Consumer + ) + } + + implicit val arbSpanOptions: Arbitrary[Span.Options] = Arbitrary { + for { + parentKernel <- arbitrary[Option[Kernel]] + spanCreationPolicy <- arbitrary[SpanCreationPolicy] + spanKind <- arbitrary[SpanKind] + links <- arbitrary[List[Kernel]].map(Chain.fromSeq) + } yield links.foldLeft { + parentKernel.foldLeft { + Span.Options.Defaults + .withSpanKind(spanKind) + .withSpanCreationPolicy(spanCreationPolicy) + }(_.withParentKernel(_)) + }(_.withLink(_)) + } + +}