Repository: http4s/http4s
Pull request #4144 (2021-01-05 20:00:17)

Poor's man discipline to run codec laws
Repeat #4142 in `main` and rename `checkAllPropF` to `checkAllF`


Patch:
diff --git a/build.sbt b/build.sbt
index 63ee4fd200..491923296f 100644
--- a/build.sbt
+++ b/build.sbt
@@ -117,7 +117,10 @@ lazy val laws = libraryProject("laws")
       catsLaws,
       disciplineCore,
       scalacheck,
+      scalacheckEffectMunit,
+      munitCatsEffect
     ),
+    unusedCompileDependenciesFilter -= moduleFilter(organization = "org.typelevel", name = "scalacheck-effect-munit"),
   )
   .dependsOn(core)
 
diff --git a/laws/src/main/scala/org/http4s/laws/EntityEncoderLaws.scala b/laws/src/main/scala/org/http4s/laws/EntityEncoderLaws.scala
index ff7e975222..c0fc3b278c 100644
--- a/laws/src/main/scala/org/http4s/laws/EntityEncoderLaws.scala
+++ b/laws/src/main/scala/org/http4s/laws/EntityEncoderLaws.scala
@@ -23,7 +23,7 @@ import cats.laws._
 import org.http4s.headers.{`Content-Length`, `Transfer-Encoding`}
 
 trait EntityEncoderLaws[F[_], A] {
-  implicit def F: Sync[F]
+  implicit def F: Effect[F]
 
   implicit def encoder: EntityEncoder[F, A]
 
@@ -44,7 +44,7 @@ trait EntityEncoderLaws[F[_], A] {
 
 object EntityEncoderLaws {
   def apply[F[_], A](implicit
-      F0: Sync[F],
+      F0: Effect[F],
       entityEncoderFA: EntityEncoder[F, A]
   ): EntityEncoderLaws[F, A] =
     new EntityEncoderLaws[F, A] {
diff --git a/laws/src/main/scala/org/http4s/laws/discipline/LawAdapter.scala b/laws/src/main/scala/org/http4s/laws/discipline/LawAdapter.scala
new file mode 100644
index 0000000000..70d2927ae0
--- /dev/null
+++ b/laws/src/main/scala/org/http4s/laws/discipline/LawAdapter.scala
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019 http4s.org
+ *
+ * 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.http4s.laws.discipline
+
+import cats.Eq
+import cats.effect._
+import cats.effect.implicits._
+import cats.laws.IsEq
+import cats.syntax.all._
+import org.scalacheck.Arbitrary
+import org.scalacheck.Shrink
+import org.scalacheck.effect.PropF
+
+import munit.CatsEffectAssertions._
+
+trait LawAdapter {
+
+  def booleanPropF[F[_]: Effect, A: Arbitrary: Shrink](
+      propLabel: String,
+      prop: => Boolean): (String, PropF[IO]) =
+    propLabel -> PropF.boolean(prop)
+
+  def isEqPropF[F[_]: Effect, A: Arbitrary: Shrink, B: Eq](
+      propLabel: String,
+      prop: A => IsEq[F[B]]): (String, PropF[IO]) =
+    propLabel -> PropF
+      .forAllF { (a: A) =>
+        val isEq = prop(a)
+        (isEq.lhs, isEq.rhs).mapN(_ === _).toIO.assert
+      }
+      .map(p => p.copy(labels = p.labels + propLabel))
+
+}
+
+object LawAdapter extends LawAdapter


Test patch:
diff --git a/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSpec.scala b/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSpec.scala
deleted file mode 100644
index df042ec98c..0000000000
--- a/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSpec.scala
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2018 http4s.org
- *
- * 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.http4s
-package booPickle
-
-import boopickle.Default._
-import cats.effect.IO
-import cats.Eq
-import cats.effect.laws.util.TestContext
-import cats.effect.laws.util.TestInstances._
-import org.http4s.laws.discipline.EntityCodecTests
-import org.scalacheck.Arbitrary
-import org.scalacheck.Gen
-
-class BoopickleSpec extends Http4sSpec with BooPickleInstances {
-  implicit val testContext = TestContext()
-
-  trait Fruit {
-    val weight: Double
-    def color: String
-  }
-
-  case class Banana(weight: Double) extends Fruit {
-    def color = "yellow"
-  }
-
-  case class Kiwi(weight: Double) extends Fruit {
-    def color = "brown"
-  }
-
-  case class Carambola(weight: Double) extends Fruit {
-    def color = "yellow"
-  }
-
-  implicit val fruitPickler: Pickler[Fruit] =
-    compositePickler[Fruit].addConcreteType[Banana].addConcreteType[Kiwi].addConcreteType[Carambola]
-
-  implicit val encoder = booEncoderOf[IO, Fruit]
-  implicit val decoder = booOf[IO, Fruit]
-
-  implicit val fruitArbitrary: Arbitrary[Fruit] = Arbitrary {
-    for {
-      w <- Gen.posNum[Double]
-      f <- Gen.oneOf(Banana(w), Kiwi(w), Carambola(w))
-    } yield f
-  }
-
-  implicit val fruitEq: Eq[Fruit] = Eq.fromUniversalEquals
-
-  checkAll("EntityCodec[IO, Fruit]", EntityCodecTests[IO, Fruit].entityCodec)
-}
diff --git a/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSuite.scala b/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSuite.scala
index 2ff02946bc..41ba09a158 100644
--- a/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSuite.scala
+++ b/boopickle/src/test/scala/org/http4s/booPickle/BoopickleSuite.scala
@@ -22,12 +22,12 @@ import cats.effect.IO
 import cats.Eq
 import cats.effect.laws.util.TestContext
 import org.http4s.headers.`Content-Type`
-// import org.http4s.laws.discipline.EntityCodecTests
+import org.http4s.laws.discipline.EntityCodecTests
 import org.http4s.MediaType
 import org.scalacheck.Arbitrary
 import org.scalacheck.Gen
 
-class BoopickleSuite extends Http4sSuite with BooPickleInstances {
+class BoopickleSuite extends Http4sSuite with BooPickleInstances with Http4sLawSuite {
   implicit val testContext = TestContext()
 
   trait Fruit {
@@ -80,5 +80,5 @@ class BoopickleSuite extends Http4sSuite with BooPickleInstances {
     result.value.map(assertEquals(_, Right(Banana(10.0))))
   }
 
-  // checkAll("EntityCodec[IO, Fruit]", EntityCodecTests[IO, Fruit].entityCodec)
+  checkAllF("EntityCodec[IO, Fruit]", EntityCodecTests[IO, Fruit].entityCodecF)
 }
diff --git a/circe/src/test/scala/org/http4s/circe/CirceSuite.scala b/circe/src/test/scala/org/http4s/circe/CirceSuite.scala
index 070888d96a..6c82b1e60f 100644
--- a/circe/src/test/scala/org/http4s/circe/CirceSuite.scala
+++ b/circe/src/test/scala/org/http4s/circe/CirceSuite.scala
@@ -29,10 +29,12 @@ import org.http4s.Status.Ok
 import org.http4s.circe._
 import org.http4s.headers.`Content-Type`
 import org.http4s.jawn.JawnDecodeSupportSuite
+import org.http4s.laws.discipline.EntityCodecTests
+import io.circe.testing.instances._
 import io.circe.jawn.CirceSupportParser
 
 // Originally based on ArgonautSuite
-class CirceSuite extends JawnDecodeSupportSuite[Json] {
+class CirceSuite extends JawnDecodeSupportSuite[Json] with Http4sLawSuite {
   implicit val testContext = TestContext()
 
   val CirceInstancesWithCustomErrors = CirceInstances.builder
@@ -369,5 +371,5 @@ class CirceSuite extends JawnDecodeSupportSuite[Json] {
       .assertEquals(true)
   }
 
-  // checkAll("EntityCodec[IO, Json]", EntityCodecTests[IO, Json].entityCodec)
+  checkAllF("EntityCodec[IO, Json]", EntityCodecTests[IO, Json].entityCodecF)
 }
diff --git a/laws/src/main/scala/org/http4s/laws/discipline/EntityCodecTests.scala b/laws/src/main/scala/org/http4s/laws/discipline/EntityCodecTests.scala
index 0fb823c6fc..6454d82f2d 100644
--- a/laws/src/main/scala/org/http4s/laws/discipline/EntityCodecTests.scala
+++ b/laws/src/main/scala/org/http4s/laws/discipline/EntityCodecTests.scala
@@ -24,13 +24,11 @@ import cats.effect.laws.util.TestContext
 import cats.effect.laws.util.TestInstances._
 import cats.laws.discipline._
 import org.scalacheck.{Arbitrary, Prop, Shrink}
+import org.scalacheck.effect.PropF
 
 trait EntityCodecTests[F[_], A] extends EntityEncoderTests[F, A] {
   def laws: EntityCodecLaws[F, A]
 
-  implicit def eqDecodeFailure: Eq[DecodeFailure] =
-    Eq.fromUniversalEquals[DecodeFailure]
-
   def entityCodec(implicit
       arbitraryA: Arbitrary[A],
       shrinkA: Shrink[A],
@@ -44,6 +42,14 @@ trait EntityCodecTests[F[_], A] extends EntityEncoderTests[F, A] {
         laws.entityCodecRoundTrip(a)
       }
     )
+
+  def entityCodecF(implicit
+      arbitraryA: Arbitrary[A],
+      shrinkA: Shrink[A],
+      eqA: Eq[A]
+  ): List[(String, PropF[IO])] =
+    LawAdapter.isEqPropF("roundTrip", laws.entityCodecRoundTrip _) :: entityEncoderF
+
 }
 
 object EntityCodecTests {
diff --git a/laws/src/main/scala/org/http4s/laws/discipline/EntityEncoderTests.scala b/laws/src/main/scala/org/http4s/laws/discipline/EntityEncoderTests.scala
index 184ac544bf..9feae53890 100644
--- a/laws/src/main/scala/org/http4s/laws/discipline/EntityEncoderTests.scala
+++ b/laws/src/main/scala/org/http4s/laws/discipline/EntityEncoderTests.scala
@@ -21,7 +21,10 @@ package discipline
 import cats.Eq
 import cats.effect._
 import cats.laws.discipline._
-import org.scalacheck.{Arbitrary, Prop, Shrink}
+import org.scalacheck.Arbitrary
+import org.scalacheck.Prop
+import org.scalacheck.Shrink
+import org.scalacheck.effect.PropF
 import org.typelevel.discipline.Laws
 
 trait EntityEncoderTests[F[_], A] extends Laws {
@@ -41,6 +44,22 @@ trait EntityEncoderTests[F[_], A] extends Laws {
       "noContentLengthInStaticHeaders" -> laws.noContentLengthInStaticHeaders,
       "noTransferEncodingInStaticHeaders" -> laws.noTransferEncodingInStaticHeaders
     )
+
+  def entityEncoderF(implicit
+      arbitraryA: Arbitrary[A],
+      shrinkA: Shrink[A]
+  ): List[(String, PropF[IO])] = {
+    implicit val effectF = laws.F
+    List(
+      LawAdapter.isEqPropF("accurateContentLength", laws.accurateContentLengthIfDefined _),
+      LawAdapter.booleanPropF(
+        "noContentLengthInStaticHeaders",
+        laws.noContentLengthInStaticHeaders),
+      LawAdapter.booleanPropF(
+        "noTransferEncodingInStaticHeaders",
+        laws.noTransferEncodingInStaticHeaders)
+    )
+  }
 }
 
 object EntityEncoderTests {
diff --git a/circe/src/test/scala/org/http4s/circe/CirceSpec.scala b/testing/src/test/scala/org/http4s/Http4sLawSuite.scala
similarity index 50%
rename from circe/src/test/scala/org/http4s/circe/CirceSpec.scala
rename to testing/src/test/scala/org/http4s/Http4sLawSuite.scala
index 04aa7110be..d7e21fd634 100644
--- a/circe/src/test/scala/org/http4s/circe/CirceSpec.scala
+++ b/testing/src/test/scala/org/http4s/Http4sLawSuite.scala
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 http4s.org
+ * Copyright 2016 http4s.org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,19 +15,20 @@
  */
 
 package org.http4s
-package circe.test // Get out of circe package so we can import custom instances
 
-import cats.effect.IO
-import cats.effect.laws.util.TestContext
-import cats.effect.laws.util.TestInstances._
-import cats.instances.boolean._
-import io.circe._
-import io.circe.testing.instances._
-import org.http4s.circe._
-import org.http4s.laws.discipline.EntityCodecTests
+import munit.ScalaCheckEffectSuite
+import org.scalacheck.effect.PropF
 
-// Originally based on ArgonautSpec
-class CirceSpec extends Http4sSpec {
-  implicit val testContext = TestContext()
-  checkAll("EntityCodec[IO, Json]", EntityCodecTests[IO, Json].entityCodec)
+/** Poor's man discipline runner to check a set of PropF[F]
+  */
+trait Http4sLawSuite extends ScalaCheckEffectSuite {
+  def checkAllF[F[_]](
+      name: String,
+      original: List[(String, PropF[F])]
+  )(implicit loc: munit.Location): Unit =
+    original.foreach { case (l, p) =>
+      test(s"$name - $l") {
+        p
+      }
+    }
 }
diff --git a/tests/src/test/scala/org/http4s/EntityCodecSpec.scala b/tests/src/test/scala/org/http4s/EntityCodecSuite.scala
similarity index 60%
rename from tests/src/test/scala/org/http4s/EntityCodecSpec.scala
rename to tests/src/test/scala/org/http4s/EntityCodecSuite.scala
index 423a2576bb..15b54ba474 100644
--- a/tests/src/test/scala/org/http4s/EntityCodecSpec.scala
+++ b/tests/src/test/scala/org/http4s/EntityCodecSuite.scala
@@ -18,26 +18,22 @@ package org.http4s
 
 import cats.Eq
 import cats.effect.IO
-import cats.effect.laws.util.TestContext
-import cats.effect.laws.util.TestInstances._
 import fs2.Chunk
 import org.http4s.laws.discipline.EntityCodecTests
 import org.http4s.testing.fs2Arbitraries._
 
-class EntityCodecSpec extends Http4sSpec {
-  implicit val testContext: TestContext = TestContext()
-
+class EntityCodecSuite extends Http4sSuite with Http4sLawSuite {
   implicit def eqArray[A](implicit ev: Eq[Vector[A]]): Eq[Array[A]] =
     Eq.by(_.toVector)
 
   implicit def eqChunk[A](implicit ev: Eq[Vector[A]]): Eq[Chunk[A]] =
     Eq.by(_.toVector)
 
-  checkAll("EntityCodec[IO, String]", EntityCodecTests[IO, String].entityCodec)
-  checkAll("EntityCodec[IO, Array[Char]]", EntityCodecTests[IO, Array[Char]].entityCodec)
+  checkAllF("EntityCodec[IO, String]", EntityCodecTests[IO, String].entityCodecF)
+  checkAllF("EntityCodec[IO, Array[Char]]", EntityCodecTests[IO, Array[Char]].entityCodecF)
 
-  checkAll("EntityCodec[IO, Chunk[Byte]]", EntityCodecTests[IO, Chunk[Byte]].entityCodec)
-  checkAll("EntityCodec[IO, Array[Byte]]", EntityCodecTests[IO, Array[Byte]].entityCodec)
+  checkAllF("EntityCodec[IO, Chunk[Byte]]", EntityCodecTests[IO, Chunk[Byte]].entityCodecF)
+  checkAllF("EntityCodec[IO, Array[Byte]]", EntityCodecTests[IO, Array[Byte]].entityCodecF)
 
-  checkAll("EntityCodec[IO, Unit]", EntityCodecTests[IO, Unit].entityCodec)
+  checkAllF("EntityCodec[IO, Unit]", EntityCodecTests[IO, Unit].entityCodecF)
 }