Repository: http4s/http4s
Pull request #6657 (2022-09-08 18:21:26)

Update to munit 1.0.0-M6
At this point it is quite stable and brings many improvements we could benefit from, such as async resource fixtures (thanks to @danicheg for working on that in munit core :)


Patch:
diff --git a/project/Http4sPlugin.scala b/project/Http4sPlugin.scala
index ec645cfaed..d8924a34b9 100644
--- a/project/Http4sPlugin.scala
+++ b/project/Http4sPlugin.scala
@@ -118,13 +118,13 @@ object Http4sPlugin extends AutoPlugin {
     val logback = "1.2.6"
     val log4cats = "2.4.0"
     val log4s = "1.10.0"
-    val munit = "0.7.29"
-    val munitCatsEffect = "1.0.7"
-    val munitDiscipline = "1.0.9"
+    val munit = "1.0.0-M6"
+    val munitCatsEffect = "2.0.0-M2"
+    val munitDiscipline = "2.0.0-M3"
     val netty = "4.1.81.Final"
     val quasiquotes = "2.1.0"
     val scalacheck = "1.16.0"
-    val scalacheckEffect = "1.0.4"
+    val scalacheckEffect = "2.0.0-M1"
     val scalaJavaLocales = "1.4.1"
     val scalaJavaTime = "2.4.0"
     val scodecBits = "1.1.34"
@@ -174,7 +174,7 @@ object Http4sPlugin extends AutoPlugin {
   lazy val logbackClassic = "ch.qos.logback" % "logback-classic" % V.logback
   lazy val munit = Def.setting("org.scalameta" %%% "munit" % V.munit)
   lazy val munitCatsEffect =
-    Def.setting("org.typelevel" %%% "munit-cats-effect-3" % V.munitCatsEffect)
+    Def.setting("org.typelevel" %%% "munit-cats-effect" % V.munitCatsEffect)
   lazy val munitDiscipline = Def.setting("org.typelevel" %%% "discipline-munit" % V.munitDiscipline)
   lazy val nettyBuffer = "io.netty" % "netty-buffer" % V.netty
   lazy val nettyCodecHttp = "io.netty" % "netty-codec-http" % V.netty


Test patch:
diff --git a/client-testkit/shared/src/main/scala/org/http4s/client/testkit/ClientRouteTestBattery.scala b/client-testkit/shared/src/main/scala/org/http4s/client/testkit/ClientRouteTestBattery.scala
index 7177fb1294..277f1be6b5 100644
--- a/client-testkit/shared/src/main/scala/org/http4s/client/testkit/ClientRouteTestBattery.scala
+++ b/client-testkit/shared/src/main/scala/org/http4s/client/testkit/ClientRouteTestBattery.scala
@@ -22,6 +22,7 @@ import cats.effect._
 import cats.syntax.all._
 import fs2._
 import munit._
+import munit.catseffect._
 import org.http4s.client.dsl.Http4sClientDsl
 import org.http4s.client.testkit.scaffold.ServerScaffold
 import org.http4s.client.testkit.testroutes.GetRoutes
@@ -54,142 +55,126 @@ private[http4s] abstract class ClientRouteTestBattery(name: String)
       Ok(req.body)
   }
 
-  val serverClient: Fixture[IO[(() => ServerScaffold[IO], () => Client[IO])]] =
-    resourceSuiteDeferredFixture(
-      "serverClient",
-      ServerScaffold[IO](1, false, testHandler)
-        .map(() => _)
-        .product(clientResource.map(() => _)),
+  val server: IOFixture[ServerScaffold[IO]] =
+    resourceSuiteFixture(
+      "server",
+      ServerScaffold[IO](1, false, testHandler),
+    )
+
+  val client: IOFixture[Client[IO]] =
+    resourceSuiteFixture(
+      "client",
+      clientResource,
     )
 
   test(s"$name Repeat a simple request") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val path = GetRoutes.SimplePath
+    val address = server().addresses.head
+    val path = GetRoutes.SimplePath
 
-      def fetchBody =
-        client().toKleisli(_.as[String]).local { (uri: Uri) =>
-          Request(uri = uri)
-        }
+    def fetchBody =
+      client().toKleisli(_.as[String]).local { (uri: Uri) =>
+        Request(uri = uri)
+      }
+
+    val url = Uri.fromString(s"http://$address$path").yolo
+    (0 until 10).toVector
+      .parTraverse(_ => fetchBody.run(url).map(_.length))
+      .map(_.forall(_ =!= 0))
+      .assert
 
-      val url = Uri.fromString(s"http://$address$path").yolo
-      (0 until 10).toVector
-        .parTraverse(_ => fetchBody.run(url).map(_.length))
-        .map(_.forall(_ =!= 0))
-        .assert
-    }
   }
 
   test(s"$name POST an empty body") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val uri = Uri.fromString(s"http://$address/echo").yolo
-      val req = POST(uri)
-      val body = client().expect[String](req)
-      body.assertEquals("")
-    }
+    val address = server().addresses.head
+    val uri = Uri.fromString(s"http://$address/echo").yolo
+    val req = POST(uri)
+    val body = client().expect[String](req)
+    body.assertEquals("")
   }
 
   test(s"$name POST a normal body") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val uri = Uri.fromString(s"http://$address/echo").yolo
-      val req = POST("This is normal.", uri)
-      val body = client().expect[String](req)
-      body.assertEquals("This is normal.")
-    }
+    val address = server().addresses.head
+    val uri = Uri.fromString(s"http://$address/echo").yolo
+    val req = POST("This is normal.", uri)
+    val body = client().expect[String](req)
+    body.assertEquals("This is normal.")
   }
 
   test(s"$name POST a chunked body".flaky) {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val uri = Uri.fromString(s"http://$address/echo").yolo
-      val req = POST(Stream.emits("This is chunked.".toSeq.map(_.toString)).covary[IO], uri)
-      val body = client().expect[String](req)
-      body.assertEquals("This is chunked.")
-    }
+    val address = server().addresses.head
+    val uri = Uri.fromString(s"http://$address/echo").yolo
+    val req = POST(Stream.emits("This is chunked.".toSeq.map(_.toString)).covary[IO], uri)
+    val body = client().expect[String](req)
+    body.assertEquals("This is chunked.")
   }
 
   test(s"$name POST a multipart body") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val uri = Uri.fromString(s"http://$address/echo").yolo
-      Multiparts
-        .forSync[IO]
-        .flatMap { multiparts =>
-          multiparts.multipart(Vector(Part.formData("text", "This is text."))).flatMap {
-            multipart =>
-              val req = POST(multipart, uri).withHeaders(multipart.headers)
-              val body = client().expect[String](req)
-              body.map(_.contains("This is text."))
-          }
+    val address = server().addresses.head
+    val uri = Uri.fromString(s"http://$address/echo").yolo
+    Multiparts
+      .forSync[IO]
+      .flatMap { multiparts =>
+        multiparts.multipart(Vector(Part.formData("text", "This is text."))).flatMap { multipart =>
+          val req = POST(multipart, uri).withHeaders(multipart.headers)
+          val body = client().expect[String](req)
+          body.map(_.contains("This is text."))
         }
-        .assert
-    }
+      }
+      .assert
   }
 
   GetRoutes.getPaths.toList.foreach { case (path, expected) =>
     test(s"$name Execute GET $path") {
-      serverClient().flatMap { case (server, client) =>
-        val address = server().addresses.head
-        val req = Request[IO](uri = Uri.fromString(s"http://$address$path").yolo)
-        client()
-          .run(req)
-          .use(resp => expected.flatMap(checkResponse(resp, _)))
-          .assert
-      }
+      val address = server().addresses.head
+      val req = Request[IO](uri = Uri.fromString(s"http://$address$path").yolo)
+      client()
+        .run(req)
+        .use(resp => expected.flatMap(checkResponse(resp, _)))
+        .assert
     }
   }
 
   test("Mitigates request splitting attack in URI path") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val name = address.host.toString
-      val port = address.port.value
-      val req = Request[IO](
-        uri = Uri(
-          authority = Uri.Authority(None, Uri.RegName(name), port = port.some).some,
-          path = Uri.Path.Root / Uri.Path.Segment.encoded(
-            "request-splitting HTTP/1.0\r\nEvil:true\r\nHide-Protocol-Version:"
-          ),
-        )
+    val address = server().addresses.head
+    val name = address.host.toString
+    val port = address.port.value
+    val req = Request[IO](
+      uri = Uri(
+        authority = Uri.Authority(None, Uri.RegName(name), port = port.some).some,
+        path = Uri.Path.Root / Uri.Path.Segment.encoded(
+          "request-splitting HTTP/1.0\r\nEvil:true\r\nHide-Protocol-Version:"
+        ),
       )
-      client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
-    }
+    )
+    client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
   }
 
   test("Mitigates request splitting attack in URI RegName") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val name = address.host
-      val port = address.port.value
-      val req = Request[IO](uri =
-        Uri(
-          authority =
-            Uri.Authority(None, Uri.RegName(s"${name}\r\nEvil:true\r\n"), port = port.some).some,
-          path = path"/request-splitting",
-        )
+    val address = server().addresses.head
+    val name = address.host
+    val port = address.port.value
+    val req = Request[IO](uri =
+      Uri(
+        authority =
+          Uri.Authority(None, Uri.RegName(s"${name}\r\nEvil:true\r\n"), port = port.some).some,
+        path = path"/request-splitting",
       )
-      client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
-    }
+    )
+    client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
   }
 
   test("Mitigates request splitting attack in field name") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val req = Request[IO](uri = Uri.fromString(s"http://$address/request-splitting").yolo)
-        .putHeaders(Header.Raw(ci"Fine:\r\nEvil:true\r\n", "oops"))
-      client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
-    }
+    val address = server().addresses.head
+    val req = Request[IO](uri = Uri.fromString(s"http://$address/request-splitting").yolo)
+      .putHeaders(Header.Raw(ci"Fine:\r\nEvil:true\r\n", "oops"))
+    client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
   }
 
   test("Mitigates request splitting attack in field value") {
-    serverClient().flatMap { case (server, client) =>
-      val address = server().addresses.head
-      val req = Request[IO](uri = Uri.fromString(s"http://$address/request-splitting").yolo)
-        .putHeaders(Header.Raw(ci"X-Carrier", "\r\nEvil:true\r\n"))
-      client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
-    }
+    val address = server().addresses.head
+    val req = Request[IO](uri = Uri.fromString(s"http://$address/request-splitting").yolo)
+      .putHeaders(Header.Raw(ci"X-Carrier", "\r\nEvil:true\r\n"))
+    client().status(req).handleError(_ => Status.Ok).assertEquals(Status.Ok)
   }
 
   private def checkResponse(rec: Response[IO], expected: Response[IO]): IO[Boolean] = {
@@ -210,23 +195,23 @@ private[http4s] abstract class ClientRouteTestBattery(name: String)
     } yield true
   }
 
-  private def registerSuiteFixture[A](fixture: Fixture[A]): Fixture[A] = {
+  private def registerSuiteFixture[A](fixture: IOFixture[A]): IOFixture[A] = {
     suiteFixtures += fixture
     fixture
   }
 
-  private def resourceSuiteDeferredFixture[A](
+  private def resourceSuiteFixture[A](
       name: String,
       resource: Resource[IO, A],
-  ): Fixture[IO[A]] =
-    registerSuiteFixture(UnsafeResourceSuiteLocalDeferredFixture(name, resource))
+  ): IOFixture[A] =
+    registerSuiteFixture(ResourceSuiteLocalFixture(name, resource))
 
   implicit private class ParseResultSyntax[A](self: ParseResult[A]) {
     def yolo: A = self.valueOr(e => sys.error(e.toString))
   }
 
-  private[this] lazy val suiteFixtures = List.newBuilder[Fixture[_]]
+  private[this] lazy val suiteFixtures = List.newBuilder[IOFixture[_]]
 
-  override def munitFixtures: Seq[Fixture[_]] = suiteFixtures.result()
+  override def munitFixtures: Seq[IOFixture[_]] = suiteFixtures.result()
 
 }
diff --git a/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerMtlsSuite.scala b/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerMtlsSuite.scala
index 2ca02a3ddd..dab9ffa3e8 100644
--- a/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerMtlsSuite.scala
+++ b/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerMtlsSuite.scala
@@ -106,7 +106,7 @@ class EmberServerMtlsSuite extends Http4sSuite {
     (server(tlsParams), client(clientTlsContext)).mapN(FunFixture.map2(_, _))
 
   private def client(tlsContextResource: Resource[IO, TLSContext[IO]]) =
-    ResourceFixture(clientResource(tlsContextResource))
+    ResourceFunFixture(clientResource(tlsContextResource))
 
   private def clientResource(tlsContextResource: Resource[IO, TLSContext[IO]]) =
     for {
@@ -119,7 +119,7 @@ class EmberServerMtlsSuite extends Http4sSuite {
     } yield emberClient
 
   private def server(tlsParams: TLSParameters) =
-    ResourceFixture(serverResource(tlsParams))
+    ResourceFunFixture(serverResource(tlsParams))
 
   private def serverResource(tlsParams: TLSParameters) =
     for {
diff --git a/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerWebSocketSuite.scala b/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerWebSocketSuite.scala
index 4192cf2c7a..881a5420e5 100644
--- a/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerWebSocketSuite.scala
+++ b/ember-server/jvm/src/test/scala/org/http4s/ember/server/EmberServerWebSocketSuite.scala
@@ -83,7 +83,7 @@ class EmberServerWebSocketSuite extends Http4sSuite with DispatcherIOFixture {
       .withHttpWebSocketApp(service[IO])
       .build
 
-  private def fixture = (ResourceFixture(serverResource), dispatcher).mapN(FunFixture.map2(_, _))
+  private def fixture = (ResourceFunFixture(serverResource), dispatcher).mapN(FunFixture.map2(_, _))
 
   sealed case class Client(
       waitOpen: Deferred[IO, Option[Throwable]],
diff --git a/ember-server/shared/src/test/scala/org/http4s/ember/server/ConnectionSuite.scala b/ember-server/shared/src/test/scala/org/http4s/ember/server/ConnectionSuite.scala
index fb70a5a1d1..f4e4523faa 100644
--- a/ember-server/shared/src/test/scala/org/http4s/ember/server/ConnectionSuite.scala
+++ b/ember-server/shared/src/test/scala/org/http4s/ember/server/ConnectionSuite.scala
@@ -89,7 +89,7 @@ class ConnectionSuite extends Http4sSuite {
   private def fixture(
       idleTimeout: FiniteDuration = 60.seconds,
       headerTimeout: FiniteDuration = 60.seconds,
-  ) = ResourceFixture(
+  ) = ResourceFunFixture(
     for {
       server <- serverResource(idleTimeout, headerTimeout)
       client <- clientResource(server.addressIp4s)
diff --git a/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala b/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
index 2873807413..8793f3a4da 100644
--- a/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
+++ b/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
@@ -54,9 +54,9 @@ class EmberServerSuite extends Http4sSuite {
       .withHttpApp(service[IO])
       .build
 
-  private val client = ResourceFixture(EmberClientBuilder.default[IO].build)
+  private val client = ResourceFunFixture(EmberClientBuilder.default[IO].build)
 
-  private def server(receiveBufferSize: Int = 256 * 1024) = ResourceFixture(
+  private def server(receiveBufferSize: Int = 256 * 1024) = ResourceFunFixture(
     EmberServerBuilder
       .default[IO]
       .withHttpApp(service[IO])
diff --git a/tests/jvm/src/main/scala/org/http4s/Http4sSuitePlatform.scala b/tests/jvm/src/main/scala/org/http4s/Http4sSuitePlatform.scala
index 3351e84ad0..b226363d99 100644
--- a/tests/jvm/src/main/scala/org/http4s/Http4sSuitePlatform.scala
+++ b/tests/jvm/src/main/scala/org/http4s/Http4sSuitePlatform.scala
@@ -16,15 +16,7 @@
 
 package org.http4s
 
-import cats.effect.IO
-import cats.effect.Resource
-
 trait Http4sSuitePlatform { this: Http4sSuite =>
-
-  def resourceSuiteFixture[A](name: String, resource: Resource[IO, A]) = registerSuiteFixture(
-    ResourceSuiteLocalFixture(name, resource)
-  )
-
   // allow flaky tests on ci
-  override def munitFlakyOK = sys.env.contains("CI")
+  override def munitFlakyOK: Boolean = sys.env.contains("CI")
 }
diff --git a/tests/shared/src/main/scala/org/http4s/Http4sSuite.scala b/tests/shared/src/main/scala/org/http4s/Http4sSuite.scala
index 5cce9d896b..2ea7d3823b 100644
--- a/tests/shared/src/main/scala/org/http4s/Http4sSuite.scala
+++ b/tests/shared/src/main/scala/org/http4s/Http4sSuite.scala
@@ -22,7 +22,7 @@ import cats.syntax.all._
 import fs2._
 import fs2.text.utf8
 import munit._
-import org.scalacheck.Prop
+import munit.catseffect._
 
 /** Common stack for http4s' munit based tests
   */
@@ -32,23 +32,17 @@ trait Http4sSuite
     with munit.ScalaCheckEffectSuite
     with Http4sSuitePlatform {
 
-  private[this] val suiteFixtures = List.newBuilder[Fixture[_]]
+  private[this] val suiteFixtures = List.newBuilder[IOFixture[_]]
 
-  override def munitFixtures: Seq[Fixture[_]] = suiteFixtures.result()
+  override def munitFixtures: Seq[IOFixture[_]] = suiteFixtures.result()
 
-  // Override to remove implicit modifier
-  override def unitToProp: Unit => Prop = super.unitToProp
-
-  // Scala 3 likes this better
-  implicit def saneUnitToProp(unit: Unit): Prop = unitToProp(unit)
-
-  def registerSuiteFixture[A](fixture: Fixture[A]): Fixture[A] = {
+  def registerSuiteFixture[A](fixture: IOFixture[A]): IOFixture[A] = {
     suiteFixtures += fixture
     fixture
   }
 
-  def resourceSuiteDeferredFixture[A](name: String, resource: Resource[IO, A]): Fixture[IO[A]] =
-    registerSuiteFixture(UnsafeResourceSuiteLocalDeferredFixture(name, resource))
+  def resourceSuiteFixture[A](name: String, resource: Resource[IO, A]): IOFixture[A] =
+    registerSuiteFixture(ResourceSuiteLocalFixture(name, resource))
 
   implicit class ParseResultSyntax[A](self: ParseResult[A]) {
     def yolo: A = self.valueOr(e => sys.error(e.toString))
diff --git a/tests/shared/src/main/scala/org/http4s/testing/DispatcherIOFixture.scala b/tests/shared/src/main/scala/org/http4s/testing/DispatcherIOFixture.scala
index 6958418f42..be07923f1b 100644
--- a/tests/shared/src/main/scala/org/http4s/testing/DispatcherIOFixture.scala
+++ b/tests/shared/src/main/scala/org/http4s/testing/DispatcherIOFixture.scala
@@ -23,6 +23,6 @@ import munit.CatsEffectSuite
 
 trait DispatcherIOFixture { this: CatsEffectSuite =>
 
-  def dispatcher: SyncIO[FunFixture[Dispatcher[IO]]] = ResourceFixture(Dispatcher[IO])
+  def dispatcher: SyncIO[FunFixture[Dispatcher[IO]]] = ResourceFunFixture(Dispatcher[IO])
 
 }
diff --git a/tests/shared/src/test/scala/org/http4s/UriSpec.scala b/tests/shared/src/test/scala/org/http4s/UriSpec.scala
index 01e2bb3446..650fda9614 100644
--- a/tests/shared/src/test/scala/org/http4s/UriSpec.scala
+++ b/tests/shared/src/test/scala/org/http4s/UriSpec.scala
@@ -37,7 +37,6 @@ import org.scalacheck.Gen
 import org.scalacheck.Prop._
 import org.typelevel.ci._
 
-import java.nio.file.Paths
 import scala.collection.immutable.Seq
 
 // TODO: this needs some more filling out
@@ -1165,8 +1164,8 @@ class UriSpec extends Http4sSuite {
     forAll(pathGen) { (input: String) =>
       val prefix = "/this/isa/prefix/"
       val processed = Uri.removeDotSegments(Uri.Path.unsafeFromString(input)).renderString
-      val path = Paths.get(prefix, processed).normalize
-      assert(path.startsWith(Paths.get(prefix)))
+      val path = (fs2.io.file.Path(prefix) / processed).normalize
+      assert(path.startsWith(fs2.io.file.Path(prefix)))
       assert(!processed.contains("./"))
       assert(!processed.contains("../"))
     }