Skip to content

Commit

Permalink
Merge pull request #233 from armanbilge/feature/timeouts
Browse files Browse the repository at this point in the history
Implement test timeouts
  • Loading branch information
armanbilge authored Sep 7, 2022
2 parents 5e7abac + f307b51 commit 7588d4d
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 4 deletions.
45 changes: 41 additions & 4 deletions core/src/main/scala/munit/CatsEffectSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,17 +30,52 @@ 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

/** 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 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
* leaked.
*/
def munitIOTimeout: Duration = 30.seconds

/** 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 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

override def munitValueTransforms: List[ValueTransform] =
super.munitValueTransforms ++ List(munitIOTransform, munitSyncIOTransform)

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 =
Expand Down
6 changes: 6 additions & 0 deletions core/src/test/scala/munit/CatsEffectSuiteSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }
Expand Down

0 comments on commit 7588d4d

Please sign in to comment.