From 1eedef8dc10863b5b1f98e5e121a8d33877d25cf Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 7 Sep 2022 05:25:09 +0000 Subject: [PATCH 1/3] Implement test timeouts --- .../main/scala/munit/CatsEffectSuite.scala | 30 ++++++++++++++++--- .../scala/munit/CatsEffectSuiteSpec.scala | 6 ++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/munit/CatsEffectSuite.scala b/core/src/main/scala/munit/CatsEffectSuite.scala index 32ff1f2..1aef11a 100644 --- a/core/src/main/scala/munit/CatsEffectSuite.scala +++ b/core/src/main/scala/munit/CatsEffectSuite.scala @@ -19,7 +19,9 @@ package munit import cats.effect.unsafe.IORuntime import cats.effect.{IO, SyncIO} -import scala.concurrent.{ExecutionContext, Future} +import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future, TimeoutException} +import scala.concurrent.duration._ import munit.internal.NestingChecks.{checkNestingIO, checkNestingSyncIO} abstract class CatsEffectSuite @@ -28,9 +30,16 @@ abstract class CatsEffectSuite with CatsEffectFixtures with CatsEffectFunFixtures { - implicit def munitIoRuntime: IORuntime = IORuntime.global + @deprecated("Use munitIORuntime", "2.0.0") + def munitIoRuntime: IORuntime = IORuntime.global + implicit def munitIORuntime: IORuntime = munitIoRuntime: @nowarn - override implicit val munitExecutionContext: ExecutionContext = munitIoRuntime.compute + override implicit def munitExecutionContext: ExecutionContext = munitIORuntime.compute + + def munitIOTimeout: Duration = 30.seconds + + // buys us a 1s window to cancel the IO, before munit cancels the Future + override def munitTimeout: Duration = munitIOTimeout + 1.second override def munitValueTransforms: List[ValueTransform] = super.munitValueTransforms ++ List(munitIOTransform, munitSyncIOTransform) @@ -38,7 +47,20 @@ abstract class CatsEffectSuite private val munitIOTransform: ValueTransform = new ValueTransform( "IO", - { case e: IO[_] => checkNestingIO(e).unsafeToFuture() } + { case e: IO[_] => + val unnestedIO = checkNestingIO(e) + + // TODO cleanup after CE 3.4.0 is released + val fd = Some(munitIOTimeout).collect { case fd: FiniteDuration => fd } + val timedIO = fd.fold(unnestedIO) { duration => + unnestedIO.timeoutTo( + duration, + IO.raiseError(new TimeoutException(s"test timed out after $duration")) + ) + } + + timedIO.unsafeToFuture() + } ) private val munitSyncIOTransform: ValueTransform = diff --git a/core/src/test/scala/munit/CatsEffectSuiteSpec.scala b/core/src/test/scala/munit/CatsEffectSuiteSpec.scala index c7c3f46..6a1cd49 100644 --- a/core/src/test/scala/munit/CatsEffectSuiteSpec.scala +++ b/core/src/test/scala/munit/CatsEffectSuiteSpec.scala @@ -18,9 +18,15 @@ package munit import cats.effect.{IO, SyncIO} import scala.concurrent.Future +import scala.concurrent.duration._ class CatsEffectSuiteSpec extends CatsEffectSuite { + override def munitIOTimeout = 100.millis + override def munitTimeout = Int.MaxValue.nanos // so only our timeout is in effect + + test("times out".fail) { IO.sleep(1.second) } + test("nested IO fail".fail) { IO(IO(1)) } test("nested IO and SyncIO fail".fail) { IO(SyncIO(1)) } test("nested IO and Future fail".fail) { IO(Future.successful(1)) } From 4fe23f36ee43e7d2f7ae9ebcd90af906c58fcd01 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 7 Sep 2022 16:58:51 +0000 Subject: [PATCH 2/3] Scaladoc for `munitIOTimeout`, `munitTimeout` --- core/src/main/scala/munit/CatsEffectSuite.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/munit/CatsEffectSuite.scala b/core/src/main/scala/munit/CatsEffectSuite.scala index 1aef11a..a6c7795 100644 --- a/core/src/main/scala/munit/CatsEffectSuite.scala +++ b/core/src/main/scala/munit/CatsEffectSuite.scala @@ -36,9 +36,24 @@ abstract class CatsEffectSuite override implicit def munitExecutionContext: ExecutionContext = munitIORuntime.compute + /** The timeout for [[cats.effect.IO IO]]-based tests. When it expires it will gracefully cancel + * the fiber running the test and invoke any finalizers. + * + * Note that the fiber may still hang while running finalizers or even be uncancelable. In this + * case the [[munitTimeout]] will take effect, with the caveat that the hanging fiber will be + * leaked. + */ def munitIOTimeout: Duration = 30.seconds - // buys us a 1s window to cancel the IO, before munit cancels the Future + /** The overall timeout applicable to all tests in the suite, including those written in terms of + * [[scala.concurrent.Future Future]] or synchronous code. This is implemented by the MUnit + * framework itself. + * + * When this timeout expires, the suite will proceed without waiting for cancelation of the test + * or even attempting to cancel it. For that reason it is recommended to set this to a greater + * value than [[munitIOTimeout]], which performs graceful cancelation of + * [[cats.effect.IO IO]]-based tests. The default grace period for cancelation is 1 second. + */ override def munitTimeout: Duration = munitIOTimeout + 1.second override def munitValueTransforms: List[ValueTransform] = From f307b515749ae1c0a6a2df44315db5b9ee1f9217 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 7 Sep 2022 17:04:40 +0000 Subject: [PATCH 3/3] Clarify that timed-out tests fail --- core/src/main/scala/munit/CatsEffectSuite.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/munit/CatsEffectSuite.scala b/core/src/main/scala/munit/CatsEffectSuite.scala index a6c7795..7900941 100644 --- a/core/src/main/scala/munit/CatsEffectSuite.scala +++ b/core/src/main/scala/munit/CatsEffectSuite.scala @@ -37,7 +37,7 @@ abstract class CatsEffectSuite override implicit def munitExecutionContext: ExecutionContext = munitIORuntime.compute /** The timeout for [[cats.effect.IO IO]]-based tests. When it expires it will gracefully cancel - * the fiber running the test and invoke any finalizers. + * the fiber running the test and invoke any finalizers before ultimately failing the test. * * Note that the fiber may still hang while running finalizers or even be uncancelable. In this * case the [[munitTimeout]] will take effect, with the caveat that the hanging fiber will be @@ -49,9 +49,9 @@ abstract class CatsEffectSuite * [[scala.concurrent.Future Future]] or synchronous code. This is implemented by the MUnit * framework itself. * - * When this timeout expires, the suite will proceed without waiting for cancelation of the test - * or even attempting to cancel it. For that reason it is recommended to set this to a greater - * value than [[munitIOTimeout]], which performs graceful cancelation of + * When this timeout expires, the suite will immediately fail the test and proceed without + * waiting for its cancelation or even attempting to cancel it. For that reason it is recommended + * to set this to a greater value than [[munitIOTimeout]], which performs graceful cancelation of * [[cats.effect.IO IO]]-based tests. The default grace period for cancelation is 1 second. */ override def munitTimeout: Duration = munitIOTimeout + 1.second