From 947db344dc914d4e6a9b8988e35efced244b4bfc Mon Sep 17 00:00:00 2001 From: Kamil Kloch Date: Tue, 2 Jul 2024 00:33:23 +0200 Subject: [PATCH 1/2] Add mdoc guards to test-runtime.md. --- build.sbt | 10 +++- docs/core/test-runtime.md | 109 ++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/build.sbt b/build.sbt index 34e6ee2695..23d66178a3 100644 --- a/build.sbt +++ b/build.sbt @@ -1063,4 +1063,12 @@ lazy val stressTests = project ) .enablePlugins(NoPublishPlugin, JCStressPlugin) -lazy val docs = project.in(file("site-docs")).dependsOn(core.jvm).enablePlugins(MdocPlugin) +lazy val docs = project + .in(file("site-docs")) + .dependsOn(core.jvm, testkit.jvm) + .enablePlugins(MdocPlugin) + .settings( + libraryDependencies += "org.typelevel" %% "munit-cats-effect" % "2.0.0" + ) + + diff --git a/docs/core/test-runtime.md b/docs/core/test-runtime.md index b321d5ffb4..de1948baba 100644 --- a/docs/core/test-runtime.md +++ b/docs/core/test-runtime.md @@ -59,25 +59,30 @@ This is *exactly* the sort of functionality for which `TestControl` was built to The first sort of test we will attempt to write comes in the form of a *complete* execution. This is generally the most common scenario, in which most of the power of `TestControl` is unnecessary and the only purpose to the mock runtime is to achieve deterministic and fast time: -```scala -test("retry at least 3 times until success") { - case object TestException extends RuntimeException +```scala mdoc +import munit.CatsEffectSuite +import cats.effect.testkit.TestControl - var attempts = 0 - val action = IO { - attempts += 1 +class TestSuite extends CatsEffectSuite { + test("retry at least 3 times until success") { + case object TestException extends RuntimeException - if (attempts != 3) - throw TestException - else - "success!" - } + var attempts = 0 + val action = IO { + attempts += 1 - val program = Random.scalaUtilRandom[IO] flatMap { random => - retry(action, 1.minute, 5, random) - } + if (attempts != 3) + throw TestException + else + "success!" + } + + val program = Random.scalaUtilRandom[IO] flatMap { random => + retry(action, 1.minute, 5, random) + } - TestControl.executeEmbed(program).assertEquals("success!") + TestControl.executeEmbed(program).assertEquals("success!") + } } ``` @@ -95,33 +100,38 @@ For more advanced cases, `executeEmbed` may not be enough to properly measure th Fortunately, `TestControl` provides a more general function, `execute`, which provides precisely the functionality needed to handle such cases: -```scala -test("backoff appropriately between attempts") { - case object TestException extends RuntimeException +```scala mdoc:nest +import cats.syntax.all._ +import cats.effect.Outcome - val action = IO.raiseError(TestException) - val program = Random.scalaUtilRandom[IO] flatMap { random => - retry(action, 1.minute, 5, random) - } +class TestSuite extends CatsEffectSuite { + test("backoff appropriately between attempts") { + case object TestException extends RuntimeException - TestControl.execute(program) flatMap { control => - for { - _ <- control.results.assertEquals(None) - _ <- control.tick + val action = IO.raiseError(TestException) + val program = Random.scalaUtilRandom[IO] flatMap { random => + retry(action, 1.minute, 5, random) + } - _ <- 0.until(4) traverse { i => - for { - _ <- control.results.assertEquals(None) + TestControl.execute(program) flatMap { control: TestControl[Random[IO]] => + for { + _ <- control.results.assertEquals(None) + _ <- control.tick - interval <- control.nextInterval - _ <- IO(assert(interval >= 0.nanos)) - _ <- IO(assert(interval < (1 << i).minute)) - _ <- control.advanceAndTick(interval) - } yield () - } + _ <- List.range(0, 4) traverse { i => + for { + _ <- control.results.assertEquals(None) + + interval <- control.nextInterval + _ <- IO(assert(interval >= 0.nanos)) + _ <- IO(assert(interval < (1 << i).minute)) + _ <- control.advanceAndTick(interval) + } yield () + } - _ <- control.results.assertEquals(Some(Outcome.failed(TestException))) - } yield () + _ <- control.results.assertEquals(Some(Outcome.errored[cats.Id, Throwable, Random[IO]](TestException))) + } yield () + } } } ``` @@ -172,12 +182,17 @@ We finally have `results`, since the `program` will have terminated with an exce As you might now expect, `executeEmbed` is actually implemented in terms of `execute`: -```scala +```scala mdoc +import cats.effect.unsafe.IORuntimeConfig +import cats.{Id, ~>} +import scala.concurrent.CancellationException +import TestControl.NonTerminationException + def executeEmbed[A]( program: IO[A], config: IORuntimeConfig = IORuntimeConfig(), seed: Option[String] = None): IO[A] = - execute(program, config = config, seed = seed) flatMap { c => + TestControl.execute(program, config = config, seed = seed) flatMap { c => val nt = new (Id ~> IO) { def apply[E](e: E) = IO.pure(e) } val onCancel = IO.defer(IO.raiseError(new CancellationException())) @@ -196,7 +211,7 @@ It is very important to remember that `TestControl` is a *mock* runtime, and thu To give an intuition for the type of program which behaves strangely under `TestControl`, consider the following pathological example: -```scala +```scala mdoc IO.cede.foreverM.start flatMap { fiber => IO.sleep(1.second) *> fiber.cancel } @@ -208,23 +223,25 @@ Under `TestControl`, this program will execute forever and never terminate. What Another common pitfall with `TestControl` is the fact that you need to be careful to *not* advance time *before* a `IO.sleep` happens! Or rather, you are perfectly free to do this, but it probably won't do what you think it will do. Consider the following: -```scala +```scala mdoc +import munit.CatsEffectAssertions._ + TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.advanceAndTick(1.second) - _ <- control.results.assertEquals(Some(Outcome.succeeded(1.second))) + _ <- control.results.assertEquals(Some(Outcome.succeeded[Id, Throwable, FiniteDuration](1.second))) } yield () } ``` The above is very intuitive! Unfortunately, it is also wrong. The problem becomes a little clearer if we desugar `advanceAndTick`: -```scala +```scala mdoc TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.advance(1.second) _ <- control.tick - _ <- control.results.assertEquals(Some(Outcome.succeeded(1.second))) + _ <- control.results.assertEquals(Some(Outcome.succeeded[Id, Throwable, FiniteDuration](1.second))) } yield () } ``` @@ -233,13 +250,13 @@ We're instructing `TestControl` to advance the clock *before* we `sleep`, and th The solution is to add an additional `tick` to execute the "beginning" of the program (from the start up until the `sleep`(s)): -```scala +```scala mdoc TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.tick _ <- control.advance(1.second) _ <- control.tick - _ <- control.results.assertEquals(Some(Outcome.succeeded(1.second))) + _ <- control.results.assertEquals(Some(Outcome.succeeded[Id, Throwable, FiniteDuration](1.second))) } yield () } ``` From 34d81461652ca89d9eb6f75807b733df5387d441 Mon Sep 17 00:00:00 2001 From: Kamil Kloch Date: Tue, 2 Jul 2024 23:44:02 +0200 Subject: [PATCH 2/2] Fix fatal warnings. --- docs/core/test-runtime.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/core/test-runtime.md b/docs/core/test-runtime.md index de1948baba..6c4466a710 100644 --- a/docs/core/test-runtime.md +++ b/docs/core/test-runtime.md @@ -35,7 +35,7 @@ libraryDependencies += "org.typelevel" %% "cats-effect-testkit" % "3.5.4" % Test For the remainder of this page, we will be writing tests which verify the behavior of the following function: -```scala mdoc +```scala mdoc:silent import cats.effect.IO import cats.effect.std.Random import scala.concurrent.duration._ @@ -59,7 +59,7 @@ This is *exactly* the sort of functionality for which `TestControl` was built to The first sort of test we will attempt to write comes in the form of a *complete* execution. This is generally the most common scenario, in which most of the power of `TestControl` is unnecessary and the only purpose to the mock runtime is to achieve deterministic and fast time: -```scala mdoc +```scala mdoc:silent import munit.CatsEffectSuite import cats.effect.testkit.TestControl @@ -100,7 +100,7 @@ For more advanced cases, `executeEmbed` may not be enough to properly measure th Fortunately, `TestControl` provides a more general function, `execute`, which provides precisely the functionality needed to handle such cases: -```scala mdoc:nest +```scala mdoc:nest:silent import cats.syntax.all._ import cats.effect.Outcome @@ -155,7 +155,13 @@ More usefully, we could ask what the `nextInterval` is. When all active fibers a Since we know we're going to retry five times and ultimately fail (since the `action` never succeeds), we take advantage of `traverse` to write a simple loop within our test. For each retry, we test the following: -```scala +```scala mdoc:invisible +import munit.CatsEffectAssertions._ +import cats.effect.unsafe.IORuntime +val control: TestControl[Random[IO]] = TestControl.execute(Random.scalaUtilRandom[IO]).unsafeRunSync()(IORuntime.global) +val i: Int = 0 +``` +```scala mdoc:silent for { _ <- control.results.assertEquals(None) @@ -182,12 +188,13 @@ We finally have `results`, since the `program` will have terminated with an exce As you might now expect, `executeEmbed` is actually implemented in terms of `execute`: -```scala mdoc +```scala mdoc:invisible import cats.effect.unsafe.IORuntimeConfig import cats.{Id, ~>} import scala.concurrent.CancellationException import TestControl.NonTerminationException - +``` +```scala mdoc:silent def executeEmbed[A]( program: IO[A], config: IORuntimeConfig = IORuntimeConfig(), @@ -202,6 +209,9 @@ def executeEmbed[A]( c.tickAll *> embedded } ``` +```scala mdoc:invisible +executeEmbed(IO.unit) +``` If you ignore the messy `map` and `mapK` lifting within `Outcome`, this is actually a relatively simple bit of functionality. The `tickAll` effect causes `TestControl` to `tick` until a `sleep` boundary, then `advance` by the necessary `nextInterval`, and then repeat the process until either `isDeadlocked` is `true` or `results` is `Some`. These results are then retrieved and embedded within the outer `IO`, with cancelation and non-termination being reflected as exceptions. @@ -211,7 +221,7 @@ It is very important to remember that `TestControl` is a *mock* runtime, and thu To give an intuition for the type of program which behaves strangely under `TestControl`, consider the following pathological example: -```scala mdoc +```scala mdoc:silent IO.cede.foreverM.start flatMap { fiber => IO.sleep(1.second) *> fiber.cancel } @@ -223,9 +233,7 @@ Under `TestControl`, this program will execute forever and never terminate. What Another common pitfall with `TestControl` is the fact that you need to be careful to *not* advance time *before* a `IO.sleep` happens! Or rather, you are perfectly free to do this, but it probably won't do what you think it will do. Consider the following: -```scala mdoc -import munit.CatsEffectAssertions._ - +```scala mdoc:silent TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.advanceAndTick(1.second) @@ -236,7 +244,7 @@ TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => The above is very intuitive! Unfortunately, it is also wrong. The problem becomes a little clearer if we desugar `advanceAndTick`: -```scala mdoc +```scala mdoc:silent TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.advance(1.second) @@ -250,7 +258,7 @@ We're instructing `TestControl` to advance the clock *before* we `sleep`, and th The solution is to add an additional `tick` to execute the "beginning" of the program (from the start up until the `sleep`(s)): -```scala mdoc +```scala mdoc:silent TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => for { _ <- control.tick