Repository: http4s/http4s
Pull request #4381 (2021-02-08 09:44:58)

merge 0.22 to main
that one was painful...


Patch:
diff --git a/blaze-server/src/main/scala/org/http4s/server/blaze/SSLContextFactory.scala b/blaze-server/src/main/scala/org/http4s/server/blaze/SSLContextFactory.scala
index 9d63f4a2d4..55f72c4ed1 100644
--- a/blaze-server/src/main/scala/org/http4s/server/blaze/SSLContextFactory.scala
+++ b/blaze-server/src/main/scala/org/http4s/server/blaze/SSLContextFactory.scala
@@ -39,7 +39,7 @@ private[blaze] object SSLContextFactory {
         val stream = new ByteArrayInputStream(certificate.getEncoded)
         cf.generateCertificate(stream).asInstanceOf[X509Certificate]
       }
-    }.toOption.getOrElse(Array.empty).toList
+    }.toOption.getOrElse(Array.empty[X509Certificate]).toList
 
   /** Given the name of a TLS/SSL cipher suite, return an int representing it effective stream
     * cipher key strength. i.e. How much entropy material is in the key material being fed into the
diff --git a/project/Http4sPlugin.scala b/project/Http4sPlugin.scala
index 7625436a87..4f45390acb 100644
--- a/project/Http4sPlugin.scala
+++ b/project/Http4sPlugin.scala
@@ -284,7 +284,7 @@ object Http4sPlugin extends AutoPlugin {
     // error-prone merge conflicts in the dependencies below.
     val argonaut = "6.3.3"
     val asyncHttpClient = "2.12.2"
-    val blaze = "0.14.15"
+    val blaze = "0.15.0-M1"
     val boopickle = "1.3.3"
     val caseInsensitive = "1.0.0-RC2"
     val cats = "2.3.1"


Test patch:
diff --git a/blaze-client/src/test/scala/org/http4s/client/blaze/ClientTimeoutSuite.scala b/blaze-client/src/test/scala/org/http4s/client/blaze/ClientTimeoutSuite.scala
index e5abc895ae..cffbdb48a3 100644
--- a/blaze-client/src/test/scala/org/http4s/client/blaze/ClientTimeoutSuite.scala
+++ b/blaze-client/src/test/scala/org/http4s/client/blaze/ClientTimeoutSuite.scala
@@ -29,6 +29,7 @@ import java.nio.charset.StandardCharsets
 import org.http4s.blaze.pipeline.HeadStage
 import org.http4s.blaze.util.TickWheelExecutor
 import org.http4s.blazecore.{QueueTestHead, SeqTestHead, SlowTestHead}
+import org.http4s.syntax.all._
 import org.http4s.testing.DispatcherIOFixture
 
 import scala.concurrent.TimeoutException
@@ -42,7 +43,7 @@ class ClientTimeoutSuite extends Http4sSuite with DispatcherIOFixture {
 
   def fixture = (tickWheelFixture, dispatcher).mapN(FunFixture.map2(_, _))
 
-  val www_foo_com = Uri.uri("http://www.foo.com")
+  val www_foo_com = uri"http://www.foo.com"
   val FooRequest = Request[IO](uri = www_foo_com)
   val FooRequestKey = RequestKey.fromRequest(FooRequest)
   val resp = "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndone"
diff --git a/blaze-client/src/test/scala/org/http4s/client/blaze/Http1ClientStageSuite.scala b/blaze-client/src/test/scala/org/http4s/client/blaze/Http1ClientStageSuite.scala
index 3630722cb2..e55a1d2f61 100644
--- a/blaze-client/src/test/scala/org/http4s/client/blaze/Http1ClientStageSuite.scala
+++ b/blaze-client/src/test/scala/org/http4s/client/blaze/Http1ClientStageSuite.scala
@@ -29,6 +29,7 @@ import org.http4s.blaze.pipeline.LeafBuilder
 import org.http4s.blazecore.{QueueTestHead, SeqTestHead}
 import org.http4s.client.blaze.bits.DefaultUserAgent
 import org.http4s.headers.`User-Agent`
+import org.http4s.syntax.all._
 import org.http4s.testing.DispatcherIOFixture
 import org.typelevel.ci.CIString
 import scala.concurrent.duration._
@@ -37,7 +38,7 @@ class Http1ClientStageSuite extends Http4sSuite with DispatcherIOFixture {
 
   val trampoline = org.http4s.blaze.util.Execution.trampoline
 
-  val www_foo_test = Uri.uri("http://www.foo.test")
+  val www_foo_test = uri"http://www.foo.test"
   val FooRequest = Request[IO](uri = www_foo_test)
   val FooRequestKey = RequestKey.fromRequest(FooRequest)
 
diff --git a/blaze-core/src/test/scala/org/http4s/blazecore/util/Http1WriterSpec.scala b/blaze-core/src/test/scala/org/http4s/blazecore/util/Http1WriterSpec.scala
index a86e5274d1..03adb11b75 100644
--- a/blaze-core/src/test/scala/org/http4s/blazecore/util/Http1WriterSpec.scala
+++ b/blaze-core/src/test/scala/org/http4s/blazecore/util/Http1WriterSpec.scala
@@ -27,14 +27,14 @@ import fs2.compression.{DeflateParams, deflate}
 import java.nio.ByteBuffer
 import java.nio.charset.StandardCharsets
 import cats.effect.std.Dispatcher
-import cats.effect.testing.specs2.CatsEffect
 import org.http4s.blaze.pipeline.{LeafBuilder, TailStage}
 import org.http4s.util.StringWriter
+import org.http4s.testing.DispatcherIOFixture
 
 import scala.concurrent.Future
 import scala.concurrent.ExecutionContext.Implicits._
 
-class Http1WriterSpec extends Http4sSpec with CatsEffect {
+class Http1WriterSpec extends Http4sSuite with DispatcherIOFixture {
   case object Failed extends RuntimeException
 
   final def writeEntityBody(p: EntityBody[IO])(
@@ -63,86 +63,83 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
   val messageBuffer = Chunk.array(message.getBytes(StandardCharsets.ISO_8859_1))
 
   final def runNonChunkedTests(
-      builder: Dispatcher[IO] => TailStage[ByteBuffer] => Http1Writer[IO]) =
-    withResource(Dispatcher[IO]) { implicit dispatcher =>
-      "Write a single emit" in {
-        writeEntityBody(chunk(messageBuffer))(builder(dispatcher))
-          .map(_ must_== "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
-      }
+      name: String,
+      builder: Dispatcher[IO] => TailStage[ByteBuffer] => Http1Writer[IO]) = {
+    dispatcher.test(s"$name Write a single emit") { implicit dispatcher =>
+      writeEntityBody(chunk(messageBuffer))(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
+    }
 
-      "Write two emits" in {
-        val p = chunk(messageBuffer) ++ chunk(messageBuffer)
-        writeEntityBody(p.covary[IO])(builder(dispatcher))
-          .map(
-            _ must_== "Content-Type: text/plain\r\nContent-Length: 24\r\n\r\n" + message + message)
-      }
+    dispatcher.test(s"$name Write two emits") { implicit dispatcher =>
+      val p = chunk(messageBuffer) ++ chunk(messageBuffer)
+      writeEntityBody(p.covary[IO])(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 24\r\n\r\n" + message + message)
+    }
 
-      "Write an await" in {
-        val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
-        writeEntityBody(p)(builder(dispatcher))
-          .map(_ must_== "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
-      }
+    dispatcher.test(s"$name Write an await") { implicit dispatcher =>
+      val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+      writeEntityBody(p)(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
+    }
 
-      "Write two awaits" in {
-        val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
-        writeEntityBody(p ++ p)(builder(dispatcher))
-          .map(
-            _ must_== "Content-Type: text/plain\r\nContent-Length: 24\r\n\r\n" + message + message)
-      }
+    dispatcher.test(s"$name Write two awaits") { implicit dispatcher =>
+      val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+      writeEntityBody(p ++ p)(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 24\r\n\r\n" + message + message)
+    }
 
-      "Write a body that fails and falls back" in {
-        val p = eval(IO.raiseError(Failed)).handleErrorWith { _ =>
-          chunk(messageBuffer)
-        }
-        writeEntityBody(p)(builder(dispatcher))
-          .map(_ must_== "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
+    dispatcher.test(s"$name Write a body that fails and falls back") { implicit dispatcher =>
+      val p = eval(IO.raiseError(Failed)).handleErrorWith { _ =>
+        chunk(messageBuffer)
       }
+      writeEntityBody(p)(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
+    }
 
-      "execute cleanup" in (for {
+    dispatcher.test(s"$name execute cleanup") { implicit dispatcher =>
+      (for {
         clean <- Ref.of[IO, Boolean](false)
         p = chunk(messageBuffer).covary[IO].onFinalizeWeak(clean.set(true))
-        _ <- writeEntityBody(p)(builder(dispatcher))
-          .map(_ must_== "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
-        _ <- clean.get.map(_ must beTrue)
-      } yield ok)
-
-      "Write tasks that repeat eval" in {
-        val t = {
-          var counter = 2
-          IO {
-            counter -= 1
-            if (counter >= 0) Some(Chunk.array("foo".getBytes(StandardCharsets.ISO_8859_1)))
-            else None
-          }
+        r <- writeEntityBody(p)(builder(dispatcher))
+          .map(_ == "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n" + message)
+        c <- clean.get
+      } yield r && c).assert
+    }
+
+    dispatcher.test(s"$name Write tasks that repeat eval") { implicit dispatcher =>
+      val t = {
+        var counter = 2
+        IO {
+          counter -= 1
+          if (counter >= 0) Some(Chunk.array("foo".getBytes(StandardCharsets.ISO_8859_1)))
+          else None
         }
-        val p = repeatEval(t).unNoneTerminate.flatMap(chunk(_).covary[IO]) ++ chunk(
-          Chunk.array("bar".getBytes(StandardCharsets.ISO_8859_1)))
-        writeEntityBody(p)(builder(dispatcher))
-          .map(_ must_== "Content-Type: text/plain\r\nContent-Length: 9\r\n\r\n" + "foofoobar")
       }
+      val p = repeatEval(t).unNoneTerminate.flatMap(chunk(_).covary[IO]) ++ chunk(
+        Chunk.array("bar".getBytes(StandardCharsets.ISO_8859_1)))
+      writeEntityBody(p)(builder(dispatcher))
+        .assertEquals("Content-Type: text/plain\r\nContent-Length: 9\r\n\r\n" + "foofoobar")
     }
+  }
 
-  "CachingChunkWriter" should {
-    runNonChunkedTests(implicit dispatcher =>
+  runNonChunkedTests(
+    "CachingChunkWriter",
+    implicit dispatcher =>
       tail => new CachingChunkWriter[IO](tail, IO.pure(Headers.empty), 1024 * 1024, false))
-  }
 
-  "CachingStaticWriter" should {
-    runNonChunkedTests(implicit dispatcher =>
+  runNonChunkedTests(
+    "CachingStaticWriter",
+    implicit dispatcher =>
       tail => new CachingChunkWriter[IO](tail, IO.pure(Headers.empty), 1024 * 1024, false))
-  }
 
-  "FlushingChunkWriter" should {
-    withResource(Dispatcher[IO]) { implicit dispatcher =>
-      def builder(tail: TailStage[ByteBuffer]): FlushingChunkWriter[IO] =
-        new FlushingChunkWriter[IO](tail, IO.pure(Headers.empty))
+  def builder(tail: TailStage[ByteBuffer])(implicit D: Dispatcher[IO]): FlushingChunkWriter[IO] =
+    new FlushingChunkWriter[IO](tail, IO.pure(Headers.empty))
 
-      "Write a strict chunk" in {
-        // n.b. in the scalaz-stream version, we could introspect the
-        // stream, note the lack of effects, and write this with a
-        // Content-Length header.  In fs2, this must be chunked.
-        writeEntityBody(chunk(messageBuffer))(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Write a strict chunk") { implicit d =>
+    // n.b. in the scalaz-stream version, we could introspect the
+    // stream, note the lack of effects, and write this with a
+    // Content-Length header.  In fs2, this must be chunked.
+    writeEntityBody(chunk(messageBuffer))(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -150,12 +147,11 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "Write two strict chunks" in {
-        val p = chunk(messageBuffer) ++ chunk(messageBuffer)
-        writeEntityBody(p.covary[IO])(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Write two strict chunks") { implicit d =>
+    val p = chunk(messageBuffer) ++ chunk(messageBuffer)
+    writeEntityBody(p.covary[IO])(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -165,15 +161,14 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "Write an effectful chunk" in {
-        // n.b. in the scalaz-stream version, we could introspect the
-        // stream, note the chunk was followed by halt, and write this
-        // with a Content-Length header.  In fs2, this must be chunked.
-        val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
-        writeEntityBody(p)(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Write an effectful chunk") { implicit d =>
+    // n.b. in the scalaz-stream version, we could introspect the
+    // stream, note the chunk was followed by halt, and write this
+    // with a Content-Length header.  In fs2, this must be chunked.
+    val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+    writeEntityBody(p)(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -181,12 +176,11 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "Write two effectful chunks" in {
-        val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
-        writeEntityBody(p ++ p)(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Write two effectful chunks") { implicit d =>
+    val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+    writeEntityBody(p ++ p)(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -196,14 +190,13 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "Elide empty chunks" in {
-        // n.b. We don't do anything special here.  This is a feature of
-        // fs2, but it's important enough we should check it here.
-        val p: Stream[IO, Byte] = chunk(Chunk.empty) ++ chunk(messageBuffer)
-        writeEntityBody(p.covary[IO])(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Elide empty chunks") { implicit d =>
+    // n.b. We don't do anything special here.  This is a feature of
+    // fs2, but it's important enough we should check it here.
+    val p: Stream[IO, Byte] = chunk(Chunk.empty) ++ chunk(messageBuffer)
+    writeEntityBody(p.covary[IO])(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -211,14 +204,14 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "Write a body that fails and falls back" in {
-        val p = eval(IO.raiseError(Failed)).handleErrorWith { _ =>
-          chunk(messageBuffer)
-        }
-        writeEntityBody(p)(builder).map(_ must_==
-          """Content-Type: text/plain
+  dispatcher.test("FlushingChunkWriter should Write a body that fails and falls back") {
+    implicit d =>
+      val p = eval(IO.raiseError(Failed)).handleErrorWith { _ =>
+        chunk(messageBuffer)
+      }
+      writeEntityBody(p)(builder).assertEquals("""Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
             |c
@@ -226,12 +219,14 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-      }
+  }
 
-      "execute cleanup" in (for {
-        clean <- Ref.of[IO, Boolean](false)
-        p = chunk(messageBuffer).onFinalizeWeak(clean.set(true))
-        _ <- writeEntityBody(p)(builder).map(_ must_==
+  dispatcher.test("FlushingChunkWriter should execute cleanup") { implicit d =>
+    (for {
+      clean <- Ref.of[IO, Boolean](false)
+      p = chunk(messageBuffer).onFinalizeWeak(clean.set(true))
+      w <- writeEntityBody(p)(builder).map(
+        _ ==
           """Content-Type: text/plain
             |Transfer-Encoding: chunked
             |
@@ -240,84 +235,91 @@ class Http1WriterSpec extends Http4sSpec with CatsEffect {
             |0
             |
             |""".stripMargin.replace("\n", "\r\n"))
-        _ <- clean.get.map(_ must beTrue)
-        _ <- clean.set(false)
-        p2 = eval(IO.raiseError(new RuntimeException("asdf"))).onFinalizeWeak(clean.set(true))
-        _ <- writeEntityBody(p2)(builder)
-        _ <- clean.get.map(_ must beTrue)
-      } yield ok)
-
-      // Some tests for the raw unwinding body without HTTP encoding.
-      "write a deflated stream" in {
-        val s = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
-        val p = s.through(deflate(DeflateParams.DEFAULT))
-        (
-          p.compile.toVector.map(_.toArray),
-          DumpingWriter.dump(s.through(deflate(DeflateParams.DEFAULT)))).mapN(_ === _)
-      }
+      c <- clean.get
+      _ <- clean.set(false)
+      p2 = eval(IO.raiseError(new RuntimeException("asdf"))).onFinalizeWeak(clean.set(true))
+      _ <- writeEntityBody(p2)(builder)
+      c2 <- clean.get
+    } yield w && c && c2).assert
+  }
 
-      val resource: Stream[IO, Byte] =
-        bracket(IO("foo"))(_ => IO.unit).flatMap { str =>
-          val it = str.iterator
-          emit {
-            if (it.hasNext) Some(it.next().toByte)
-            else None
-          }
-        }.unNoneTerminate
-
-      "write a resource" in {
-        val p = resource
-        (p.compile.toVector.map(_.toArray), DumpingWriter.dump(p)).mapN(_ === _)
-      }
+  // Some tests for the raw unwinding body without HTTP encoding.
+  test("FlushingChunkWriter should write a deflated stream") {
+    val s = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+    val p = s.through(deflate(DeflateParams.DEFAULT))
+    (
+      p.compile.toVector.map(_.toArray),
+      DumpingWriter.dump(s.through(deflate(DeflateParams.DEFAULT))))
+      .mapN(_ sameElements _)
+      .assert
+  }
 
-      "write a deflated resource" in {
-        val p = resource.through(deflate(DeflateParams.DEFAULT))
-        (
-          p.compile.toVector.map(_.toArray),
-          DumpingWriter.dump(resource.through(deflate(DeflateParams.DEFAULT))))
-          .mapN(_ === _)
+  val resource: Stream[IO, Byte] =
+    bracket(IO("foo"))(_ => IO.unit).flatMap { str =>
+      val it = str.iterator
+      emit {
+        if (it.hasNext) Some(it.next().toByte)
+        else None
       }
+    }.unNoneTerminate
 
-      "must be stack safe" in {
-        val p = repeatEval(IO.pure[Byte](0.toByte)).take(300000)
+  test("FlushingChunkWriter should write a resource") {
+    val p = resource
+    (p.compile.toVector.map(_.toArray), DumpingWriter.dump(p)).mapN(_ sameElements _).assert
+  }
 
-        // The dumping writer is stack safe when using a trampolining EC
-        (new DumpingWriter).writeEntityBody(p).attempt.map(_ must beRight)
-      }
+  test("FlushingChunkWriter should write a deflated resource") {
+    val p = resource.through(deflate(DeflateParams.DEFAULT))
+    (
+      p.compile.toVector.map(_.toArray),
+      DumpingWriter.dump(resource.through(deflate(DeflateParams.DEFAULT))))
+      .mapN(_ sameElements _)
+      .assert
+  }
 
-      "Execute cleanup on a failing Http1Writer" in (for {
-        clean <- Ref.of[IO, Boolean](false)
-        p = chunk(messageBuffer).onFinalizeWeak(clean.set(true))
-        _ <- new FailingWriter().writeEntityBody(p).attempt.map(_ must beLeft)
-        _ <- clean.get.map(_ must_== true)
-      } yield ok)
+  test("FlushingChunkWriter should must be stack safe") {
+    val p = repeatEval(IO.pure[Byte](0.toByte)).take(300000)
 
-      "Execute cleanup on a failing Http1Writer with a failing process" in (for {
-        clean <- Ref.of[IO, Boolean](false)
-        p = eval(IO.raiseError(Failed)).onFinalizeWeak(clean.set(true))
-        _ <- new FailingWriter().writeEntityBody(p).attempt.map(_ must beLeft)
-        _ <- clean.get.map(_ must_== true)
-      } yield ok)
+    // The dumping writer is stack safe when using a trampolining EC
+    (new DumpingWriter).writeEntityBody(p).attempt.map(_.isRight).assert
+  }
 
-      "Write trailer headers" in {
-        def builderWithTrailer(tail: TailStage[ByteBuffer]): FlushingChunkWriter[IO] =
-          new FlushingChunkWriter[IO](
-            tail,
-            IO.pure(Headers.of(Header("X-Trailer", "trailer header value"))))
+  test("FlushingChunkWriter should Execute cleanup on a failing Http1Writer") {
+    (for {
+      clean <- Ref.of[IO, Boolean](false)
+      p = chunk(messageBuffer).onFinalizeWeak(clean.set(true))
+      w <- new FailingWriter().writeEntityBody(p).attempt
+      c <- clean.get
+    } yield w.isLeft && c).assert
+  }
 
-        val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+  test(
+    "FlushingChunkWriter should Execute cleanup on a failing Http1Writer with a failing process") {
+    (for {
+      clean <- Ref.of[IO, Boolean](false)
+      p = eval(IO.raiseError(Failed)).onFinalizeWeak(clean.set(true))
+      w <- new FailingWriter().writeEntityBody(p).attempt
+      c <- clean.get
+    } yield w.isLeft && c).assert
+  }
 
-        writeEntityBody(p)(builderWithTrailer).map(_ must_===
-          """Content-Type: text/plain
-            |Transfer-Encoding: chunked
-            |
-            |c
-            |Hello world!
-            |0
-            |X-Trailer: trailer header value
-            |
-            |""".stripMargin.replace("\n", "\r\n"))
-      }
-    }
+  dispatcher.test("FlushingChunkWriter should Write trailer headers") { implicit d =>
+    def builderWithTrailer(tail: TailStage[ByteBuffer]): FlushingChunkWriter[IO] =
+      new FlushingChunkWriter[IO](
+        tail,
+        IO.pure(Headers.of(Header("X-Trailer", "trailer header value"))))
+
+    val p = eval(IO(messageBuffer)).flatMap(chunk(_).covary[IO])
+
+    writeEntityBody(p)(builderWithTrailer).assertEquals("""Content-Type: text/plain
+          |Transfer-Encoding: chunked
+          |
+          |c
+          |Hello world!
+          |0
+          |X-Trailer: trailer header value
+          |
+          |""".stripMargin.replace("\n", "\r\n"))
   }
+
 }
diff --git a/blaze-core/src/test/scala/org/http4s/blazecore/websocket/Http4sWSStageSpec.scala b/blaze-core/src/test/scala/org/http4s/blazecore/websocket/Http4sWSStageSpec.scala
index 25e5e83d4b..dce4fb4386 100644
--- a/blaze-core/src/test/scala/org/http4s/blazecore/websocket/Http4sWSStageSpec.scala
+++ b/blaze-core/src/test/scala/org/http4s/blazecore/websocket/Http4sWSStageSpec.scala
@@ -22,21 +22,20 @@ import fs2.concurrent.SignallingRef
 import cats.effect.IO
 import cats.syntax.all._
 import cats.effect.std.{Dispatcher, Queue}
-import cats.effect.testing.specs2.CatsEffect
 import java.util.concurrent.atomic.AtomicBoolean
-import org.http4s.Http4sSpec
+import org.http4s.Http4sSuite
 import org.http4s.blaze.pipeline.LeafBuilder
 import org.http4s.websocket.{WebSocketFrame, WebSocketSeparatePipe}
 import org.http4s.websocket.WebSocketFrame._
 import org.http4s.blaze.pipeline.Command
+import org.http4s.testing.DispatcherIOFixture
 
 import scala.concurrent.ExecutionContext
 import scala.concurrent.duration._
 import scodec.bits.ByteVector
 
-class Http4sWSStageSpec extends Http4sSpec with CatsEffect {
-  implicit def testExecutionContext: ExecutionContext =
-    ExecutionContext.global
+class Http4sWSStageSpec extends Http4sSuite with DispatcherIOFixture {
+  implicit val testExecutionContext: ExecutionContext = munitExecutionContext
 
   class TestWebsocketStage(
       outQ: Queue[IO, WebSocketFrame],
@@ -89,81 +88,95 @@ class Http4sWSStageSpec extends Http4sSpec with CatsEffect {
       } yield new TestWebsocketStage(outQ, head, closeHook, backendInQ)
   }
 
-  "Http4sWSStage" should {
-    withResource(Dispatcher[IO]) { implicit dispatcher =>
-      "reply with pong immediately after ping" in (for {
-        socket <- TestWebsocketStage()
-        _ <- socket.sendInbound(Ping())
-        _ <- socket.pollOutbound(2).map(_ must beSome[WebSocketFrame](Pong()))
-        _ <- socket.sendInbound(Close())
-      } yield ok)
+  dispatcher.test("Http4sWSStage should reply with pong immediately after ping") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      _ <- socket.sendInbound(Ping())
+      p <- socket.pollOutbound(2).map(_.exists(_ == Pong()))
+      _ <- socket.sendInbound(Close())
+    } yield assert(p)
+  }
 
-      "not write any more frames after close frame sent" in (for {
+  dispatcher.test("Http4sWSStage should not write any more frames after close frame sent") {
+    implicit d =>
+      for {
         socket <- TestWebsocketStage()
         _ <- socket.sendWSOutbound(Text("hi"), Close(), Text("lol"))
-        _ <- socket.pollOutbound().map(_ must_=== Some(Text("hi")))
-        _ <- socket.pollOutbound().map(_ must_=== Some(Close()))
-        _ <- socket.pollOutbound().map(_ must_=== None)
+        p1 <- socket.pollOutbound().map(_.contains(Text("hi")))
+        p2 <- socket.pollOutbound().map(_.contains(Close()))
+        p3 <- socket.pollOutbound().map(_.isEmpty)
         _ <- socket.sendInbound(Close())
-      } yield ok)
+      } yield assert(p1 && p2 && p3)
+  }
 
-      "send a close frame back and call the on close handler upon receiving a close frame" in (for {
+  dispatcher.test(
+    "Http4sWSStage should send a close frame back and call the on close handler upon receiving a close frame") {
+    implicit d =>
+      for {
         socket <- TestWebsocketStage()
         _ <- socket.sendInbound(Close())
-        _ <- socket.pollBatchOutputbound(2, 2).map(_ must_=== List(Close()))
-        _ <- socket.wasCloseHookCalled().map(_ must_=== true)
-      } yield ok)
+        p1 <- socket.pollBatchOutputbound(2, 2).map(_ == List(Close()))
+        p2 <- socket.wasCloseHookCalled().map(_ == true)
+      } yield assert(p1 && p2)
+  }
 
-      "not send two close frames " in (for {
-        socket <- TestWebsocketStage()
-        _ <- socket.sendWSOutbound(Close())
-        _ <- socket.sendInbound(Close())
-        _ <- socket.pollBatchOutputbound(2).map(_ must_=== List(Close()))
-        _ <- socket.wasCloseHookCalled().map(_ must_=== true)
-      } yield ok)
+  dispatcher.test("Http4sWSStage should not send two close frames ") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      _ <- socket.sendWSOutbound(Close())
+      _ <- socket.sendInbound(Close())
+      p1 <- socket.pollBatchOutputbound(2).map(_ == List(Close()))
+      p2 <- socket.wasCloseHookCalled()
+    } yield assert(p1 && p2)
+  }
 
-      "ignore pong frames" in (for {
-        socket <- TestWebsocketStage()
-        _ <- socket.sendInbound(Pong())
-        _ <- socket.pollOutbound().map(_ must_=== None)
-        _ <- socket.sendInbound(Close())
-      } yield ok)
+  dispatcher.test("Http4sWSStage should ignore pong frames") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      _ <- socket.sendInbound(Pong())
+      p <- socket.pollOutbound().map(_.isEmpty)
+      _ <- socket.sendInbound(Close())
+    } yield assert(p)
+  }
 
-      "send a ping frames to backend" in (for {
-        socket <- TestWebsocketStage()
-        _ <- socket.sendInbound(Ping())
-        _ <- socket.pollBackendInbound().map(_ must_=== Some(Ping()))
-        pingWithBytes = Ping(ByteVector(Array[Byte](1, 2, 3)))
-        _ <- socket.sendInbound(pingWithBytes)
-        _ <- socket.pollBackendInbound().map(_ must_=== Some(pingWithBytes))
-        _ <- socket.sendInbound(Close())
-      } yield ok)
+  dispatcher.test("Http4sWSStage should send a ping frames to backend") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      _ <- socket.sendInbound(Ping())
+      p1 <- socket.pollBackendInbound().map(_.contains(Ping()))
+      pingWithBytes = Ping(ByteVector(Array[Byte](1, 2, 3)))
+      _ <- socket.sendInbound(pingWithBytes)
+      p2 <- socket.pollBackendInbound().map(_.contains(pingWithBytes))
+      _ <- socket.sendInbound(Close())
+    } yield assert(p1 && p2)
+  }
 
-      "send a pong frames to backend" in (for {
-        socket <- TestWebsocketStage()
-        _ <- socket.sendInbound(Pong())
-        _ <- socket.pollBackendInbound().map(_ must_=== Some(Pong()))
-        pongWithBytes = Pong(ByteVector(Array[Byte](1, 2, 3)))
-        _ <- socket.sendInbound(pongWithBytes)
-        _ <- socket.pollBackendInbound().map(_ must_=== Some(pongWithBytes))
-        _ <- socket.sendInbound(Close())
-      } yield ok)
+  dispatcher.test("Http4sWSStage should send a pong frames to backend") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      _ <- socket.sendInbound(Pong())
+      p1 <- socket.pollBackendInbound().map(_.contains(Pong()))
+      pongWithBytes = Pong(ByteVector(Array[Byte](1, 2, 3)))
+      _ <- socket.sendInbound(pongWithBytes)
+      p2 <- socket.pollBackendInbound().map(_.contains(pongWithBytes))
+      _ <- socket.sendInbound(Close())
+    } yield assert(p1 && p2)
+  }
 
-      "not fail on pending write request" in (for {
-        socket <- TestWebsocketStage()
-        reasonSent = ByteVector(42)
-        in = Stream.eval(socket.sendInbound(Ping())).repeat.take(100)
-        out = Stream.eval(socket.sendWSOutbound(Text("."))).repeat.take(200)
-        _ <- in.merge(out).compile.drain
-        _ <- socket.sendInbound(Close(reasonSent))
-        reasonReceived <-
-          socket.outStream
-            .collectFirst { case Close(reasonReceived) => reasonReceived }
-            .compile
-            .toList
-            .timeout(5.seconds)
-        _ = reasonReceived must_== (List(reasonSent))
-      } yield ok)
-    }
+  dispatcher.test("Http4sWSStage should not fail on pending write request") { implicit d =>
+    for {
+      socket <- TestWebsocketStage()
+      reasonSent = ByteVector(42)
+      in = Stream.eval(socket.sendInbound(Ping())).repeat.take(100)
+      out = Stream.eval(socket.sendWSOutbound(Text("."))).repeat.take(200)
+      _ <- in.merge(out).compile.drain
+      _ <- socket.sendInbound(Close(reasonSent))
+      reasonReceived <-
+        socket.outStream
+          .collectFirst { case Close(reasonReceived) => reasonReceived }
+          .compile
+          .toList
+          .timeout(5.seconds)
+    } yield assert(reasonReceived == List(reasonSent))
   }
 }
diff --git a/blaze-server/src/test/scala/org/http4s/server/blaze/BlazeServerMtlsSpec.scala b/blaze-server/src/test/scala/org/http4s/server/blaze/BlazeServerMtlsSpec.scala
index 57e94e8568..4cd2788b33 100644
--- a/blaze-server/src/test/scala/org/http4s/server/blaze/BlazeServerMtlsSpec.scala
+++ b/blaze-server/src/test/scala/org/http4s/server/blaze/BlazeServerMtlsSpec.scala
@@ -24,16 +24,16 @@ import java.security.KeyStore
 import javax.net.ssl._
 import org.http4s.dsl.io._
 import org.http4s.server.{Server, ServerRequestKeys}
-import org.http4s.{Http4sSpec, HttpApp}
+import org.http4s.testing.ErrorReporting
+import org.http4s.{Http4sSuite, HttpApp}
 import scala.concurrent.duration._
 import scala.io.Source
 import scala.util.Try
 import scala.concurrent.ExecutionContext.global
-import org.http4s.testing.SilenceOutputStream
 
 /** Test cases for mTLS support in blaze server
   */
-class BlazeServerMtlsSpec extends Http4sSpec with SilenceOutputStream {
+class BlazeServerMtlsSpec extends Http4sSuite {
   {
     val hostnameVerifier: HostnameVerifier = new HostnameVerifier {
       override def verify(s: String, sslSession: SSLSession): Boolean = true
@@ -53,9 +53,9 @@ class BlazeServerMtlsSpec extends Http4sSpec with SilenceOutputStream {
         .lookup(ServerRequestKeys.SecureSession)
         .flatten
         .map { session =>
-          session.sslSessionId shouldNotEqual ""
-          session.cipherSuite shouldNotEqual ""
-          session.keySize shouldNotEqual 0
+          assert(session.sslSessionId != "")
+          assert(session.cipherSuite != "")
+          assert(session.keySize != 0)
 
           session.X509Certificate.head.getSubjectX500Principal.getName
         }
@@ -68,10 +68,10 @@ class BlazeServerMtlsSpec extends Http4sSpec with SilenceOutputStream {
         .lookup(ServerRequestKeys.SecureSession)
         .flatten
         .foreach { session =>
-          session.sslSessionId shouldNotEqual ""
-          session.cipherSuite shouldNotEqual ""
-          session.keySize shouldNotEqual 0
-          session.X509Certificate.size shouldEqual 0
+          assert(session.sslSessionId != "")
+          assert(session.cipherSuite != "")
+          assert(session.keySize != 0)
+          assert(session.X509Certificate.isEmpty)
         }
 
       Ok("success")
@@ -120,10 +120,8 @@ class BlazeServerMtlsSpec extends Http4sSpec with SilenceOutputStream {
     sc
   }
 
-  /** Test "required" auth mode
-    */
-  withResource(serverR(TLSParameters(needClientAuth = true).toSSLParameters)) { server =>
-    def get(path: String, clientAuth: Boolean = true): String = {
+  def get(server: Server, path: String, clientAuth: Boolean = true): String =
+    ErrorReporting.silenceOutputStreams {
       val url = new URL(s"https://localhost:${server.address.getPort}$path")
       val conn = url.openConnection().asInstanceOf[HttpsURLConnection]
       conn.setRequestMethod("GET")
@@ -141,46 +139,30 @@ class BlazeServerMtlsSpec extends Http4sSpec with SilenceOutputStream {
         .getOrElse("")
     }
 
-    "Server" should {
-      "send mTLS request correctly" in {
-        get("/dummy") shouldEqual "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US"
-      }
+  def blazeServer(sslParameters: SSLParameters) =
+    ResourceFixture(serverR(sslParameters))
 
-      "fail for invalid client auth" in {
-        get("/dummy", clientAuth = false) shouldNotEqual "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US"
-      }
+  /** Test "required" auth mode
+    */
+  blazeServer(TLSParameters(needClientAuth = true).toSSLParameters)
+    .test("Server should send mTLS request correctly") { server =>
+      assertEquals(get(server, "/dummy", true), "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US")
+    }
+  blazeServer(TLSParameters(needClientAuth = true).toSSLParameters)
+    .test("Server should fail for invalid client auth") { server =>
+      assertNotEquals(get(server, "/dummy", false), "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US")
     }
-  }
 
   /** Test "requested" auth mode
     */
-  withResource(serverR(TLSParameters(wantClientAuth = true).toSSLParameters)) { server =>
-    def get(path: String, clientAuth: Boolean = true): String = {
-      val url = new URL(s"https://localhost:${server.address.getPort}$path")
-      val conn = url.openConnection().asInstanceOf[HttpsURLConnection]
-      conn.setRequestMethod("GET")
-
-      if (clientAuth)
-        conn.setSSLSocketFactory(sslContext.getSocketFactory)
-      else
-        conn.setSSLSocketFactory(noAuthClientContext.getSocketFactory)
-
-      Try {
-        Source.fromInputStream(conn.getInputStream, StandardCharsets.UTF_8.name).getLines().mkString
-      }.recover { case ex: Throwable =>
-        ex.getMessage
-      }.toOption
-        .getOrElse("")
+  blazeServer(TLSParameters(wantClientAuth = true).toSSLParameters)
+    .test("Server should send mTLS request correctly with optional auth") { server =>
+      assertEquals(get(server, "/dummy", true), "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US")
     }
 
-    "Server" should {
-      "send mTLS request correctly with optional auth" in {
-        get("/dummy") shouldEqual "CN=Test,OU=Test,O=Test,L=CA,ST=CA,C=US"
-      }
-
-      "send mTLS request correctly without clientAuth" in {
-        get("/noauth", clientAuth = false) shouldEqual "success"
-      }
+  blazeServer(TLSParameters(wantClientAuth = true).toSSLParameters)
+    .test("Server should send mTLS request correctly without clientAuth") { server =>
+      assertEquals(get(server, "/noauth", false), "success")
     }
-  }
+
 }
diff --git a/blaze-server/src/test/scala/org/http4s/server/blaze/Http1ServerStageSpec.scala b/blaze-server/src/test/scala/org/http4s/server/blaze/Http1ServerStageSpec.scala
index 4dec600ea7..eeb3d013f1 100644
--- a/blaze-server/src/test/scala/org/http4s/server/blaze/Http1ServerStageSpec.scala
+++ b/blaze-server/src/test/scala/org/http4s/server/blaze/Http1ServerStageSpec.scala
@@ -19,6 +19,7 @@ package server
 package blaze
 
 import cats.data.Kleisli
+import cats.syntax.eq._
 import cats.effect._
 import cats.effect.kernel.Deferred
 import cats.effect.std.Dispatcher
@@ -31,29 +32,33 @@ import org.http4s.blaze.util.TickWheelExecutor
 import org.http4s.blazecore.{ResponseParser, SeqTestHead}
 import org.http4s.dsl.io._
 import org.http4s.headers.{Date, `Content-Length`, `Transfer-Encoding`}
+import org.http4s.syntax.all._
 import org.http4s.testing.ErrorReporting._
-import org.specs2.specification.AfterAll
-import org.specs2.specification.core.Fragment
 import org.typelevel.ci.CIString
 import org.typelevel.vault._
 import scala.concurrent.duration._
-import scala.concurrent.Await
-import scala.concurrent.ExecutionContext
 
-class Http1ServerStageSpec extends Http4sSpec with AfterAll {
-  sequential
-
-  implicit val ec: ExecutionContext = Http4sSpec.TestExecutionContext
-
-  val tickWheel = new TickWheelExecutor()
-
-  val dispatcherAndShutdown = Dispatcher[IO].allocated.unsafeRunSync()
-  val dispatcher = dispatcherAndShutdown._1
-
-  def afterAll() = {
-    tickWheel.shutdown()
-    dispatcherAndShutdown._2.unsafeRunSync()
+class Http1ServerStageSpec extends Http4sSuite {
+  implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
+  val fixture = ResourceFixture(Resource.make(IO.delay(new TickWheelExecutor())) { twe =>
+    IO.delay(twe.shutdown())
+  })
+
+  // todo replace with DispatcherIOFixture
+  val dispatcher = new Fixture[Dispatcher[IO]]("dispatcher") {
+
+    private var d: Dispatcher[IO] = null
+    private var shutdown: IO[Unit] = null
+    def apply() = d
+    override def beforeAll(): Unit = {
+      val dispatcherAndShutdown = Dispatcher[IO].allocated.unsafeRunSync()
+      shutdown = dispatcherAndShutdown._2
+      d = dispatcherAndShutdown._1
+    }
+    override def afterAll(): Unit =
+      shutdown.unsafeRunSync()
   }
+  override def munitFixtures = List(dispatcher)
 
   def makeString(b: ByteBuffer): String = {
     val p = b.position()
@@ -71,6 +76,7 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
   }
 
   def runRequest(
+      tw: TickWheelExecutor,
       req: Seq[String],
       httpApp: HttpApp[IO],
       maxReqLine: Int = 4 * 1024,
@@ -80,7 +86,7 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
     val httpStage = Http1ServerStage[IO](
       httpApp,
       () => Vault.empty,
-      ec,
+      munitExecutionContext,
       enableWebSockets = true,
       maxReqLine,
       maxHeaders,
@@ -88,8 +94,8 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       silentErrorHandler,
       30.seconds,
       30.seconds,
-      tickWheel,
-      dispatcher
+      tw,
+      dispatcher()
     )
 
     pipeline.LeafBuilder(httpStage).base(head)
@@ -97,129 +103,138 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
     head
   }
 
-  "Http1ServerStage: Invalid Lengths" should {
-    val req = "GET /foo HTTP/1.1\r\nheader: value\r\n\r\n"
+  val req = "GET /foo HTTP/1.1\r\nheader: value\r\n\r\n"
 
-    val routes = HttpRoutes
-      .of[IO] { case _ =>
-        Ok("foo!")
-      }
-      .orNotFound
-
-    "fail on too long of a request line" in {
-      val buff = Await.result(runRequest(Seq(req), routes, maxReqLine = 1).result, 5.seconds)
-      val str = StandardCharsets.ISO_8859_1.decode(buff.duplicate()).toString
-      // make sure we don't have signs of chunked encoding.
-      str.contains("400 Bad Request") must_== true
-    }
-
-    "fail on too long of a header" in {
-      val buff = Await.result(runRequest(Seq(req), routes, maxHeaders = 1).result, 5.seconds)
-      val str = StandardCharsets.ISO_8859_1.decode(buff.duplicate()).toString
-      // make sure we don't have signs of chunked encoding.
-      str.contains("400 Bad Request") must_== true
+  val routes = HttpRoutes
+    .of[IO] { case _ =>
+      Ok("foo!")
     }
+    .orNotFound
+
+  fixture.test("Http1ServerStage: Invalid Lengths should fail on too long of a request line") {
+    tickwheel =>
+      runRequest(tickwheel, Seq(req), routes, maxReqLine = 1).result.map { buff =>
+        val str = StandardCharsets.ISO_8859_1.decode(buff.duplicate()).toString
+        // make sure we don't have signs of chunked encoding.
+        assert(str.contains("400 Bad Request"))
+      }
   }
 
-  "Http1ServerStage: Common responses" should {
-    Fragment.foreach(ServerTestRoutes.testRequestResults.zipWithIndex) {
-      case ((req, (status, headers, resp)), i) =>
-        if (i == 7 || i == 8) // Awful temporary hack
-          s"Run request $i Run request: --------\n${req.split("\r\n\r\n")(0)}\n" in {
-            val result = Await.result(runRequest(Seq(req), ServerTestRoutes()).result, 5.seconds)
-            parseAndDropDate(result) must_== ((status, headers, resp))
-          }
-        else
-          s"Run request $i Run request: --------\n${req.split("\r\n\r\n")(0)}\n" in {
-            val result = Await.result(runRequest(Seq(req), ServerTestRoutes()).result, 5.seconds)
-            parseAndDropDate(result) must_== ((status, headers, resp))
-          }
-    }
+  fixture.test("Http1ServerStage: Invalid Lengths should fail on too long of a header") {
+    tickwheel =>
+      (runRequest(tickwheel, Seq(req), routes, maxHeaders = 1).result).map { buff =>
+        val str = StandardCharsets.ISO_8859_1.decode(buff.duplicate()).toString
+        // make sure we don't have signs of chunked encoding.
+        assert(str.contains("400 Bad Request"))
+      }
   }
 
-  "Http1ServerStage: Errors" should {
-    val exceptionService = HttpRoutes
-      .of[IO] {
-        case GET -> Root / "sync" => sys.error("Synchronous error!")
-        case GET -> Root / "async" => IO.raiseError(new Exception("Asynchronous error!"))
-        case GET -> Root / "sync" / "422" =>
-          throw InvalidMessageBodyFailure("lol, I didn't even look")
-        case GET -> Root / "async" / "422" =>
-          IO.raiseError(InvalidMessageBodyFailure("lol, I didn't even look"))
-      }
-      .orNotFound
+  ServerTestRoutes.testRequestResults.zipWithIndex.foreach {
+    case ((req, (status, headers, resp)), i) =>
+      if (i == 7 || i == 8) // Awful temporary hack
+        fixture.test(
+          s"Http1ServerStage: Common responses should Run request $i Run request: --------\n${req
+            .split("\r\n\r\n")(0)}\n") { tw =>
+          runRequest(tw, Seq(req), ServerTestRoutes()).result
+            .map(parseAndDropDate)
+            .map(assertEquals(_, (status, headers, resp)))
+
+        }
+      else
+        fixture.test(
+          s"Http1ServerStage: Common responses should Run request $i Run request: --------\n${req
+            .split("\r\n\r\n")(0)}\n") { tw =>
+          runRequest(tw, Seq(req), ServerTestRoutes()).result
+            .map(parseAndDropDate)
+            .map(assertEquals(_, (status, headers, resp)))
 
-    def runError(path: String) =
-      runRequest(List(path), exceptionService).result
-        .map(parseAndDropDate)
-        .map { case (s, h, r) =>
-          val close = h.exists { h =>
-            h.toRaw.name == CIString("connection") && h.toRaw.value == "close"
-          }
-          (s, close, r)
         }
+  }
 
-    "Deal with synchronous errors" in {
-      val path = "GET /sync HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
-      val (s, c, _) = Await.result(runError(path), 10.seconds)
-      s must_== InternalServerError
-      c must_== true
+  val exceptionService = HttpRoutes
+    .of[IO] {
+      case GET -> Root / "sync" => sys.error("Synchronous error!")
+      case GET -> Root / "async" => IO.raiseError(new Exception("Asynchronous error!"))
+      case GET -> Root / "sync" / "422" =>
+        throw InvalidMessageBodyFailure("lol, I didn't even look")
+      case GET -> Root / "async" / "422" =>
+        IO.raiseError(InvalidMessageBodyFailure("lol, I didn't even look"))
     }
+    .orNotFound
+
+  def runError(tw: TickWheelExecutor, path: String) =
+    runRequest(tw, List(path), exceptionService).result
+      .map(parseAndDropDate)
+      .map { case (s, h, r) =>
+        val close = h.exists { h =>
+          h.toRaw.name == CIString("connection") && h.toRaw.value == "close"
+        }
+        (s, close, r)
+      }
 
-    "Call toHttpResponse on synchronous errors" in {
-      val path = "GET /sync/422 HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
-      val (s, c, _) = Await.result(runError(path), 10.seconds)
-      s must_== UnprocessableEntity
-      c must_== false
+  fixture.test("Http1ServerStage: Errors should Deal with synchronous errors") { tw =>
+    val path = "GET /sync HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
+    runError(tw, path).map { case (s, c, _) =>
+      assert(s == InternalServerError && c)
     }
+  }
 
-    "Deal with asynchronous errors" in {
-      val path = "GET /async HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
-      val (s, c, _) = Await.result(runError(path), 10.seconds)
-      s must_== InternalServerError
-      c must_== true
+  fixture.test("Http1ServerStage: Errors should Call toHttpResponse on synchronous errors") { tw =>
+    val path = "GET /sync/422 HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
+    runError(tw, path).map { case (s, c, _) =>
+      assert(s == UnprocessableEntity && !c)
     }
+  }
 
-    "Call toHttpResponse on asynchronous errors" in {
-      val path = "GET /async/422 HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
-      val (s, c, _) = Await.result(runError(path), 10.seconds)
-      s must_== UnprocessableEntity
-      c must_== false
+  fixture.test("Http1ServerStage: Errors should Deal with asynchronous errors") { tw =>
+    val path = "GET /async HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
+    runError(tw, path).map { case (s, c, _) =>
+      assert(s == InternalServerError && c)
     }
+  }
 
-    "Handle parse error" in {
-      val path = "THIS\u0000IS\u0000NOT\u0000HTTP"
-      val (s, c, _) = Await.result(runError(path), 10.seconds)
-      s must_== BadRequest
-      c must_== true
+  fixture.test("Http1ServerStage: Errors should Call toHttpResponse on asynchronous errors") { tw =>
+    val path = "GET /async/422 HTTP/1.1\r\nConnection:keep-alive\r\n\r\n"
+    runError(tw, path).map { case (s, c, _) =>
+      assert(s == UnprocessableEntity && !c)
     }
   }
 
-  "Http1ServerStage: routes" should {
-    "Do not send `Transfer-Encoding: identity` response" in {
-      val routes = HttpRoutes
-        .of[IO] { case _ =>
-          val headers = Headers.of(H.`Transfer-Encoding`(TransferCoding.identity))
-          IO.pure(Response[IO](headers = headers)
-            .withEntity("hello world"))
-        }
-        .orNotFound
+  fixture.test("Http1ServerStage: Errors should Handle parse error") { tw =>
+    val path = "THIS\u0000IS\u0000NOT\u0000HTTP"
+    runError(tw, path).map { case (s, c, _) =>
+      assert(s == BadRequest && c)
+    }
+  }
 
-      // The first request will get split into two chunks, leaving the last byte off
-      val req = "GET /foo HTTP/1.1\r\n\r\n"
+  fixture.test(
+    "Http1ServerStage: routes should Do not send `Transfer-Encoding: identity` response") { tw =>
+    val routes = HttpRoutes
+      .of[IO] { case _ =>
+        val headers = Headers.of(H.`Transfer-Encoding`(TransferCoding.identity))
+        IO.pure(
+          Response[IO](headers = headers)
+            .withEntity("hello world"))
+      }
+      .orNotFound
 
-      val buff = Await.result(runRequest(Seq(req), routes).result, 5.seconds)
+    // The first request will get split into two chunks, leaving the last byte off
+    val req = "GET /foo HTTP/1.1\r\n\r\n"
 
+    runRequest(tw, Seq(req), routes).result.map { buff =>
       val str = StandardCharsets.ISO_8859_1.decode(buff.duplicate()).toString
       // make sure we don't have signs of chunked encoding.
-      str.contains("0\r\n\r\n") must_== false
-      str.contains("hello world") must_== true
+      assert(!str.contains("0\r\n\r\n"))
+      assert(str.contains("hello world"))
 
       val (_, hdrs, _) = ResponseParser.apply(buff)
-      hdrs.find(_.name == `Transfer-Encoding`.name) must_== None
+      assert(!hdrs.exists(_.name == `Transfer-Encoding`.name))
     }
+  }
 
-    "Do not send an entity or entity-headers for a status that doesn't permit it" in {
+  fixture.test(
+    "Http1ServerStage: routes should Do not send an entity or entity-headers for a status that doesn't permit it") {
+    tw =>
       val routes: HttpApp[IO] = HttpRoutes
         .of[IO] { case _ =>
           IO.pure(
@@ -231,52 +246,55 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
 
       val req = "GET /foo HTTP/1.1\r\n\r\n"
 
-      val buf = Await.result(runRequest(Seq(req), routes).result, 5.seconds)
-      val (status, hs, body) = ResponseParser.parseBuffer(buf)
-
-      val hss = Headers(hs.toList)
-      `Content-Length`.from(hss).isDefined must_== false
-      body must_== ""
-      status must_== Status.NotModified
-    }
+      (runRequest(tw, Seq(req), routes).result).map { buf =>
+        val (status, hs, body) = ResponseParser.parseBuffer(buf)
 
-    "Add a date header" in {
-      val routes = HttpRoutes
-        .of[IO] { case req =>
-          IO.pure(Response(body = req.body))
-        }
-        .orNotFound
+        val hss = Headers(hs.toList)
+        assert(`Content-Length`.from(hss).isDefined == false)
+        assert(body == "")
+        assert(status == Status.NotModified)
+      }
+  }
 
-      // The first request will get split into two chunks, leaving the last byte off
-      val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
+  fixture.test("Http1ServerStage: routes should Add a date header") { tw =>
+    val routes = HttpRoutes
+      .of[IO] { case req =>
+        IO.pure(Response(body = req.body))
+      }
+      .orNotFound
 
-      val buff = Await.result(runRequest(Seq(req1), routes).result, 5.seconds)
+    // The first request will get split into two chunks, leaving the last byte off
+    val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
 
+    (runRequest(tw, Seq(req1), routes).result).map { buff =>
       // Both responses must succeed
       val (_, hdrs, _) = ResponseParser.apply(buff)
-      hdrs.find(_.name == Date.name) must beSome[Header]
+      assert(hdrs.exists(_.name == Date.name))
     }
+  }
 
-    "Honor an explicitly added date header" in {
-      val dateHeader = Date(HttpDate.Epoch)
-      val routes = HttpRoutes
-        .of[IO] { case req =>
-          IO.pure(Response(body = req.body).withHeaders(dateHeader))
-        }
-        .orNotFound
-
-      // The first request will get split into two chunks, leaving the last byte off
-      val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
+  fixture.test("Http1ServerStage: routes should Honor an explicitly added date header") { tw =>
+    val dateHeader = Date(HttpDate.Epoch)
+    val routes = HttpRoutes
+      .of[IO] { case req =>
+        IO.pure(Response(body = req.body).withHeaders(dateHeader))
+      }
+      .orNotFound
 
-      val buff = Await.result(runRequest(Seq(req1), routes).result, 5.seconds)
+    // The first request will get split into two chunks, leaving the last byte off
+    val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
 
+    (runRequest(tw, Seq(req1), routes).result).map { buff =>
       // Both responses must succeed
       val (_, hdrs, _) = ResponseParser.apply(buff)
 
-      hdrs.find(_.name == Date.name) must_== Some(dateHeader)
+      assert(hdrs.find(_.name == Date.name) == Some(dateHeader))
     }
+  }
 
-    "Handle routes that echos full request body for non-chunked" in {
+  fixture.test(
+    "Http1ServerStage: routes should Handle routes that echos full request body for non-chunked") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case req =>
           IO.pure(Response(body = req.body))
@@ -287,13 +305,15 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
       val (r11, r12) = req1.splitAt(req1.length - 1)
 
-      val buff = Await.result(runRequest(Seq(r11, r12), routes).result, 5.seconds)
-
-      // Both responses must succeed
-      parseAndDropDate(buff) must_== ((Ok, Set(H.`Content-Length`.unsafeFromLong(4)), "done"))
-    }
+      (runRequest(tw, Seq(r11, r12), routes).result).map { buff =>
+        // Both responses must succeed
+        assert(parseAndDropDate(buff) == ((Ok, Set(H.`Content-Length`.unsafeFromLong(4)), "done")))
+      }
+  }
 
-    "Handle routes that consumes the full request body for non-chunked" in {
+  fixture.test(
+    "Http1ServerStage: routes should Handle routes that consumes the full request body for non-chunked") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case req =>
           req.as[String].map { s =>
@@ -306,19 +326,22 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
       val (r11, r12) = req1.splitAt(req1.length - 1)
 
-      val buff = Await.result(runRequest(Seq(r11, r12), routes).result, 5.seconds)
-
-      // Both responses must succeed
-      parseAndDropDate(buff) must_== (
-        (
-          Ok,
-          Set(
-            H.`Content-Length`.unsafeFromLong(8 + 4),
-            H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`)),
-          "Result: done"))
-    }
+      (runRequest(tw, Seq(r11, r12), routes).result).map { buff =>
+        // Both responses must succeed
+        assert(
+          parseAndDropDate(buff) == (
+            (
+              Ok,
+              Set(
+                H.`Content-Length`.unsafeFromLong(8 + 4),
+                H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`)),
+              "Result: done")))
+      }
+  }
 
-    "Maintain the connection if the body is ignored but was already read to completion by the Http1Stage" in {
+  fixture.test(
+    "Http1ServerStage: routes should Maintain the connection if the body is ignored but was already read to completion by the Http1Stage") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case _ =>
           IO.pure(Response().withEntity("foo"))
@@ -329,17 +352,19 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
       val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
 
-      val buff = Await.result(runRequest(Seq(req1, req2), routes).result, 5.seconds)
-
-      val hs = Set(
-        H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
-        H.`Content-Length`.unsafeFromLong(3))
-      // Both responses must succeed
-      dropDate(ResponseParser.parseBuffer(buff)) must_== ((Ok, hs, "foo"))
-      dropDate(ResponseParser.parseBuffer(buff)) must_== ((Ok, hs, "foo"))
-    }
+      (runRequest(tw, Seq(req1, req2), routes).result).map { buff =>
+        val hs = Set(
+          H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
+          H.`Content-Length`.unsafeFromLong(3))
+        // Both responses must succeed
+        assert(dropDate(ResponseParser.parseBuffer(buff)) == ((Ok, hs, "foo")))
+        assert(dropDate(ResponseParser.parseBuffer(buff)) == ((Ok, hs, "foo")))
+      }
+  }
 
-    "Drop the connection if the body is ignored and was not read to completion by the Http1Stage" in {
+  fixture.test(
+    "Http1ServerStage: routes should Drop the connection if the body is ignored and was not read to completion by the Http1Stage") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case _ =>
           IO.pure(Response().withEntity("foo"))
@@ -352,17 +377,19 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
 
       val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
 
-      val buff = Await.result(runRequest(Seq(r11, r12, req2), routes).result, 5.seconds)
-
-      val hs = Set(
-        H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
-        H.`Content-Length`.unsafeFromLong(3))
-      // Both responses must succeed
-      dropDate(ResponseParser.parseBuffer(buff)) must_== ((Ok, hs, "foo"))
-      buff.remaining() must_== 0
-    }
+      (runRequest(tw, Seq(r11, r12, req2), routes).result).map { buff =>
+        val hs = Set(
+          H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
+          H.`Content-Length`.unsafeFromLong(3))
+        // Both responses must succeed
+        assert(dropDate(ResponseParser.parseBuffer(buff)) == ((Ok, hs, "foo")))
+        assert(buff.remaining() == 0)
+      }
+  }
 
-    "Handle routes that runs the request body for non-chunked" in {
+  fixture.test(
+    "Http1ServerStage: routes should Handle routes that runs the request body for non-chunked") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case req =>
           req.body.compile.drain *> IO.pure(Response().withEntity("foo"))
@@ -374,18 +401,19 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       val (r11, r12) = req1.splitAt(req1.length - 1)
       val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
 
-      val buff = Await.result(runRequest(Seq(r11, r12, req2), routes).result, 5.seconds)
-
-      val hs = Set(
-        H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
-        H.`Content-Length`.unsafeFromLong(3))
-      // Both responses must succeed
-      dropDate(ResponseParser.parseBuffer(buff)) must_== ((Ok, hs, "foo"))
-      dropDate(ResponseParser.parseBuffer(buff)) must_== ((Ok, hs, "foo"))
-    }
+      (runRequest(tw, Seq(r11, r12, req2), routes).result).map { buff =>
+        val hs = Set(
+          H.`Content-Type`(MediaType.text.plain, Charset.`UTF-8`),
+          H.`Content-Length`.unsafeFromLong(3))
+        // Both responses must succeed
+        assert(dropDate(ResponseParser.parseBuffer(buff)) == ((Ok, hs, "foo")))
+        assert(dropDate(ResponseParser.parseBuffer(buff)) == ((Ok, hs, "foo")))
+      }
+  }
 
-    // Think of this as drunk HTTP pipelining
-    "Not die when two requests come in back to back" in {
+  // Think of this as drunk HTTP pipelining
+  fixture.test("Http1ServerStage: routes should Not die when two requests come in back to back") {
+    tw =>
       val routes = HttpRoutes
         .of[IO] { case req =>
           IO.pure(Response(body = req.body))
@@ -396,112 +424,117 @@ class Http1ServerStageSpec extends Http4sSpec with AfterAll {
       val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
       val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
 
-      val buff = Await.result(runRequest(Seq(req1 + req2), routes).result, 5.seconds)
-
-      // Both responses must succeed
-      dropDate(ResponseParser.parseBuffer(buff)) must_== (
-        (
-          Ok,
-          Set(H.`Content-Length`.unsafeFromLong(4)),
-          "done"))
-      dropDate(ResponseParser.parseBuffer(buff)) must_== (
-        (
-          Ok,
-          Set(H.`Content-Length`.unsafeFromLong(5)),
-          "total"))
-    }
-
-    "Handle using the request body as the response body" in {
-      val routes = HttpRoutes
-        .of[IO] { case req =>
-          IO.pure(Response(body = req.body))
-        }
-        .orNotFound
+      (runRequest(tw, Seq(req1 + req2), routes).result).map { buff =>
+        // Both responses must succeed
+        assert(
+          dropDate(ResponseParser.parseBuffer(buff)) == (
+            (
+              Ok,
+              Set(H.`Content-Length`.unsafeFromLong(4)),
+              "done")))
+        assert(
+          dropDate(ResponseParser.parseBuffer(buff)) == (
+            (
+              Ok,
+              Set(H.`Content-Length`.unsafeFromLong(5)),
+              "total")))
+      }
+  }
 
-      // The first request will get split into two chunks, leaving the last byte off
-      val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
-      val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
+  fixture.test(
+    "Http1ServerStage: routes should Handle using the request body as the response body") { tw =>
+    val routes = HttpRoutes
+      .of[IO] { case req =>
+        IO.pure(Response(body = req.body))
+      }
+      .orNotFound
 
-      val buff = Await.result(runRequest(Seq(req1, req2), routes).result, 5.seconds)
+    // The first request will get split into two chunks, leaving the last byte off
+    val req1 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
+    val req2 = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 5\r\n\r\ntotal"
 
+    (runRequest(tw, Seq(req1, req2), routes).result).map { buff =>
       // Both responses must succeed
-      dropDate(ResponseParser.parseBuffer(buff)) must_== (
-        (
-          Ok,
-          Set(H.`Content-Length`.unsafeFromLong(4)),
-          "done"))
-      dropDate(ResponseParser.parseBuffer(buff)) must_== (
-        (
-          Ok,
-          Set(H.`Content-Length`.unsafeFromLong(5)),
-          "total"))
+      assert(
+        dropDate(ResponseParser.parseBuffer(buff)) == (
+          (
+            Ok,
+            Set(H.`Content-Length`.unsafeFromLong(4)),
+            "done")))
+      assert(
+        dropDate(ResponseParser.parseBuffer(buff)) == (
+          (
+            Ok,
+            Set(H.`Content-Length`.unsafeFromLong(5)),
+            "total")))
     }
+  }
 
-    {
-      def req(path: String) =
-        s"GET /$path HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" +
-          "3\r\n" +
-          "foo\r\n" +
-          "0\r\n" +
-          "Foo:Bar\r\n\r\n"
-
-      val routes = HttpRoutes
-        .of[IO] {
-          case req if req.pathInfo == path"/foo" =>
-            for {
-              _ <- req.body.compile.drain
-              hs <- req.trailerHeaders
-              resp <- Ok(hs.toList.mkString)
-            } yield resp
-
-          case req if req.pathInfo == path"/bar" =>
-            for {
-              // Don't run the body
-              hs <- req.trailerHeaders
-              resp <- Ok(hs.toList.mkString)
-            } yield resp
-        }
-        .orNotFound
-
-      "Handle trailing headers" in {
-        val buff = Await.result(runRequest(Seq(req("foo")), routes).result, 5.seconds)
-
-        val results = dropDate(ResponseParser.parseBuffer(buff))
-        results._1 must_== Ok
-        results._3 must_== "Foo: Bar"
-      }
+  def req(path: String) =
+    s"POST /$path HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" +
+      "3\r\n" +
+      "foo\r\n" +
+      "0\r\n" +
+      "Foo:Bar\r\n\r\n"
+
+  val routes2 = HttpRoutes
+    .of[IO] {
+      case req if req.pathInfo === path"/foo" =>
+        for {
+          _ <- req.body.compile.drain
+          hs <- req.trailerHeaders
+          resp <- Ok(hs.toList.mkString)
+        } yield resp
+
+      case req if req.pathInfo === path"/bar" =>
+        for {
+          // Don't run the body
+          hs <- req.trailerHeaders
+          resp <- Ok(hs.toList.mkString)
+        } yield resp
+    }
+    .orNotFound
 
-      "Fail if you use the trailers before they have resolved" in {
-        val buff = Await.result(runRequest(Seq(req("bar")), routes).result, 5.seconds)
+  fixture.test("Http1ServerStage: routes should Handle trailing headers") { tw =>
+    (runRequest(tw, Seq(req("foo")), routes2).result).map { buff =>
+      val results = dropDate(ResponseParser.parseBuffer(buff))
+      assert(results._1 == Ok)
+      assert(results._3 == "Foo: Bar")
+    }
+  }
 
+  fixture.test(
+    "Http1ServerStage: routes should Fail if you use the trailers before they have resolved") {
+    tw =>
+      (runRequest(tw, Seq(req("bar")), routes2).result).map { buff =>
         val results = dropDate(ResponseParser.parseBuffer(buff))
-        results._1 must_== InternalServerError
+        assertEquals(results._1, InternalServerError)
       }
-    }
   }
 
-  "cancels on stage shutdown" in skipOnCi {
+  fixture.test("Http1ServerStage: routes should cancels on stage shutdown".flaky) { tw =>
     Deferred[IO, Unit]
       .flatMap { canceled =>
         Deferred[IO, Unit].flatMap { gate =>
-          val req = "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
+          val req =
+            "POST /sync HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length: 4\r\n\r\ndone"
           val app: HttpApp[IO] = HttpApp { _ =>
             gate.complete(()) >> canceled.complete(()) >> IO.never[Response[IO]]
           }
           for {
-            head <- IO(runRequest(List(req), app))
+            head <- IO(runRequest(tw, List(req), app))
             _ <- gate.get
             _ <- IO(head.closePipeline(None))
             _ <- canceled.get
           } yield ()
         }
       }
-      .unsafeRunTimed(3.seconds) must beSome(())
   }
 
-  "Disconnect if we read an EOF" in {
-    val head = runRequest(Seq.empty, Kleisli.liftF(Ok("")))
-    Await.ready(head.result, 10.seconds)
-    head.closeCauses must_== Seq(None)
+  fixture.test("Http1ServerStage: routes should Disconnect if we read an EOF") { tw =>
+    val head = runRequest(tw, Seq.empty, Kleisli.liftF(Ok("")))
+    head.result.map { _ =>
+      assert(head.closeCauses == Seq(None))
+    }
   }
 }
diff --git a/ember-core/src/test/scala/org/http4s/ember/core/ParsingSpec.scala b/ember-core/src/test/scala/org/http4s/ember/core/ParsingSpec.scala
index b76fb33880..c714f7ef91 100644
--- a/ember-core/src/test/scala/org/http4s/ember/core/ParsingSpec.scala
+++ b/ember-core/src/test/scala/org/http4s/ember/core/ParsingSpec.scala
@@ -16,20 +16,17 @@
 
 package org.http4s.ember.core
 
-import org.specs2.mutable.Specification
 import org.http4s._
 import org.http4s.implicits._
 import scodec.bits.ByteVector
 import fs2._
-import cats.effect.unsafe.implicits.global
 import cats.effect._
 import cats.data.OptionT
 import cats.syntax.all._
 import org.http4s.ember.core.Parser.Request.ReqPrelude.ParsePreludeComplete
 import org.http4s.headers.Expires
 
-class ParsingSpec extends Specification {
-  sequential
+class ParsingSpec extends Http4sSuite {
   object Helpers {
     def stripLines(s: String): String = s.replace("\r\n", "\n")
     def httpifyString(s: String): String = s.replace("\n", "\r\n")
@@ -87,292 +84,287 @@ class ParsingSpec extends Specification {
     }
   }
 
-  "Parser.Request.parser" should {
-    "Parse a request with no body correctly" in {
-      val raw =
-        """GET / HTTP/1.1
+  test("Parser.Request.parser should Parse a request with no body correctly") {
+    val raw =
+      """GET / HTTP/1.1
       |Host: www.google.com
       |
       |""".stripMargin
-      val expected = Request[IO](
-        Method.GET,
-        Uri.unsafeFromString("www.google.com"),
-        headers = Headers.of(org.http4s.headers.Host("www.google.com"))
-      )
-
-      val result = Helpers.parseRequestRig[IO](raw).unsafeRunSync()
-
-      result.method must_=== expected.method
-      result.uri.scheme must_=== expected.uri.scheme
-      // result.uri.authority must_=== expected.uri.authority
-      // result.uri.path must_=== expected.uri.path
-      // result.uri.query must_=== expected.uri.query
-      result.uri.fragment must_=== expected.uri.fragment
-      result.headers must_=== expected.headers
-      result.body.compile.toVector.unsafeRunSync() must_=== expected.body.compile.toVector
-        .unsafeRunSync()
-    }
+    val expected = Request[IO](
+      Method.GET,
+      Uri.unsafeFromString("www.google.com"),
+      headers = Headers.of(org.http4s.headers.Host("www.google.com"))
+    )
+
+    val result = Helpers.parseRequestRig[IO](raw)
+
+    result.map(_.method).assertEquals(expected.method)
+    result.map(_.uri.scheme).assertEquals(expected.uri.scheme)
+    // result.map(_.uri.authority).assertEquals(expected.uri.authority)
+    // result.map(_.uri.path).assertEquals(expected.uri.path)
+    // result.map(_.uri.query).assertEquals(expected.uri.query)
+    result.map(_.uri.fragment).assertEquals(expected.uri.fragment)
+    result.map(_.headers).assertEquals(expected.headers)
+    for {
+      r <- result
+      a <- r.body.compile.toVector
+      b <- expected.body.compile.toVector
+    } yield assertEquals(a, b)
+  }
 
-    "Parse a request with a body correctly" in {
-      val raw =
-        """POST /foo HTTP/1.1
+  test("Parser.Request.parser should Parse a request with a body correctly") {
+    val raw =
+      """POST /foo HTTP/1.1
       |Content-Type: text/plain; charset=UTF-8
       |Content-Length: 11
       |
       |Entity Here""".stripMargin
-      val expected = Request[IO](Method.POST, Uri.unsafeFromString("/foo"))
-        .withEntity("Entity Here")
-
-      val result = Helpers.parseRequestRig[IO](raw).unsafeRunSync()
-
-      result.method must_=== expected.method
-      result.uri.scheme must_=== expected.uri.scheme
-      // result.uri.authority must_=== expected.uri.authority
-      // result.uri.path must_=== expected.uri.path
-      // result.uri.query must_=== expected.uri.query
-      result.uri.fragment must_=== expected.uri.fragment
-      result.headers.toList must containTheSameElementsAs(expected.headers.toList)
-      result.body.compile.toVector.unsafeRunSync() must_=== expected.body.compile.toVector
-        .unsafeRunSync()
-    }
+    val expected = Request[IO](Method.POST, Uri.unsafeFromString("/foo"))
+      .withEntity("Entity Here")
+
+    val result = Helpers.parseRequestRig[IO](raw)
+
+    result.map(_.method).assertEquals(expected.method)
+    result.map(_.uri.scheme).assertEquals(expected.uri.scheme)
+    // result.map(_.uri.authority).assertEquals(expected.uri.authority)
+    // result.map(_.uri.path).assertEquals(expected.uri.path)
+    // result.map(_.uri.query).assertEquals(expected.uri.query)
+    result.map(_.uri.fragment).assertEquals(expected.uri.fragment)
+    result.map(_.headers).assertEquals(expected.headers)
+    for {
+      r <- result
+      a <- r.body.compile.toVector
+      b <- expected.body.compile.toVector
+    } yield assertEquals(a, b)
+  }
 
-    "Parse a simple request" in {
-      val raw =
-        """GET /foo HTTP/1.1
+  test("Parser.Request.parser should Parse a simple request") {
+    val raw =
+      """GET /foo HTTP/1.1
         |Host: localhost:8080
         |User-Agent: curl/7.64.1
         |Accept: */*
         |
         |""".stripMargin
-      val expected = Request[IO](Method.GET, Uri.unsafeFromString("/foo"))
+    val expected = Request[IO](Method.GET, Uri.unsafeFromString("/foo"))
 
-      val result = Helpers.parseRequestRig[IO](raw).unsafeRunSync()
+    val result = Helpers.parseRequestRig[IO](raw)
 
-      result.method must_=== expected.method
-      result.uri.scheme must_=== expected.uri.scheme
-    }
+    result.map(_.method).assertEquals(expected.method)
+    result.map(_.uri.scheme).assertEquals(expected.uri.scheme)
+  }
 
-    "handle a response that requires multiple chunks to be read" in {
-      val defaultMaxHeaderLength = 4096
-      val raw1 =
-        """HTTP/1.1 200 OK
+  test("Parser.Request.parser should handle a response that requires multiple chunks to be read") {
+    val defaultMaxHeaderLength = 4096
+    val raw1 =
+      """HTTP/1.1 200 OK
           |Content-type: application/json
           |Content-Length: 2
           |
           |{""".stripMargin
 
-      val raw2 = """}
+    val raw2 = """}
           |""".stripMargin
-      val http1 = Helpers.httpifyString(raw1)
-
-      val http2 = Helpers.httpifyString(raw2)
-      val encoded = (Stream(http1) ++ Stream(http2)).through(fs2.text.utf8Encode)
-
-      (for {
-        parsed <-
-          Parser.Response
-            .parser[IO](defaultMaxHeaderLength, None)(
-              encoded
-              //Helpers.forceScopedParsing[IO](raw) // Cuts off `}` in current test. Why?
-              // I don't follow what the rig is testing vs this.
-            ) //(logger)
-            .flatMap { case (resp, _) =>
-              resp.body.through(text.utf8Decode).compile.string
-            }
-      } yield parsed must_== "{}").unsafeRunSync()
-    }
-
+    val http1 = Helpers.httpifyString(raw1)
+
+    val http2 = Helpers.httpifyString(raw2)
+    val encoded = (Stream(http1) ++ Stream(http2)).through(fs2.text.utf8Encode)
+
+    (for {
+      parsed <-
+        Parser.Response
+          .parser[IO](defaultMaxHeaderLength, None)(
+            encoded
+            //Helpers.forceScopedParsing[IO](raw) // Cuts off `}` in current test. Why?
+            // I don't follow what the rig is testing vs this.
+          ) //(logger)
+          .flatMap { case (resp, _) =>
+            resp.body.through(text.utf8Decode).compile.string
+          }
+    } yield parsed == "{}").assert
   }
 
-  "Parser.Response.parser" should {
-    "handle a chunked response" in {
-      val defaultMaxHeaderLength = 4096
-      val base =
-        "SFRUUC8xLjEgMjAwIE9LDQpBcGktVmVyc2lvbjogMS40MA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEb2NrZXItRXhwZXJpbWVudGFsOiBmYWxzZQ0KT3N0eXBlOiBsaW51eA0KU2VydmVyOiBEb2NrZXIvMTkuMDMuMTEtY2UgKGxpbnV4KQ0KRGF0ZTogRnJpLCAyNiBKdW4gMjAyMCAyMjozNTo0MiBHTVQNClRyYW5zZmVyLUVuY29kaW5nOiBjaHVua2VkDQoNCjhjMw0KeyJJRCI6IllNS0U6MkZZMzpTUUc3OjZSSFo6TFlTVDpRUk9JOkU1NEU6UTdXRjpERElLOlNOSUE6Rk5UTzpJVllSIiwiQ29udGFpbmVycyI6MjUsIkNvbnRhaW5lcnNSdW5uaW5nIjowLCJDb250YWluZXJzUGF1c2VkIjowLCJDb250YWluZXJzU3RvcHBlZCI6MjUsIkltYWdlcyI6ODMsIkRyaXZlciI6Im92ZXJsYXkyIiwiRHJpdmVyU3RhdHVzIjpbWyJCYWNraW5nIEZpbGVzeXN0ZW0iLCJleHRmcyJdLFsiU3VwcG9ydHMgZF90eXBlIiwidHJ1ZSJdLFsiTmF0aXZlIE92ZXJsYXkgRGlmZiIsImZhbHNlIl1dLCJTeXN0ZW1TdGF0dXMiOm51bGwsIlBsdWdpbnMiOnsiVm9sdW1lIjpbImxvY2FsIl0sIk5ldHdvcmsiOlsiYnJpZGdlIiwiaG9zdCIsImlwdmxhbiIsIm1hY3ZsYW4iLCJudWxsIiwib3ZlcmxheSJdLCJBdXRob3JpemF0aW9uIjpudWxsLCJMb2ciOlsiYXdzbG9ncyIsImZsdWVudGQiLCJnY3Bsb2dzIiwiZ2VsZiIsImpvdXJuYWxkIiwianNvbi1maWxlIiwibG9jYWwiLCJsb2dlbnRyaWVzIiwic3BsdW5rIiwic3lzbG9nIl19LCJNZW1vcnlMaW1pdCI6dHJ1ZSwiU3dhcExpbWl0Ijp0cnVlLCJLZXJuZWxNZW1vcnkiOnRydWUsIktlcm5lbE1lbW9yeVRDUCI6dHJ1ZSwiQ3B1Q2ZzUGVyaW9kIjp0cnVlLCJDcHVDZnNRdW90YSI6dHJ1ZSwiQ1BVU2hhcmVzIjp0cnVlLCJDUFVTZXQiOnRydWUsIlBpZHNMaW1pdCI6dHJ1ZSwiSVB2NEZvcndhcmRpbmciOnRydWUsIkJyaWRnZU5mSXB0YWJsZXMiOnRydWUsIkJyaWRnZU5mSXA2dGFibGVzIjp0cnVlLCJEZWJ1ZyI6ZmFsc2UsIk5GZCI6MjQsIk9vbUtpbGxEaXNhYmxlIjp0cnVlLCJOR29yb3V0aW5lcyI6NDAsIlN5c3RlbVRpbWUiOiIyMDIwLTA2LTI2VDE1OjM1OjQyLjU1MjUzMzQzMS0wNzowMCIsIkxvZ2dpbmdEcml2ZXIiOiJqc29uLWZpbGUiLCJDZ3JvdXBEcml2ZXIiOiJjZ3JvdXBmcyIsIk5FdmVudHNMaXN0ZW5lciI6MCwiS2VybmVsVmVyc2lvbiI6IjUuNy42LWFyY2gxLTEiLCJPcGVyYXRpbmdTeXN0ZW0iOiJBcmNoIExpbnV4IiwiT1NUeXBlIjoibGludXgiLCJBcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJJbmRleFNlcnZlckFkZHJlc3MiOiJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iLCJSZWdpc3RyeUNvbmZpZyI6eyJBbGxvd05vbmRpc3RyaWJ1dGFibGVBcnRpZmFjdHNDSURScyI6W10sIkFsbG93Tm9uZGlzdHJpYnV0YWJsZUFydGlmYWN0c0hvc3RuYW1lcyI6W10sIkluc2VjdXJlUmVnaXN0cnlDSURScyI6WyIxMjcuMC4wLjAvOCJdLCJJbmRleENvbmZpZ3MiOnsiZG9ja2VyLmlvIjp7Ik5hbWUiOiJkb2NrZXIuaW8iLCJNaXJyb3JzIjpbXSwiU2VjdXJlIjp0cnVlLCJPZmZpY2lhbCI6dHJ1ZX19LCJNaXJyb3JzIjpbXX0sIk5DUFUiOjQsIk1lbVRvdGFsIjo4MjIwOTgzMjk2LCJHZW5lcmljUmVzb3VyY2VzIjpudWxsLCJEb2NrZXJSb290RGlyIjoiL3Zhci9saWIvZG9ja2VyIiwiSHR0cFByb3h5IjoiIiwiSHR0cHNQcm94eSI6IiIsIk5vUHJveHkiOiIiLCJOYW1lIjoiZGF2ZW5wb3J0LWxhcHRvcCIsIkxhYmVscyI6W10sIkV4cGVyaW1lbnRhbEJ1aWxkIjpmYWxzZSwiU2VydmVyVmVyc2lvbiI6IjE5LjAzLjExLWNlIiwiQ2x1c3RlclN0b3JlIjoiIiwiQ2x1c3RlckFkdmVydGlzZSI6IiIsIlJ1bnRpbWVzIjp7InJ1bmMiOnsicGF0aCI6InJ1bmMifX0sIkRlZmF1bHRSdW50aW1lIjoicnVuYyIsIlN3YXJtIjp7Ik5vZGVJRCI6IiIsIk5vZGVBZGRyIjoiIiwiTG9jYWxOb2RlU3RhdGUiOiJpbmFjdGl2ZSIsIkNvbnRyb2xBdmFpbGFibGUiOmZhbHNlLCJFcnJvciI6IiIsIlJlbW90ZU1hbmFnZXJzIjpudWxsfSwiTGl2ZVJlc3RvcmVFbmFibGVkIjpmYWxzZSwiSXNvbGF0aW9uIjoiIiwiSW5pdEJpbmFyeSI6ImRvY2tlci1pbml0IiwiQ29udGFpbmVyZENvbW1pdCI6eyJJRCI6ImQ3NmMxMjFmNzZhNWZjOGE0NjJkYzY0NTk0YWVhNzJmZTE4ZTExNzgubSIsIkV4cGVjdGVkIjoiZDc2YzEyMWY3NmE1ZmM4YTQ2MmRjNjQ1OTRhZWE3MmZlMThlMTE3OC5tIn0sIlJ1bmNDb21taXQiOnsiSUQiOiJkYzkyMDhhMzMwM2ZlZWY1YjM4MzlmNDMyM2Q5YmViMzZkZjBhOWRkIiwiRXhwZWN0ZWQiOiJkYzkyMDhhMzMwM2ZlZWY1YjM4MzlmNDMyM2Q5YmViMzZkZjBhOWRkIn0sIkluaXRDb21taXQiOnsiSUQiOiJmZWMzNjgzIiwiRXhwZWN0ZWQiOiJmZWMzNjgzIn0sIlNlY3VyaXR5T3B0aW9ucyI6WyJuYW1lPXNlY2NvbXAscHJvZmlsZT1kZWZhdWx0Il0sIldhcm5pbmdzIjpudWxsfQoNCjANCg0K"
-      val baseBv = ByteVector.fromBase64(base).get
-
-      Parser.Response
-        .parser[IO](defaultMaxHeaderLength, None)(Stream.chunk(Chunk.byteVector(baseBv)))
-        .flatMap { case (resp, _) =>
-          resp.body.through(text.utf8Decode).compile.string
+  test("Parser.Response.parser should handle a chunked response") {
+    val defaultMaxHeaderLength = 4096
+    val base =
+      "SFRUUC8xLjEgMjAwIE9LDQpBcGktVmVyc2lvbjogMS40MA0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uDQpEb2NrZXItRXhwZXJpbWVudGFsOiBmYWxzZQ0KT3N0eXBlOiBsaW51eA0KU2VydmVyOiBEb2NrZXIvMTkuMDMuMTEtY2UgKGxpbnV4KQ0KRGF0ZTogRnJpLCAyNiBKdW4gMjAyMCAyMjozNTo0MiBHTVQNClRyYW5zZmVyLUVuY29kaW5nOiBjaHVua2VkDQoNCjhjMw0KeyJJRCI6IllNS0U6MkZZMzpTUUc3OjZSSFo6TFlTVDpRUk9JOkU1NEU6UTdXRjpERElLOlNOSUE6Rk5UTzpJVllSIiwiQ29udGFpbmVycyI6MjUsIkNvbnRhaW5lcnNSdW5uaW5nIjowLCJDb250YWluZXJzUGF1c2VkIjowLCJDb250YWluZXJzU3RvcHBlZCI6MjUsIkltYWdlcyI6ODMsIkRyaXZlciI6Im92ZXJsYXkyIiwiRHJpdmVyU3RhdHVzIjpbWyJCYWNraW5nIEZpbGVzeXN0ZW0iLCJleHRmcyJdLFsiU3VwcG9ydHMgZF90eXBlIiwidHJ1ZSJdLFsiTmF0aXZlIE92ZXJsYXkgRGlmZiIsImZhbHNlIl1dLCJTeXN0ZW1TdGF0dXMiOm51bGwsIlBsdWdpbnMiOnsiVm9sdW1lIjpbImxvY2FsIl0sIk5ldHdvcmsiOlsiYnJpZGdlIiwiaG9zdCIsImlwdmxhbiIsIm1hY3ZsYW4iLCJudWxsIiwib3ZlcmxheSJdLCJBdXRob3JpemF0aW9uIjpudWxsLCJMb2ciOlsiYXdzbG9ncyIsImZsdWVudGQiLCJnY3Bsb2dzIiwiZ2VsZiIsImpvdXJuYWxkIiwianNvbi1maWxlIiwibG9jYWwiLCJsb2dlbnRyaWVzIiwic3BsdW5rIiwic3lzbG9nIl19LCJNZW1vcnlMaW1pdCI6dHJ1ZSwiU3dhcExpbWl0Ijp0cnVlLCJLZXJuZWxNZW1vcnkiOnRydWUsIktlcm5lbE1lbW9yeVRDUCI6dHJ1ZSwiQ3B1Q2ZzUGVyaW9kIjp0cnVlLCJDcHVDZnNRdW90YSI6dHJ1ZSwiQ1BVU2hhcmVzIjp0cnVlLCJDUFVTZXQiOnRydWUsIlBpZHNMaW1pdCI6dHJ1ZSwiSVB2NEZvcndhcmRpbmciOnRydWUsIkJyaWRnZU5mSXB0YWJsZXMiOnRydWUsIkJyaWRnZU5mSXA2dGFibGVzIjp0cnVlLCJEZWJ1ZyI6ZmFsc2UsIk5GZCI6MjQsIk9vbUtpbGxEaXNhYmxlIjp0cnVlLCJOR29yb3V0aW5lcyI6NDAsIlN5c3RlbVRpbWUiOiIyMDIwLTA2LTI2VDE1OjM1OjQyLjU1MjUzMzQzMS0wNzowMCIsIkxvZ2dpbmdEcml2ZXIiOiJqc29uLWZpbGUiLCJDZ3JvdXBEcml2ZXIiOiJjZ3JvdXBmcyIsIk5FdmVudHNMaXN0ZW5lciI6MCwiS2VybmVsVmVyc2lvbiI6IjUuNy42LWFyY2gxLTEiLCJPcGVyYXRpbmdTeXN0ZW0iOiJBcmNoIExpbnV4IiwiT1NUeXBlIjoibGludXgiLCJBcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJJbmRleFNlcnZlckFkZHJlc3MiOiJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iLCJSZWdpc3RyeUNvbmZpZyI6eyJBbGxvd05vbmRpc3RyaWJ1dGFibGVBcnRpZmFjdHNDSURScyI6W10sIkFsbG93Tm9uZGlzdHJpYnV0YWJsZUFydGlmYWN0c0hvc3RuYW1lcyI6W10sIkluc2VjdXJlUmVnaXN0cnlDSURScyI6WyIxMjcuMC4wLjAvOCJdLCJJbmRleENvbmZpZ3MiOnsiZG9ja2VyLmlvIjp7Ik5hbWUiOiJkb2NrZXIuaW8iLCJNaXJyb3JzIjpbXSwiU2VjdXJlIjp0cnVlLCJPZmZpY2lhbCI6dHJ1ZX19LCJNaXJyb3JzIjpbXX0sIk5DUFUiOjQsIk1lbVRvdGFsIjo4MjIwOTgzMjk2LCJHZW5lcmljUmVzb3VyY2VzIjpudWxsLCJEb2NrZXJSb290RGlyIjoiL3Zhci9saWIvZG9ja2VyIiwiSHR0cFByb3h5IjoiIiwiSHR0cHNQcm94eSI6IiIsIk5vUHJveHkiOiIiLCJOYW1lIjoiZGF2ZW5wb3J0LWxhcHRvcCIsIkxhYmVscyI6W10sIkV4cGVyaW1lbnRhbEJ1aWxkIjpmYWxzZSwiU2VydmVyVmVyc2lvbiI6IjE5LjAzLjExLWNlIiwiQ2x1c3RlclN0b3JlIjoiIiwiQ2x1c3RlckFkdmVydGlzZSI6IiIsIlJ1bnRpbWVzIjp7InJ1bmMiOnsicGF0aCI6InJ1bmMifX0sIkRlZmF1bHRSdW50aW1lIjoicnVuYyIsIlN3YXJtIjp7Ik5vZGVJRCI6IiIsIk5vZGVBZGRyIjoiIiwiTG9jYWxOb2RlU3RhdGUiOiJpbmFjdGl2ZSIsIkNvbnRyb2xBdmFpbGFibGUiOmZhbHNlLCJFcnJvciI6IiIsIlJlbW90ZU1hbmFnZXJzIjpudWxsfSwiTGl2ZVJlc3RvcmVFbmFibGVkIjpmYWxzZSwiSXNvbGF0aW9uIjoiIiwiSW5pdEJpbmFyeSI6ImRvY2tlci1pbml0IiwiQ29udGFpbmVyZENvbW1pdCI6eyJJRCI6ImQ3NmMxMjFmNzZhNWZjOGE0NjJkYzY0NTk0YWVhNzJmZTE4ZTExNzgubSIsIkV4cGVjdGVkIjoiZDc2YzEyMWY3NmE1ZmM4YTQ2MmRjNjQ1OTRhZWE3MmZlMThlMTE3OC5tIn0sIlJ1bmNDb21taXQiOnsiSUQiOiJkYzkyMDhhMzMwM2ZlZWY1YjM4MzlmNDMyM2Q5YmViMzZkZjBhOWRkIiwiRXhwZWN0ZWQiOiJkYzkyMDhhMzMwM2ZlZWY1YjM4MzlmNDMyM2Q5YmViMzZkZjBhOWRkIn0sIkluaXRDb21taXQiOnsiSUQiOiJmZWMzNjgzIiwiRXhwZWN0ZWQiOiJmZWMzNjgzIn0sIlNlY3VyaXR5T3B0aW9ucyI6WyJuYW1lPXNlY2NvbXAscHJvZmlsZT1kZWZhdWx0Il0sIldhcm5pbmdzIjpudWxsfQoNCjANCg0K"
+    val baseBv = ByteVector.fromBase64(base).get
 
-        }
-        .map {
-
-          _.size must beGreaterThan(0)
-        }
-        .unsafeRunSync()
-    }
+    Parser.Response
+      .parser[IO](defaultMaxHeaderLength, None)(Stream.chunk(Chunk.byteVector(baseBv)))
+      .flatMap { case (resp, _) =>
+        resp.body.through(text.utf8Decode).compile.string
+      }
+      .map { x =>
+        x.size > 0
+      }
+      .assert
+  }
 
-    "parse a chunked simple" in {
-      val defaultMaxHeaderLength = 4096
-      val respS =
-        Stream(
-          "HTTP/1.1 200 OK\r\n",
-          "Content-Type: text/plain\r\n",
-          "Transfer-Encoding: chunked\r\n\r\n",
-          // "Trailer: Expires\r\n\r\n",
-          "7\r\n",
-          "Mozilla\r\n",
-          "9\r\n",
-          "Developer\r\n",
-          "7\r\n",
-          "Network\r\n",
-          "0\r\n",
-          "\r\n"
-          // "Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
-        )
-      val byteStream: Stream[IO, Byte] = respS
-        .flatMap(s =>
-          Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1))))
-
-      Parser.Response
-        .parser[IO](defaultMaxHeaderLength, None)(byteStream)
-        .flatMap { case (resp, _) =>
-          resp.body.through(text.utf8Decode).compile.string.map { body =>
-            body must beEqualTo("MozillaDeveloperNetwork")
-          }
+  test("Parser.Response.parser should parse a chunked simple") {
+    val defaultMaxHeaderLength = 4096
+    val respS =
+      Stream(
+        "HTTP/1.1 200 OK\r\n",
+        "Content-Type: text/plain\r\n",
+        "Transfer-Encoding: chunked\r\n\r\n",
+        // "Trailer: Expires\r\n\r\n",
+        "7\r\n",
+        "Mozilla\r\n",
+        "9\r\n",
+        "Developer\r\n",
+        "7\r\n",
+        "Network\r\n",
+        "0\r\n",
+        "\r\n"
+        // "Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
+      )
+    val byteStream: Stream[IO, Byte] = respS
+      .flatMap(s =>
+        Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1))))
+
+    Parser.Response
+      .parser[IO](defaultMaxHeaderLength, None)(byteStream)
+      .flatMap { case (resp, _) =>
+        resp.body.through(text.utf8Decode).compile.string.map { body =>
+          body == "MozillaDeveloperNetwork"
         }
-        .unsafeRunSync()
-    }
+      }
+      .assert
+  }
 
-    "parse a chunked with trailer headers" in {
-      val defaultMaxHeaderLength = 4096
-      val respS =
-        Stream(
-          "HTTP/1.1 200 OK\r\n",
-          "Content-Type: text/plain\r\n",
-          "Transfer-Encoding: chunked\r\n",
-          "Trailer: Expires\r\n\r\n",
-          "7\r\n",
-          "Mozilla\r\n",
-          "9\r\n",
-          "Developer\r\n",
-          "7\r\n",
-          "Network\r\n",
-          "0\r\n",
-          "Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
-          // "\r\n"
+  test("Parser.Response.parser should parse a chunked with trailer headers") {
+    val defaultMaxHeaderLength = 4096
+    val respS =
+      Stream(
+        "HTTP/1.1 200 OK\r\n",
+        "Content-Type: text/plain\r\n",
+        "Transfer-Encoding: chunked\r\n",
+        "Trailer: Expires\r\n\r\n",
+        "7\r\n",
+        "Mozilla\r\n",
+        "9\r\n",
+        "Developer\r\n",
+        "7\r\n",
+        "Network\r\n",
+        "0\r\n",
+        "Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
+        // "\r\n"
+      )
+    val byteStream: Stream[IO, Byte] = respS
+      .flatMap(s =>
+        Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))))
+
+    Parser.Response
+      .parser[IO](defaultMaxHeaderLength, None)(byteStream)
+      .flatMap { case (resp, _) =>
+        for {
+          body <- resp.body.through(text.utf8Decode).compile.string
+          trailers <- resp.trailerHeaders
+        } yield (body == "MozillaDeveloperNetwork").&&(
+          trailers.get(Expires).isDefined
         )
-      val byteStream: Stream[IO, Byte] = respS
-        .flatMap(s =>
-          Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))))
-
-      Parser.Response
-        .parser[IO](defaultMaxHeaderLength, None)(byteStream)
-        .flatMap { case (resp, _) =>
-          for {
-            body <- resp.body.through(text.utf8Decode).compile.string
-            trailers <- resp.trailerHeaders
-          } yield (body must beEqualTo("MozillaDeveloperNetwork")).and(
-            trailers.get(Expires) must beSome
-          )
-        }
-        .unsafeRunSync()
-    }
+      }
+      .assert
   }
 
-  "Header Parser" should {
-    "handle headers in a section" in {
-      val base = """Content-Type: text/plain; charset=UTF-8
+  test("Header Parser should handle headers in a section") {
+    val base = """Content-Type: text/plain; charset=UTF-8
       |Content-Length: 11
       |
       |""".stripMargin
-      val asHttp = Helpers.httpifyString(base)
-      val bv = asHttp.getBytes()
+    val asHttp = Helpers.httpifyString(base)
+    val bv = asHttp.getBytes()
 
-      val (headers, rest, chunked, length) = Parser.HeaderP.headersInSection(bv) match {
-        case Parser.HeaderP.ParseHeadersCompleted(headers, rest, chunked, length) =>
-          (headers, rest, chunked, length)
-        case _ => ???
-      }
+    val (headers, rest, chunked, length) = Parser.HeaderP.headersInSection(bv) match {
+      case Parser.HeaderP.ParseHeadersCompleted(headers, rest, chunked, length) =>
+        (headers, rest, chunked, length)
+      case _ => ???
+    }
 
-      (
-        headers.toList must_=== List(
-          Header("Content-Type", "text/plain; charset=UTF-8"),
-          Header("Content-Length", "11"))
-      ).and(
-        rest.isEmpty must beTrue
-      ).and(
-        chunked must beFalse
-      ).and(
-        length must beSome.like { case l =>
-          l must_=== 11L
-        }
-      )
+    assert(
+      headers.toList == List(
+        Header("Content-Type", "text/plain; charset=UTF-8"),
+        Header("Content-Length", "11"))
+    )
+    assert(
+      rest.isEmpty
+    )
+    assert(
+      !chunked
+    )
+    assert(
+      length == Some(11L)
+    )
 
-    }
+  }
 
-    "Handle weird chunking" in {
-      val defaultMaxHeaderLength = 4096
-      val respS =
-        Stream(
-          "Content-Type: text/plain\r\n",
-          "Transfer-Encoding: chunked\r\n",
-          "Trailer: Expires\r\n",
-          "\r\n"
-        )
-      val byteStream: Stream[IO, Byte] = respS
-        .flatMap(s =>
-          Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))))
-
-      val headers = Parser.HeaderP
-        .parseHeaders(byteStream, defaultMaxHeaderLength, None)
-        .flatMap { case (headers, _, _, _) =>
-          Pull.output1(headers)
-        }
-        .stream
-        .compile
-        .lastOrError
-        .unsafeRunSync()
+  test("Header Parser should Handle weird chunking") {
+    val defaultMaxHeaderLength = 4096
+    val respS =
+      Stream(
+        "Content-Type: text/plain\r\n",
+        "Transfer-Encoding: chunked\r\n",
+        "Trailer: Expires\r\n",
+        "\r\n"
+      )
+    val byteStream: Stream[IO, Byte] = respS
+      .flatMap(s =>
+        Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))))
+
+    val headers = Parser.HeaderP
+      .parseHeaders(byteStream, defaultMaxHeaderLength, None)
+      .flatMap { case (headers, _, _, _) =>
+        Pull.output1(headers)
+      }
+      .stream
+      .compile
+      .lastOrError
+      .map(_.toList)
 
-      headers.toList must_=== List(
+    headers.assertEquals(
+      List(
         Header("Content-Type", "text/plain"),
         Header("Transfer-Encoding", "chunked"),
         Header("Trailer", "Expires")
-      )
-    }
+      ))
   }
 
-  "Request Prelude" should {
-    "parse an expected value" in {
-      val raw =
-        """GET / HTTP/1.1
+  test("Request Prelude should parse an expected value") {
+    val raw =
+      """GET / HTTP/1.1
           |""".stripMargin
-      val asHttp = Helpers.httpifyString(raw)
-      val bv = asHttp.getBytes()
-
-      Parser.Request.ReqPrelude.preludeInSection(bv) match {
-        case ParsePreludeComplete(method, uri, httpVersion, rest) =>
-          (method must_=== Method.GET)
-            .and(uri must_=== uri"/")
-            .and(httpVersion must_=== HttpVersion.`HTTP/1.1`)
-            .and(rest must beEmpty)
-        case _ => ko
-        // case ParsePreludeError(throwable, method, uri, httpVersion) =>
-        // case ParsePreludeIncomlete(idx, bv, buffer, method, uri, httpVersion) =>
-      }
+    val asHttp = Helpers.httpifyString(raw)
+    val bv = asHttp.getBytes()
+
+    Parser.Request.ReqPrelude.preludeInSection(bv) match {
+      case ParsePreludeComplete(method, uri, httpVersion, rest) =>
+        assert(method == Method.GET)
+        assert(uri == uri"/")
+        assert(httpVersion == HttpVersion.`HTTP/1.1`)
+        assert(rest.isEmpty)
+      case _ => fail("Parse Error")
+      // case ParsePreludeError(throwable, method, uri, httpVersion) =>
+      // case ParsePreludeIncomlete(idx, bv, buffer, method, uri, httpVersion) =>
     }
   }
 
-  "Response Prelude" should {
-    "parse an expected value" in {
-      val raw =
-        """HTTP/1.1 200 OK
+  test("Response Prelude should parse an expected value") {
+    val raw =
+      """HTTP/1.1 200 OK
         |""".stripMargin
-      val asHttp = Helpers.httpifyString(raw)
-      val bv = asHttp.getBytes()
-      Parser.Response.RespPrelude.preludeInSection(bv) match {
-        case Parser.Response.RespPrelude.RespPreludeComplete(version, status, rest) =>
-          (version must_=== HttpVersion.`HTTP/1.1`)
-            .and(
-              status must_=== Status.Ok
-            )
-            .and(
-              rest.isEmpty must beTrue
-            )
-        case _ => ko
-      }
+    val asHttp = Helpers.httpifyString(raw)
+    val bv = asHttp.getBytes()
+    Parser.Response.RespPrelude.preludeInSection(bv) match {
+      case Parser.Response.RespPrelude.RespPreludeComplete(version, status, rest) =>
+        assert(version == HttpVersion.`HTTP/1.1`)
+        assert(
+          status == Status.Ok
+        )
+        assert(
+          rest.isEmpty
+        )
+      case _ => fail("Parse Error")
     }
   }
 
diff --git a/testing/src/test/scala/org/http4s/Http4sSuite.scala b/testing/src/test/scala/org/http4s/Http4sSuite.scala
index 0c7dd65d80..3f37f871d4 100644
--- a/testing/src/test/scala/org/http4s/Http4sSuite.scala
+++ b/testing/src/test/scala/org/http4s/Http4sSuite.scala
@@ -36,6 +36,9 @@ trait Http4sSuite extends CatsEffectSuite with DisciplineSuite with munit.ScalaC
     ExecutionContext.fromExecutor(newDaemonPool("http4s-munit", min = 1, timeout = true))
   override implicit val ioRuntime: IORuntime = Http4sSuite.TestIORuntime
 
+  // allow flaky tests on ci
+  override def munitFlakyOK = sys.env.get("CI").isDefined
+
   implicit class ParseResultSyntax[A](self: ParseResult[A]) {
     def yolo: A = self.valueOr(e => sys.error(e.toString))
   }
diff --git a/tests/src/test/scala/org/http4s/ContentCodingSpec.scala b/tests/src/test/scala/org/http4s/ContentCodingSpec.scala
deleted file mode 100644
index 818d87b4ea..0000000000
--- a/tests/src/test/scala/org/http4s/ContentCodingSpec.scala
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2013 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
-
-import cats.kernel.laws.discipline.OrderTests
-import org.http4s.laws.discipline.HttpCodecTests
-import org.http4s.util.Renderer
-
-import org.specs2.scalacheck.Parameters
-import org.scalacheck.rng.Seed
-
-class ContentCodingSpec extends Http4sSpec {
-  "equals" should {
-    "be consistent with equalsIgnoreCase of the codings and quality" in prop {
-      (a: ContentCoding, b: ContentCoding) =>
-        (a == b) must_== a.coding.equalsIgnoreCase(b.coding) && a.qValue == b.qValue
-    }
-  }
-
-  "compare" should {
-    "be consistent with coding.compareToIgnoreCase for same qValue" in prop {
-      (a: ContentCoding, b: ContentCoding, q: QValue) =>
-        val aq = a.withQValue(q)
-        val bq = b.withQValue(q)
-        (aq.compare(bq)) must_== (aq.coding.compareToIgnoreCase(bq.coding))
-    }
-  }
-
-  "hashCode" should {
-    "be consistent with equality" in
-      prop { (a: ContentCoding, b: ContentCoding) =>
-        a == b must_== (a.## == b.##)
-      }
-  }
-
-  "matches" should {
-    "ContentCoding.* always matches" in
-      prop { (a: ContentCoding) =>
-        ContentCoding.`*`.matches(a) must beTrue
-      }
-    "always matches itself" in
-      prop { (a: ContentCoding) =>
-        a.matches(a) must beTrue
-      }
-  }
-
-  "parses" should {
-    "parse plain coding" in {
-      ContentCoding.parse("gzip") must_== ParseResult.success(ContentCoding.gzip)
-    }
-    "parse custom codings" in {
-      ContentCoding.parse("mycoding") must_== ContentCoding.fromString("mycoding")
-    }
-    "parse with quality" in {
-      ContentCoding.parse("gzip;q=0.8") must_== Right(ContentCoding.gzip.withQValue(qValue"0.8"))
-    }
-    "fail on empty" in {
-      ContentCoding.parse("") must beLeft
-      ContentCoding.parse(";q=0.8") must beLeft
-    }
-    "fail on non token" in {
-      ContentCoding.parse("\\\\") must beLeft
-    }
-    "parse *" in {
-      ContentCoding.parse("*") must_== ParseResult.success(ContentCoding.`*`)
-    }
-    "parse tokens starting with *" in {
-      // Strange content coding but valid
-      ContentCoding.parse("*fahon") must_== ParseResult.success(
-        ContentCoding.unsafeFromString("*fahon"))
-      ContentCoding.parse("*fahon;q=0.1") must_== ParseResult.success(
-        ContentCoding.unsafeFromString("*fahon").withQValue(qValue"0.1"))
-    }
-  }
-
-  "render" should {
-    "return coding and quality" in
-      prop { (s: ContentCoding) =>
-        Renderer
-          .renderString(s) must_== s"${s.coding.toLowerCase}${Renderer.renderString(s.qValue)}"
-      }
-  }
-
-  checkAll("Order[ContentCoding]", OrderTests[ContentCoding].order)
-  checkAll("HttpCodec[ContentCoding]", HttpCodecTests[ContentCoding].httpCodec)
-
-  checkAll("Order[ContentCoding] for #3328", OrderTests[ContentCoding].order)(
-    Parameters(
-      seed = Seed.fromBase64("2kpw5tJ8tADijqPv2GG3pUWPhjzpJUnaypQufFSWHBB=").toOption,
-      minTestsOk = 1))
-}
diff --git a/tests/src/test/scala/org/http4s/ContentCodingSuite.scala b/tests/src/test/scala/org/http4s/ContentCodingSuite.scala
new file mode 100644
index 0000000000..306e63cfa8
--- /dev/null
+++ b/tests/src/test/scala/org/http4s/ContentCodingSuite.scala
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2013 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
+
+import cats.kernel.laws.discipline.OrderTests
+import org.http4s.laws.discipline.HttpCodecTests
+import org.http4s.laws.discipline.arbitrary._
+import org.http4s.syntax.all._
+import org.http4s.util.Renderer
+
+import org.scalacheck.rng.Seed
+import org.scalacheck.Prop._
+import org.scalacheck.Test
+
+class ContentCodingSuite extends Http4sSuite {
+
+  test("equals should be consistent with equalsIgnoreCase of the codings and quality") {
+    forAll { (a: ContentCoding, b: ContentCoding) =>
+      (a == b) == (a.coding.equalsIgnoreCase(b.coding) && a.qValue == b.qValue)
+    }
+  }
+
+  test("compare should be consistent with coding.compareToIgnoreCase for same qValue") {
+    forAll { (a: ContentCoding, b: ContentCoding, q: QValue) =>
+      val aq = a.withQValue(q)
+      val bq = b.withQValue(q)
+      (aq.compare(bq)) == (aq.coding.compareToIgnoreCase(bq.coding))
+    }
+  }
+
+  test("hashCode should be consistent with equality") {
+    forAll { (a: ContentCoding, b: ContentCoding) =>
+      (a == b) == (a.## == b.##)
+    }
+  }
+
+  test("ContentCoding.* should always matches") {
+    forAll { (a: ContentCoding) =>
+      ContentCoding.`*`.matches(a)
+    }
+  }
+  test("ContentCoding should always matches itself") {
+    forAll { (a: ContentCoding) =>
+      a.matches(a)
+    }
+  }
+
+  test("parses should parse plain coding") {
+    assertEquals(ContentCoding.parse("gzip"), ParseResult.success(ContentCoding.gzip))
+  }
+  test("parses should parse custom codings") {
+    assertEquals(ContentCoding.parse("mycoding"), ContentCoding.fromString("mycoding"))
+  }
+  test("parses should parse with quality") {
+    assertEquals(
+      ContentCoding.parse("gzip;q=0.8"),
+      Right(ContentCoding.gzip.withQValue(qValue"0.8")))
+  }
+  test("parses should fail on empty") {
+    assert(ContentCoding.parse("").isLeft)
+    assert(ContentCoding.parse(";q=0.8").isLeft)
+  }
+  test("parses should fail on non token") {
+    assert(ContentCoding.parse("\\\\").isLeft)
+  }
+  test("parses should parse *") {
+    assertEquals(ContentCoding.parse("*"), ParseResult.success(ContentCoding.`*`))
+  }
+  test("parses should parse tokens starting with *") {
+    // Strange content coding but valid
+    assertEquals(
+      ContentCoding.parse("*fahon"),
+      ParseResult.success(ContentCoding.unsafeFromString("*fahon")))
+    assertEquals(
+      ContentCoding.parse("*fahon;q=0.1"),
+      ParseResult.success(ContentCoding.unsafeFromString("*fahon").withQValue(qValue"0.1")))
+  }
+
+  test("render sould return coding and quality") {
+    forAll { (s: ContentCoding) =>
+      Renderer
+        .renderString(s) == s"${s.coding.toLowerCase}${Renderer.renderString(s.qValue)}"
+    }
+  }
+
+  checkAll("Order[ContentCoding]", OrderTests[ContentCoding].order)
+  checkAll("HttpCodec[ContentCoding]", HttpCodecTests[ContentCoding].httpCodec)
+}
+
+class ContentCodingSuiteFixedSeed extends Http4sSuite {
+  override protected def scalaCheckTestParameters = Test.Parameters.default
+    .withMinSuccessfulTests(1)
+    .withInitialSeed(Seed.fromBase64("2kpw5tJ8tADijqPv2GG3pUWPhjzpJUnaypQufFSWHBB=").toOption)
+
+  checkAll("Order[ContentCoding] for #3328", OrderTests[ContentCoding].order)
+}
diff --git a/tests/src/test/scala/org/http4s/ContextRequestSpec.scala b/tests/src/test/scala/org/http4s/ContextRequestSuite.scala
similarity index 71%
rename from tests/src/test/scala/org/http4s/ContextRequestSpec.scala
rename to tests/src/test/scala/org/http4s/ContextRequestSuite.scala
index 0ed3f5d880..7d2b5289a4 100644
--- a/tests/src/test/scala/org/http4s/ContextRequestSpec.scala
+++ b/tests/src/test/scala/org/http4s/ContextRequestSuite.scala
@@ -16,24 +16,21 @@
 
 package org.http4s
 
-import org.specs2.Specification
-import org.typelevel.discipline.specs2.Discipline
 import cats._
 import cats.syntax.all._
 import cats.laws.discipline.NonEmptyTraverseTests
 import org.http4s.laws.discipline.arbitrary._
 
-class ContextRequestSpec extends Specification with Discipline {
+class ContextRequestSuite extends Http4sSuite {
   implicit def nonBodyEquality[F[_], A: Eq]: Eq[ContextRequest[F, A]] =
     Eq.instance { case (first, second) =>
       first.context === second.context &&
-        first.req === second.req
+        first.req == second.req
     }
 
-  def is =
-    checkAll(
-      "ContextRequest[F, *]",
-      NonEmptyTraverseTests[ContextRequest[Option, *]]
-        .nonEmptyTraverse[Option, Int, Int, Int, Int, Option, Option]
-    )
+  checkAll(
+    "ContextRequest[F, *]",
+    NonEmptyTraverseTests[ContextRequest[Option, *]]
+      .nonEmptyTraverse[Option, Int, Int, Int, Int, Option, Option]
+  )
 }
diff --git a/tests/src/test/scala/org/http4s/Ipv4AddressSuite.scala b/tests/src/test/scala/org/http4s/Ipv4AddressSuite.scala
new file mode 100644
index 0000000000..6cf3f51664
--- /dev/null
+++ b/tests/src/test/scala/org/http4s/Ipv4AddressSuite.scala
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2013 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
+
+import cats.kernel.laws.discipline.{HashTests, OrderTests}
+import org.http4s.Uri.Ipv4Address
+import org.http4s.laws.discipline.HttpCodecTests
+import org.http4s.laws.discipline.arbitrary._
+import org.http4s.syntax.all._
+import org.http4s.util.Renderer.renderString
+import org.scalacheck.Prop._
+
+class Ipv4AddressSuite extends Http4sSuite {
+  checkAll("Order[Ipv4Address]", OrderTests[Ipv4Address].order)
+  checkAll("Hash[Ipv4Address]", HashTests[Ipv4Address].hash)
+  checkAll("HttpCodec[Ipv4Address]", HttpCodecTests[Ipv4Address].httpCodec)
+
+  test("render should render all 4 octets") {
+    assert(renderString(ipv4"192.168.0.1") == "192.168.0.1")
+  }
+
+  test("fromInet4Address should round trip with toInet4Address") {
+    forAll { (ipv4: Ipv4Address) =>
+      assert(Ipv4Address.fromInet4Address(ipv4.toInet4Address) == ipv4)
+    }
+  }
+
+  test("fromByteArray should round trip with toByteArray") {
+    forAll { (ipv4: Ipv4Address) =>
+      assert(Ipv4Address.fromByteArray(ipv4.toByteArray) == Right(ipv4))
+    }
+  }
+
+  test("compare should be consistent with unsigned int") {
+    forAll { (xs: List[Ipv4Address]) =>
+      assert(xs.sorted.map(_.address) == xs.map(_.address).sorted)
+    }
+  }
+
+  test("compare should be consistent with Ordered") {
+    forAll { (a: Ipv4Address, b: Ipv4Address) =>
+      assert(math.signum(a.compareTo(b)) == math.signum(a.compare(b)))
+    }
+  }
+
+  test("ipv4 interpolator should be consistent with fromString") {
+    assert(Right(ipv4"127.0.0.1") == Ipv4Address.fromString("127.0.0.1"))
+    assert(Right(ipv4"192.168.0.1") == Ipv4Address.fromString("192.168.0.1"))
+  }
+
+  test("ipv4 interpolator should reject invalid values") {
+    illTyped("""ipv4"256.0.0.0"""")
+    true
+  }
+
+}
diff --git a/tests/src/test/scala/org/http4s/Ipv6AddressSuite.scala b/tests/src/test/scala/org/http4s/Ipv6AddressSuite.scala
new file mode 100644
index 0000000000..ea956d7738
--- /dev/null
+++ b/tests/src/test/scala/org/http4s/Ipv6AddressSuite.scala
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2013 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
+
+import cats.kernel.laws.discipline.{HashTests, OrderTests}
+import org.http4s.Uri.Ipv6Address
+import org.http4s.laws.discipline.HttpCodecTests
+import org.http4s.laws.discipline.arbitrary._
+import org.http4s.syntax.all._
+import org.http4s.util.Renderer.renderString
+import org.scalacheck.Prop._
+
+class Ipv6AddressSuite extends Http4sSuite {
+  checkAll("Order[Ipv6Address]", OrderTests[Ipv6Address].order)
+  checkAll("Hash[Ipv6Address]", HashTests[Ipv6Address].hash)
+  checkAll("HttpCodec[Ipv6Address]", HttpCodecTests[Ipv6Address].httpCodec)
+
+  test("render consistently with RFC5952 should 4.1: handle leading zeroes in a 16-bit field") {
+    assert(renderString(ipv6"2001:0db8::0001") == "2001:db8::1")
+  }
+  test("render consistently with RFC5952 should 4.2.1: shorten as much as possible") {
+    assert(renderString(ipv6"2001:db8:0:0:0:0:2:1") == "2001:db8::2:1")
+  }
+  test("render consistently with RFC5952 should 4.2.2: handle one 16-bit 0 field") {
+    assert(renderString(ipv6"2001:db8:0:1:1:1:1:1") == "2001:db8:0:1:1:1:1:1")
+  }
+  test("""render consistently with RFC5952 should 4.2.3: chose placement of "::"""") {
+    assert(renderString(ipv6"2001:0:0:1:0:0:0:1") == "2001:0:0:1::1")
+    renderString(ipv6"2001:db8:0:0:1:0:0:1") == "2001:db8::1:0:0:1"
+  }
+  test("render consistently with RFC5952 should 4.3: lowercase") {
+    assert(renderString(ipv6"::A:B:C:D:E:F") == "::a:b:c:d:e:f")
+  }
+
+  test("fromInet6Address should round trip with toInet6Address") {
+    forAll { (ipv6: Ipv6Address) =>
+      Ipv6Address.fromInet6Address(ipv6.toInet6Address) == ipv6
+    }
+  }
+
+  test("fromByteArray should round trip with toByteArray") {
+    forAll { (ipv6: Ipv6Address) =>
+      Ipv6Address.fromByteArray(ipv6.toByteArray) == Right(ipv6)
+    }
+  }
+
+  test("compare should be consistent with address") {
+    forAll { (xs: List[Ipv6Address]) =>
+      xs.sorted.map(_.address) == xs.map(_.address).sorted
+    }
+  }
+
+  test("compare should be consistent with Ordered") {
+    forAll { (a: Ipv6Address, b: Ipv6Address) =>
+      math.signum(a.compareTo(b)) == math.signum(a.compare(b))
+    }
+  }
+
+  test("ipv6 interpolator should be consistent with fromString") {
+    assert(Right(ipv6"::1") == Ipv6Address.fromString("::1"))
+    assert(Right(ipv6"::") == Ipv6Address.fromString("::"))
+    assert(Right(ipv6"2001:db8::7") == Ipv6Address.fromString("2001:db8::7"))
+  }
+
+  test("ipv6 interpolator should reject invalid values") {
+    illTyped("""ipv6"127.0.0.1"""")
+    true
+  }
+
+}
diff --git a/tests/src/test/scala/org/http4s/ParsedHeaderSpec.scala b/tests/src/test/scala/org/http4s/ParsedHeaderSpec.scala
deleted file mode 100644
index b723abdfd1..0000000000
--- a/tests/src/test/scala/org/http4s/ParsedHeaderSpec.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright 2013 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
-
-class ParsedHeaderSpec {}
diff --git a/tests/src/test/scala/org/http4s/QueryParamCodecLaws.scala b/tests/src/test/scala/org/http4s/QueryParamCodecLaws.scala
index deedfd20cd..5fb4db90ab 100644
--- a/tests/src/test/scala/org/http4s/QueryParamCodecLaws.scala
+++ b/tests/src/test/scala/org/http4s/QueryParamCodecLaws.scala
@@ -18,42 +18,37 @@ package org.http4s
 
 import cats._
 import cats.syntax.all._
-import org.scalacheck.{Arbitrary, Properties}
+import org.scalacheck.Arbitrary
 import org.scalacheck.Prop._
+import org.typelevel.discipline.Laws
 
 /** Instances of [[QueryParamDecoder]] and [[QueryParamEncoder]]
   * must satisfy the following properties
   */
-object QueryParamCodecLaws {
+object QueryParamCodecLaws extends Laws {
   val parseFailure = ParseFailure("For Test", "Let's assume we didn't like this value")
 
   def apply[T: Arbitrary: Eq: QueryParamDecoder: QueryParamEncoder] =
-    new Properties("QueryParamCodec") {
-      property("decode . encode == successNel") = forAll { (value: T) =>
+    new SimpleRuleSet(
+      "QueryParamCodec",
+      "decode . encode == successNel" -> forAll { (value: T) =>
         (QueryParamDecoder[T].decode _)
           .compose(QueryParamEncoder[T].encode)(value) === value.validNel
-      }
-
-      property("decode . emap(Right) . encode == successNel") = forAll { (value: T) =>
+      },
+      "decode . emap(Right) . encode == successNel" -> forAll { (value: T) =>
         (QueryParamDecoder[T].emap((t: T) => t.asRight[ParseFailure]).decode _)
           .compose(QueryParamEncoder[T].encode)(value) === value.validNel
-      }
-
-      property("decode . emap(Left) . encode == failedNel") = forAll { (value: T) =>
+      },
+      "decode . emap(Left) . encode == failedNel" -> forAll { (value: T) =>
         (QueryParamDecoder[T].emap(_ => parseFailure.asLeft[T]).decode _)
           .compose(QueryParamEncoder[T].encode)(value) === parseFailure.invalidNel
-      }
-
-      property("decode . emapValidatedNel(ValidNel) . encode == successNel") = forAll {
-        (value: T) =>
-          (QueryParamDecoder[T].emapValidatedNel((t: T) => t.validNel[ParseFailure]).decode _)
-            .compose(QueryParamEncoder[T].encode)(value) === value.validNel
-      }
-
-      property("decode . emapValidatedNel(InvalidNel) . encode == failedNel") = forAll {
-        (value: T) =>
-          (QueryParamDecoder[T].emapValidatedNel(_ => parseFailure.invalidNel[T]).decode _)
-            .compose(QueryParamEncoder[T].encode)(value) === parseFailure.invalidNel
-      }
-    }
+      },
+      "decode . emapValidatedNel(ValidNel) . encode == successNel" -> forAll { (value: T) =>
+        (QueryParamDecoder[T].emapValidatedNel((t: T) => t.validNel[ParseFailure]).decode _)
+          .compose(QueryParamEncoder[T].encode)(value) === value.validNel
+      },
+      "decode . emapValidatedNel(InvalidNel) . encode == failedNel" -> forAll { (value: T) =>
+        (QueryParamDecoder[T].emapValidatedNel(_ => parseFailure.invalidNel[T]).decode _)
+          .compose(QueryParamEncoder[T].encode)(value) === parseFailure.invalidNel
+      })
 }
diff --git a/tests/src/test/scala/org/http4s/QueryParamCodecSpec.scala b/tests/src/test/scala/org/http4s/QueryParamCodecSuite.scala
similarity index 94%
rename from tests/src/test/scala/org/http4s/QueryParamCodecSpec.scala
rename to tests/src/test/scala/org/http4s/QueryParamCodecSuite.scala
index 4ae062443f..a8891a790d 100644
--- a/tests/src/test/scala/org/http4s/QueryParamCodecSpec.scala
+++ b/tests/src/test/scala/org/http4s/QueryParamCodecSuite.scala
@@ -29,7 +29,7 @@ import org.scalacheck.{Arbitrary, Cogen, Gen}
 import org.scalacheck.Arbitrary._
 import org.scalacheck.Prop._
 
-class QueryParamCodecSpec extends Http4sSpec with QueryParamCodecInstances {
+class QueryParamCodecSuite extends Http4sSuite with QueryParamCodecInstances {
   checkAll("Boolean QueryParamCodec", QueryParamCodecLaws[Boolean])
   checkAll("Double QueryParamCodec", QueryParamCodecLaws[Double])
   checkAll("Float QueryParamCodec", QueryParamCodecLaws[Float])
@@ -51,12 +51,14 @@ class QueryParamCodecSpec extends Http4sSpec with QueryParamCodecInstances {
     ContravariantTests[QueryParamEncoder].contravariant[Int, String, Boolean])
 
   // The PlusEmpty check above validates fail() but we need an explicit test for success().
-  "success(a) always succeeds" >> forAll { (n: Int, qpv: QueryParameterValue) =>
-    QueryParamDecoder.success(n).decode(qpv) must_== n.valid
+  test("success(a) always succeeds") {
+    forAll { (n: Int, qpv: QueryParameterValue) =>
+      QueryParamDecoder.success(n).decode(qpv) == n.valid
+    }
   }
 }
 
-trait QueryParamCodecInstances { this: Http4sSpec =>
+trait QueryParamCodecInstances {
 
   // We will assume for the purposes of testing that QueryParamDecoders are equal if they
   // produce the same result for a bunch of arbitrary strings.
@@ -73,7 +75,7 @@ trait QueryParamCodecInstances { this: Http4sSpec =>
   implicit def EqQueryParamEncoder[A: Arbitrary]: Eq[QueryParamEncoder[A]] =
     Eq.instance { (x, y) =>
       val as = List.fill(100)(arbitrary[A].sample).flatten
-      as.forall(a => x.encode(a) === y.encode(a))
+      as.forall(a => x.encode(a) == y.encode(a))
     }
 
   implicit val eqInstant: Eq[Instant] = Eq.fromUniversalEquals[Instant]