From d1e7376cd409449ae0bf5bf32441bb0fd5379f92 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Tue, 3 Oct 2023 21:00:55 +0300 Subject: [PATCH 1/4] core-common and sdk-common: Add `Hash` and `Show` instances --- build.sbt | 4 +- .../org/typelevel/otel4s/Attribute.scala | 8 ++- .../org/typelevel/otel4s/AttributeKey.scala | 3 + .../org/typelevel/otel4s/AttributeType.scala | 3 + .../org/typelevel/otel4s/trace/SpanKind.scala | 6 ++ .../org/typelevel/otel4s/trace/Status.scala | 6 ++ .../otel4s/trace/SpanKindSuite.scala | 66 +++++++++++++++++++ .../typelevel/otel4s/trace/StatusSuite.scala | 52 +++++++++++++++ .../org/typelevel/otel4s/sdk/Attributes.scala | 15 +++++ .../org/typelevel/otel4s/sdk/Resource.scala | 17 +++-- .../sdk/trace/samplers/SamplingDecision.scala | 10 +++ .../samplers/SamplingDecisionSuite.scala | 56 ++++++++++++++-- 12 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala create mode 100644 core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala diff --git a/build.sbt b/build.sbt index d7629c216..bc9d98657 100644 --- a/build.sbt +++ b/build.sbt @@ -120,7 +120,8 @@ lazy val `core-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "otel4s-core-trace", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion, - "org.scodec" %%% "scodec-bits" % ScodecVersion + "org.scodec" %%% "scodec-bits" % ScodecVersion, + "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test ) ) .settings(scalafixSettings) @@ -169,6 +170,7 @@ lazy val `sdk-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "otel4s-sdk-trace", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % CatsEffectVersion, + "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test ), ) .settings(munitDependencies) diff --git a/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala b/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala index e070f8789..e28b3ac87 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala @@ -16,9 +16,9 @@ package org.typelevel.otel4s +import cats.Hash import cats.Show -import cats.implicits.showInterpolator -import cats.kernel.Hash +import cats.syntax.show._ /** Represents the key-value attribute. * @@ -76,4 +76,8 @@ String, Boolean, Long, Double, List[String], List[Boolean], List[Long], List[Dou implicit def hashAttribute[T: Hash]: Hash[Attribute[T]] = Hash.by(a => (a.key, a.value)) + implicit val hashAnyAttribute: Hash[Attribute[_]] = { + implicit val hashAny: Hash[Any] = Hash.fromUniversalHashCode + Hash.by(a => (a.key, a.value)) + } } diff --git a/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala b/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala index f0a770d28..844031f41 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala @@ -80,4 +80,7 @@ object AttributeKey { implicit def attributeKeyShow[A]: Show[AttributeKey[A]] = Show.show(key => show"${key.`type`}(${key.name})") + implicit val attributeKeyAnyHash: Hash[AttributeKey[_]] = + Hash.by(key => (key.name, key.`type`)) + } diff --git a/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala b/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala index 4b8ddad36..989b3eeac 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala @@ -41,4 +41,7 @@ object AttributeType { implicit def attributeTypeShow[A]: Show[AttributeType[A]] = Show.fromToString + implicit val attributeTypeAnyHash: Hash[AttributeType[_]] = + Hash.fromUniversalHashCode + } diff --git a/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanKind.scala b/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanKind.scala index a62eb001d..f0b4cdc7a 100644 --- a/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanKind.scala +++ b/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanKind.scala @@ -17,6 +17,9 @@ package org.typelevel.otel4s package trace +import cats.Hash +import cats.Show + /** Type of [[Span]]. Can be used to specify additional relationships between * spans in addition to a parent/child relationship. */ @@ -48,4 +51,7 @@ object SpanKind { * relationship between producer and consumer spans. */ case object Consumer extends SpanKind + + implicit val spanKindHash: Hash[SpanKind] = Hash.fromUniversalHashCode + implicit val spanKindShow: Show[SpanKind] = Show.fromToString } diff --git a/core/trace/src/main/scala/org/typelevel/otel4s/trace/Status.scala b/core/trace/src/main/scala/org/typelevel/otel4s/trace/Status.scala index 9b7e03715..d11d73641 100644 --- a/core/trace/src/main/scala/org/typelevel/otel4s/trace/Status.scala +++ b/core/trace/src/main/scala/org/typelevel/otel4s/trace/Status.scala @@ -16,6 +16,9 @@ package org.typelevel.otel4s.trace +import cats.Hash +import cats.Show + /** The set of canonical status codes */ sealed trait Status extends Product with Serializable @@ -33,4 +36,7 @@ object Status { /** The operation contains an error. */ case object Error extends Status + implicit val statusHash: Hash[Status] = Hash.fromUniversalHashCode + implicit val statusShow: Show[Status] = Show.fromToString + } diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala new file mode 100644 index 000000000..1dc03bcd8 --- /dev/null +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.trace + +import cats.Hash +import cats.Show +import munit._ +import org.scalacheck.Gen +import org.scalacheck.Prop + +class SpanKindSuite extends ScalaCheckSuite { + + private val spanKindGen: Gen[SpanKind] = + Gen.oneOf( + SpanKind.Internal, + SpanKind.Server, + SpanKind.Client, + SpanKind.Producer, + SpanKind.Consumer + ) + + property("Show[SpanKind]") { + Prop.forAll(spanKindGen) { spanKind => + val expected = spanKind match { + case SpanKind.Internal => "Internal" + case SpanKind.Server => "Server" + case SpanKind.Client => "Client" + case SpanKind.Producer => "Producer" + case SpanKind.Consumer => "Consumer" + } + + assertEquals(Show[SpanKind].show(spanKind), expected) + } + } + + test("Hash[SpanKind]") { + val allWithIndex = List( + SpanKind.Internal, + SpanKind.Server, + SpanKind.Client, + SpanKind.Producer, + SpanKind.Consumer + ).zipWithIndex + + allWithIndex.foreach { case (that, thatIdx) => + allWithIndex.foreach { case (other, otherIdx) => + assertEquals(Hash[SpanKind].eqv(that, other), thatIdx == otherIdx) + } + } + } + +} diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala new file mode 100644 index 000000000..a97bfd249 --- /dev/null +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.trace + +import cats.Hash +import cats.Show +import munit._ +import org.scalacheck.Gen +import org.scalacheck.Prop + +class StatusSuite extends ScalaCheckSuite { + + private val statusGen: Gen[Status] = + Gen.oneOf(Status.Unset, Status.Ok, Status.Error) + + property("Show[Status]") { + Prop.forAll(statusGen) { status => + val expected = status match { + case Status.Unset => "Unset" + case Status.Ok => "Ok" + case Status.Error => "Error" + } + + assertEquals(Show[Status].show(status), expected) + } + } + + test("Hash[Status]") { + val allWithIndex = List(Status.Unset, Status.Ok, Status.Error).zipWithIndex + + allWithIndex.foreach { case (that, thatIdx) => + allWithIndex.foreach { case (other, otherIdx) => + assertEquals(Hash[Status].eqv(that, other), thatIdx == otherIdx) + } + } + } + +} diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala index b2a3a3d83..d28871612 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala @@ -17,6 +17,7 @@ package org.typelevel.otel4s.sdk import cats.Applicative +import cats.Hash import cats.Monad import cats.Monoid import cats.Show @@ -59,6 +60,17 @@ final class Attributes private ( def toMap: Map[AttributeKey[_], Attribute[_]] = m def toList: List[Attribute[_]] = m.values.toList + override def hashCode(): Int = + Hash[Attributes].hash(this) + + override def equals(obj: Any): Boolean = + obj match { + case other: Attributes => Hash[Attributes].eqv(this, other) + case _ => false + } + + override def toString: String = + Show[Attributes].show(this) } object Attributes { @@ -79,6 +91,9 @@ object Attributes { .mkString("Attributes(", ", ", ")") } + implicit val hashAttributes: Hash[Attributes] = + Hash.by(_.m) + implicit val monoidAttributes: Monoid[Attributes] = new Monoid[Attributes] { def empty: Attributes = Attributes.Empty diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala index 0e7912f68..d4d071bf5 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala @@ -16,11 +16,12 @@ package org.typelevel.otel4s.sdk +import cats.Hash import cats.Show -import cats.implicits.catsSyntaxEitherId -import cats.implicits.catsSyntaxOptionId -import cats.implicits.catsSyntaxSemigroup -import cats.implicits.showInterpolator +import cats.syntax.either._ +import cats.syntax.option._ +import cats.syntax.semigroup._ +import cats.syntax.show._ import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError import org.typelevel.otel4s.semconv.resource.attributes.ResourceAttributes._ @@ -76,6 +77,9 @@ final case class Resource( throw _, identity ) + + override def toString: String = + Show[Resource].show(this) } object Resource { @@ -119,6 +123,9 @@ object Resource { val Default: Resource = TelemetrySdk.mergeIntoUnsafe(Mandatory) implicit val showResource: Show[Resource] = - r => show"Resource(${r.attributes}, ${r.schemaUrl})" + r => show"Resource{attributes=${r.attributes}, schemaUrl=${r.schemaUrl}}" + + implicit val hashResource: Hash[Resource] = + Hash.by(r => (r.attributes, r.schemaUrl)) } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecision.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecision.scala index 9cb01c4d0..4f3ef6c08 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecision.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecision.scala @@ -16,6 +16,9 @@ package org.typelevel.otel4s.sdk.trace.samplers +import cats.Hash +import cats.Show + /** A decision on whether a span should be recorded, sampled, or dropped. */ sealed abstract class SamplingDecision(val isSampled: Boolean) @@ -35,4 +38,11 @@ object SamplingDecision { /** The span is recorded, and the Sampled flag will be set. */ case object RecordAndSample extends SamplingDecision(true) + + implicit val samplingDecisionHash: Hash[SamplingDecision] = + Hash.fromUniversalHashCode + + implicit val samplingDecisionShow: Show[SamplingDecision] = + Show.fromToString + } diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala index ddb703162..8f8ba3b5c 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala @@ -16,18 +16,60 @@ package org.typelevel.otel4s.sdk.trace.samplers +import cats.Hash +import cats.Show import munit._ +import org.scalacheck.Gen +import org.scalacheck.Prop -class SamplingDecisionSuite extends FunSuite { - test("Drop should have isSampled = false") { - assertEquals(SamplingDecision.Drop.isSampled, false) +class SamplingDecisionSuite extends ScalaCheckSuite { + + private val samplingDecisionGen: Gen[SamplingDecision] = + Gen.oneOf( + SamplingDecision.Drop, + SamplingDecision.RecordOnly, + SamplingDecision.RecordAndSample + ) + + property("is sampled") { + Prop.forAll(samplingDecisionGen) { decision => + val expected = decision match { + case SamplingDecision.Drop => false + case SamplingDecision.RecordOnly => false + case SamplingDecision.RecordAndSample => true + } + + assertEquals(decision.isSampled, expected) + } } - test("RecordOnly should have isSampled = false") { - assertEquals(SamplingDecision.RecordOnly.isSampled, false) + property("Show[SamplingDecision]") { + Prop.forAll(samplingDecisionGen) { decision => + val expected = decision match { + case SamplingDecision.Drop => "Drop" + case SamplingDecision.RecordOnly => "RecordOnly" + case SamplingDecision.RecordAndSample => "RecordAndSample" + } + + assertEquals(Show[SamplingDecision].show(decision), expected) + } } - test("RecordAndSample should have isSampled = true") { - assertEquals(SamplingDecision.RecordAndSample.isSampled, true) + test("Hash[SamplingDecision]") { + val allWithIndex = List( + SamplingDecision.Drop, + SamplingDecision.RecordOnly, + SamplingDecision.RecordAndSample + ).zipWithIndex + + allWithIndex.foreach { case (that, thatIdx) => + allWithIndex.foreach { case (other, otherIdx) => + assertEquals( + Hash[SamplingDecision].eqv(that, other), + thatIdx == otherIdx + ) + } + } } + } From 282da326053615dd99d9d4aa533f5ddceb1942c8 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Wed, 4 Oct 2023 09:56:47 +0300 Subject: [PATCH 2/4] Use cats-discipline to test `Hash` laws --- build.sbt | 8 +++- .../org/typelevel/otel4s/Attribute.scala | 2 +- .../org/typelevel/otel4s/AttributeKey.scala | 2 +- .../org/typelevel/otel4s/AttributeType.scala | 2 +- .../otel4s/trace/SpanKindSuite.scala | 30 ++++++-------- .../typelevel/otel4s/trace/StatusSuite.scala | 24 +++++------ .../otel4s/trace/TraceFlagsSuite.scala | 25 +++++++++++- .../otel4s/sdk/ArbitraryInstances.scala | 40 +++++++++++++++++++ .../otel4s/sdk/AttributesLawTests.scala | 13 ++---- .../samplers/SamplingDecisionSuite.scala | 31 ++++++-------- 10 files changed, 112 insertions(+), 65 deletions(-) diff --git a/build.sbt b/build.sbt index bc9d98657..8355d0239 100644 --- a/build.sbt +++ b/build.sbt @@ -121,7 +121,9 @@ lazy val `core-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform) libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion, "org.scodec" %%% "scodec-bits" % ScodecVersion, - "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test + "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test, + "org.typelevel" %%% "cats-laws" % CatsVersion % Test, + "org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test ) ) .settings(scalafixSettings) @@ -170,7 +172,9 @@ lazy val `sdk-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "otel4s-sdk-trace", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % CatsEffectVersion, - "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test + "org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test, + "org.typelevel" %%% "cats-laws" % CatsVersion % Test, + "org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test ), ) .settings(munitDependencies) diff --git a/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala b/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala index e28b3ac87..ad386f4a5 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala @@ -76,7 +76,7 @@ String, Boolean, Long, Double, List[String], List[Boolean], List[Long], List[Dou implicit def hashAttribute[T: Hash]: Hash[Attribute[T]] = Hash.by(a => (a.key, a.value)) - implicit val hashAnyAttribute: Hash[Attribute[_]] = { + implicit val hashAttributeExistential: Hash[Attribute[_]] = { implicit val hashAny: Hash[Any] = Hash.fromUniversalHashCode Hash.by(a => (a.key, a.value)) } diff --git a/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala b/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala index 844031f41..aae0427eb 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala @@ -80,7 +80,7 @@ object AttributeKey { implicit def attributeKeyShow[A]: Show[AttributeKey[A]] = Show.show(key => show"${key.`type`}(${key.name})") - implicit val attributeKeyAnyHash: Hash[AttributeKey[_]] = + implicit val attributeKeyExistentialHash: Hash[AttributeKey[_]] = Hash.by(key => (key.name, key.`type`)) } diff --git a/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala b/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala index 989b3eeac..aa1c12900 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/AttributeType.scala @@ -41,7 +41,7 @@ object AttributeType { implicit def attributeTypeShow[A]: Show[AttributeType[A]] = Show.fromToString - implicit val attributeTypeAnyHash: Hash[AttributeType[_]] = + implicit val attributeTypeExistentialHash: Hash[AttributeType[_]] = Hash.fromUniversalHashCode } diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala index 1dc03bcd8..e90c0ddf9 100644 --- a/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanKindSuite.scala @@ -16,13 +16,15 @@ package org.typelevel.otel4s.trace -import cats.Hash import cats.Show +import cats.kernel.laws.discipline.HashTests import munit._ +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen import org.scalacheck.Gen import org.scalacheck.Prop -class SpanKindSuite extends ScalaCheckSuite { +class SpanKindSuite extends DisciplineSuite { private val spanKindGen: Gen[SpanKind] = Gen.oneOf( @@ -33,6 +35,14 @@ class SpanKindSuite extends ScalaCheckSuite { SpanKind.Consumer ) + private implicit val spanKindArbitrary: Arbitrary[SpanKind] = + Arbitrary(spanKindGen) + + private implicit val spanKindCogen: Cogen[SpanKind] = + Cogen[String].contramap(_.toString) + + checkAll("SpanKind.HashLaws", HashTests[SpanKind].hash) + property("Show[SpanKind]") { Prop.forAll(spanKindGen) { spanKind => val expected = spanKind match { @@ -47,20 +57,4 @@ class SpanKindSuite extends ScalaCheckSuite { } } - test("Hash[SpanKind]") { - val allWithIndex = List( - SpanKind.Internal, - SpanKind.Server, - SpanKind.Client, - SpanKind.Producer, - SpanKind.Consumer - ).zipWithIndex - - allWithIndex.foreach { case (that, thatIdx) => - allWithIndex.foreach { case (other, otherIdx) => - assertEquals(Hash[SpanKind].eqv(that, other), thatIdx == otherIdx) - } - } - } - } diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala index a97bfd249..b55f801fc 100644 --- a/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/StatusSuite.scala @@ -16,17 +16,27 @@ package org.typelevel.otel4s.trace -import cats.Hash import cats.Show +import cats.kernel.laws.discipline.HashTests import munit._ +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen import org.scalacheck.Gen import org.scalacheck.Prop -class StatusSuite extends ScalaCheckSuite { +class StatusSuite extends DisciplineSuite { private val statusGen: Gen[Status] = Gen.oneOf(Status.Unset, Status.Ok, Status.Error) + private implicit val statusArbitrary: Arbitrary[Status] = + Arbitrary(statusGen) + + private implicit val statusCogen: Cogen[Status] = + Cogen[String].contramap(_.toString) + + checkAll("Status.HashLaws", HashTests[Status].hash) + property("Show[Status]") { Prop.forAll(statusGen) { status => val expected = status match { @@ -39,14 +49,4 @@ class StatusSuite extends ScalaCheckSuite { } } - test("Hash[Status]") { - val allWithIndex = List(Status.Unset, Status.Ok, Status.Error).zipWithIndex - - allWithIndex.foreach { case (that, thatIdx) => - allWithIndex.foreach { case (other, otherIdx) => - assertEquals(Hash[Status].eqv(that, other), thatIdx == otherIdx) - } - } - } - } diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/TraceFlagsSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/TraceFlagsSuite.scala index 4553952ce..8eba8f882 100644 --- a/core/trace/src/test/scala/org/typelevel/otel4s/trace/TraceFlagsSuite.scala +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/TraceFlagsSuite.scala @@ -16,9 +16,26 @@ package org.typelevel.otel4s.trace +import cats.Show +import cats.kernel.laws.discipline.HashTests import munit._ +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen +import org.scalacheck.Gen +import org.scalacheck.Prop -class TraceFlagsSuite extends FunSuite { +class TraceFlagsSuite extends DisciplineSuite { + + private val traceFlagsGen: Gen[TraceFlags] = + Gen.chooseNum(0, 255).map(byte => TraceFlags.fromByte(byte.toByte)) + + private implicit val traceFlagsArbitrary: Arbitrary[TraceFlags] = + Arbitrary(traceFlagsGen) + + private implicit val traceFlagsCogen: Cogen[TraceFlags] = + Cogen[Byte].contramap(_.toByte) + + checkAll("TraceFlags.HashLaws", HashTests[TraceFlags].hash) test("default instances") { assertEquals(TraceFlags.Default.toHex, "00") @@ -51,4 +68,10 @@ class TraceFlagsSuite extends FunSuite { assertEquals(TraceFlags.fromHex("zxc"), None) } + property("Show[TraceFlags]") { + Prop.forAll(traceFlagsGen) { traceFlags => + assertEquals(Show[TraceFlags].show(traceFlags), traceFlags.toHex) + } + } + } diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala index 30c5a6adf..4da745a87 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala @@ -17,10 +17,14 @@ package org.typelevel.otel4s.sdk import org.scalacheck.Arbitrary +import org.scalacheck.Cogen import org.scalacheck.Gen import org.scalacheck.Gen.listOf import org.scalacheck.Gen.nonEmptyListOf +import org.scalacheck.rng.Seed import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.AttributeKey +import org.typelevel.otel4s.AttributeType object arbitrary extends ArbitraryInstances trait ArbitraryInstances { @@ -90,4 +94,40 @@ trait ArbitraryInstances { schemaUrl <- Gen.option(nonEmptyString) } yield Resource(attrs, schemaUrl)) + implicit val attributeTypeCogen: Cogen[AttributeType[_]] = + Cogen[String].contramap(_.toString) + + implicit def attributeKeyCogen[A]: Cogen[AttributeKey[A]] = + Cogen[(String, String)].contramap[AttributeKey[A]] { attribute => + (attribute.name, attribute.`type`.toString) + } + + implicit def attributeCogen[A: Cogen]: Cogen[Attribute[A]] = + Cogen[(AttributeKey[A], A)].contramap(a => (a.key, a.value)) + + implicit val attributeExistentialCogen: Cogen[Attribute[_]] = + Cogen { (seed, attr) => + def primitive[A: Cogen](seed: Seed): Seed = + Cogen[A].perturb(seed, attr.value.asInstanceOf[A]) + + def list[A: Cogen](seed: Seed): Seed = + Cogen[List[A]].perturb(seed, attr.value.asInstanceOf[List[A]]) + + val valueCogen: Seed => Seed = attr.key.`type` match { + case AttributeType.Boolean => primitive[Boolean] + case AttributeType.Double => primitive[Double] + case AttributeType.String => primitive[String] + case AttributeType.Long => primitive[Long] + case AttributeType.BooleanList => list[Boolean] + case AttributeType.DoubleList => list[Double] + case AttributeType.StringList => list[String] + case AttributeType.LongList => list[Long] + } + + valueCogen(attributeKeyCogen.perturb(seed, attr.key)) + } + + implicit val attributesCogen: Cogen[Attributes] = + Cogen[List[Attribute[_]]].contramap(_.toList) + } diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesLawTests.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesLawTests.scala index 7e2567e04..dacd414dc 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesLawTests.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesLawTests.scala @@ -16,21 +16,14 @@ package org.typelevel.otel4s.sdk -import cats.Hash -import cats.implicits.catsKernelStdHashForList -import cats.kernel.Eq +import cats.kernel.laws.discipline.HashTests import cats.kernel.laws.discipline.MonoidTests import munit.DisciplineSuite -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.sdk.arbitrary.attributes +import org.typelevel.otel4s.sdk.arbitrary._ class AttributesLawTests extends DisciplineSuite { - implicit def hashAttribute: Hash[Attribute[_]] = - Hash.fromUniversalHashCode[Attribute[_]] - - implicit val attributesEq: Eq[Attributes] = Eq.by(_.toList) - + checkAll("Attributes.HashLaws", HashTests[Attributes].hash) checkAll("Attributes.MonoidLaws", MonoidTests[Attributes].monoid) } diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala index 8f8ba3b5c..7976e4ce7 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/samplers/SamplingDecisionSuite.scala @@ -16,13 +16,15 @@ package org.typelevel.otel4s.sdk.trace.samplers -import cats.Hash import cats.Show +import cats.kernel.laws.discipline.HashTests import munit._ +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen import org.scalacheck.Gen import org.scalacheck.Prop -class SamplingDecisionSuite extends ScalaCheckSuite { +class SamplingDecisionSuite extends DisciplineSuite { private val samplingDecisionGen: Gen[SamplingDecision] = Gen.oneOf( @@ -31,6 +33,14 @@ class SamplingDecisionSuite extends ScalaCheckSuite { SamplingDecision.RecordAndSample ) + private implicit val samplingDecisionArbitrary: Arbitrary[SamplingDecision] = + Arbitrary(samplingDecisionGen) + + private implicit val samplingDecisionCogen: Cogen[SamplingDecision] = + Cogen[String].contramap(_.toString) + + checkAll("SamplingDecision.HashLaws", HashTests[SamplingDecision].hash) + property("is sampled") { Prop.forAll(samplingDecisionGen) { decision => val expected = decision match { @@ -55,21 +65,4 @@ class SamplingDecisionSuite extends ScalaCheckSuite { } } - test("Hash[SamplingDecision]") { - val allWithIndex = List( - SamplingDecision.Drop, - SamplingDecision.RecordOnly, - SamplingDecision.RecordAndSample - ).zipWithIndex - - allWithIndex.foreach { case (that, thatIdx) => - allWithIndex.foreach { case (other, otherIdx) => - assertEquals( - Hash[SamplingDecision].eqv(that, other), - thatIdx == otherIdx - ) - } - } - } - } From 571900a62b0d55c7ad529a14b43325fe7437341f Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Wed, 4 Oct 2023 10:32:07 +0300 Subject: [PATCH 3/4] Add more tests --- .../org/typelevel/otel4s/sdk/Resource.scala | 2 +- .../otel4s/sdk/ArbitraryInstances.scala | 5 ++++ .../otel4s/sdk/AttributesProps.scala | 14 +++++++++- .../otel4s/sdk/ResourceLawTests.scala | 27 +++++++++++++++++++ .../typelevel/otel4s/sdk/ResourceProps.scala | 11 ++++++++ .../typelevel/otel4s/sdk/ResourceSuite.scala | 12 +++++++-- 6 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceLawTests.scala diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala index d4d071bf5..069f01e90 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala @@ -62,7 +62,7 @@ final case class Resource( schemaUrlOptEither.map( Resource( - other.attributes |+| attributes, + attributes |+| other.attributes, _ ) ) diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala index 4da745a87..335f8079a 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ArbitraryInstances.scala @@ -130,4 +130,9 @@ trait ArbitraryInstances { implicit val attributesCogen: Cogen[Attributes] = Cogen[List[Attribute[_]]].contramap(_.toList) + implicit val resourceCogen: Cogen[Resource] = + Cogen[(Attributes, Option[String])].contramap { r => + (r.attributes, r.schemaUrl) + } + } diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala index c04804092..e189b504b 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala @@ -17,13 +17,15 @@ package org.typelevel.otel4s.sdk import cats.Id -import cats.implicits.catsSyntaxSemigroup +import cats.Show +import cats.syntax.semigroup._ import munit.ScalaCheckSuite import org.scalacheck.Arbitrary import org.scalacheck.Gen import org.scalacheck.Prop.forAll import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.sdk.arbitrary.attribute +import org.typelevel.otel4s.sdk.arbitrary.attributes class AttributesProps extends ScalaCheckSuite { @@ -128,4 +130,14 @@ class AttributesProps extends ScalaCheckSuite { } } + property("Show[Attributes]") { + forAll(attributes.arbitrary) { attributes => + val expected = attributes.toList + .map(Show[Attribute[_]].show) + .mkString("Attributes(", ", ", ")") + + assertEquals(Show[Attributes].show(attributes), expected) + } + } + } diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceLawTests.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceLawTests.scala new file mode 100644 index 000000000..4f28648ee --- /dev/null +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceLawTests.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk + +import cats.kernel.laws.discipline.HashTests +import munit.DisciplineSuite +import org.typelevel.otel4s.sdk.arbitrary._ + +class ResourceLawTests extends DisciplineSuite { + + checkAll("Resource.HashLaws", HashTests[Resource].hash) + +} diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala index cb4af061e..488016cb4 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala @@ -17,6 +17,8 @@ package org.typelevel.otel4s.sdk import cats.Id +import cats.Show +import cats.syntax.show._ import munit.ScalaCheckSuite import org.scalacheck.Prop.forAll import org.typelevel.otel4s.sdk.arbitrary.resource @@ -44,4 +46,13 @@ class ResourceProps extends ScalaCheckSuite { } } + property("Show[Resource]") { + forAll(resource.arbitrary) { resource => + val expected = + show"Resource{attributes=${resource.attributes}, schemaUrl=${resource.schemaUrl}}" + + assertEquals(Show[Resource].show(resource), expected) + } + } + } diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala index 50ed945b8..de75cd99b 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.typelevel.otel4s.sdk +package org.typelevel.otel4s +package sdk -import cats.implicits.catsSyntaxEitherId +import cats.syntax.either._ import munit.FunSuite import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError.SchemaUrlConflict @@ -81,4 +82,11 @@ class ResourceSuite extends FunSuite { checkSchemaMerge(None, None, None.asRight) } + test("Resource#mergeInto - merge attributes and prioritize the latter") { + val that = Resource(Attributes(Attribute("key", "that"))) + val other = Resource(Attributes(Attribute("key", "other"))) + + assertEquals(that.mergeInto(other), Right(other)) + } + } From 8e79f83e561eeb190e53a221e15c76eb6f8a5d3a Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Fri, 6 Oct 2023 10:05:46 +0300 Subject: [PATCH 4/4] Use `import cats.syntax.all._` --- .../src/main/scala/org/typelevel/otel4s/sdk/Resource.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala index 069f01e90..3547b19ed 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala @@ -18,10 +18,7 @@ package org.typelevel.otel4s.sdk import cats.Hash import cats.Show -import cats.syntax.either._ -import cats.syntax.option._ -import cats.syntax.semigroup._ -import cats.syntax.show._ +import cats.syntax.all._ import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError import org.typelevel.otel4s.semconv.resource.attributes.ResourceAttributes._