diff --git a/core/src/main/scala/munit/CatsEffectSuite.scala b/core/src/main/scala/munit/CatsEffectSuite.scala index d2ab97a..37f6d9b 100644 --- a/core/src/main/scala/munit/CatsEffectSuite.scala +++ b/core/src/main/scala/munit/CatsEffectSuite.scala @@ -17,12 +17,12 @@ package munit import cats.effect.unsafe.IORuntime -import cats.effect.{IO, SyncIO} +import cats.effect.{IO, ResourceIO, SyncIO} import scala.annotation.nowarn import scala.concurrent.{ExecutionContext, Future, TimeoutException} -import scala.concurrent.duration._ -import munit.internal.NestingChecks.{checkNestingIO, checkNestingSyncIO} +import scala.concurrent.duration.* +import munit.internal.NestingChecks.{checkNestingIO, checkNestingSyncIO, checkNestingResourceIO} abstract class CatsEffectSuite extends FunSuite @@ -62,7 +62,11 @@ abstract class CatsEffectSuite override def munitTimeout: Duration = munitIOTimeout + 1.second override def munitValueTransforms: List[ValueTransform] = - super.munitValueTransforms ++ List(munitIOTransform, munitSyncIOTransform) + super.munitValueTransforms ++ List( + munitIOTransform, + munitResourceIOTransform, + munitSyncIOTransform + ) private val munitIOTransform: ValueTransform = new ValueTransform( @@ -79,6 +83,21 @@ abstract class CatsEffectSuite } ) + private val munitResourceIOTransform: ValueTransform = + new ValueTransform( + "ResourceIO", + { case e: ResourceIO[_] => + val unnestedResourceIO = checkNestingResourceIO(e) + + val timedIO = unnestedResourceIO.use_.timeoutTo( + munitIOTimeout, + IO.raiseError(new TimeoutException(s"test timed out after $munitIOTimeout")) + ) + + timedIO.unsafeToFuture() + } + ) + private val munitSyncIOTransform: ValueTransform = new ValueTransform( "SyncIO", diff --git a/core/src/main/scala/munit/internal.scala b/core/src/main/scala/munit/internal.scala index fcf0ceb..017c5aa 100644 --- a/core/src/main/scala/munit/internal.scala +++ b/core/src/main/scala/munit/internal.scala @@ -16,8 +16,9 @@ package munit.internal -import cats.effect.{IO, SyncIO} -import cats.syntax.all._ +import cats.effect.{IO, ResourceIO, SyncIO} +import cats.syntax.all.* + import scala.concurrent.Future private[munit] object NestingChecks { @@ -41,6 +42,11 @@ private[munit] object NestingChecks { "your test returns an `IO[IO[_]]`, which means the inner `IO` will not execute." ++ " Call `.flatten` if you want it to execute, or `.void` if you want to discard it" ) + case _: ResourceIO[_] => + err( + "your test returns an `IO[ResourceIO[_]]`, which means the inner `ResourceIO` will not execute." ++ + " Call `.flatMap(_.use_)` if you want it to execute, or `.void` if you want to discard it" + ) case _: SyncIO[_] => err( "your test returns an `IO[SyncIO[_]]`, which means the inner `SyncIO` will not execute." ++ @@ -55,6 +61,35 @@ private[munit] object NestingChecks { } } + // same as above, but for ResourceIO + def checkNestingResourceIO(fa: ResourceIO[_]): ResourceIO[Any] = { + def err(msg: String) = IO.raiseError[Any](new Exception(msg)).toResource + + fa.flatMap { + case _: IO[_] => + err( + "your test returns a `ResourceIO[IO[_]]`, which means the inner `IO` will not execute." ++ + " Call `.flatMap(_.toResource)` if you want it to execute, or `.void` if you want to discard it" + ) + case _: ResourceIO[_] => + err( + "your test returns a `ResourceIO[ResourceIO[_]]`, which means the inner `ResourceIO` will not execute." ++ + " Call `.flatten` if you want it to execute, or `.void` if you want to discard it" + ) + case _: SyncIO[_] => + err( + "your test returns a `ResourceIO[SyncIO[_]]`, which means the inner `SyncIO` will not execute." ++ + " Call `.flatMap(_.to[IO].toResource)` if you want it to execute, or `.void` if you want to discard it" + ) + case _: Future[_] => + err( + "your test returns a `ResourceIO[Future[_]]`, which means the inner `Future` might not execute." ++ + " Change it to `_.flatMap(x => IO.fromFuture(IO.pure(x)).toResource)` if you want it to execute, or call `.void` if you want to discard it" + ) + case v => v.pure[ResourceIO] + } + } + // same as above, but for SyncIO def checkNestingSyncIO(fa: SyncIO[_]): SyncIO[Any] = { def err(msg: String) = SyncIO.raiseError[Any](new Exception(msg)) @@ -65,6 +100,11 @@ private[munit] object NestingChecks { "your test returns a `SyncIO[IO[_]]`, which means the inner `IO` will not execute." ++ " Call `.to[IO].flatten` if you want it to execute, or `.void` if you want to discard it" ) + case _: ResourceIO[_] => + err( + "your test returns an `SyncIO[ResourceIO[_]]`, which means the inner `ResourceIO` will not execute." ++ + " Call `.to[IO].flatMap(_.use_)` if you want it to execute, or `.void` if you want to discard it" + ) case _: SyncIO[_] => err( "your test returns a `SyncIO[SyncIO[_]]`, which means the inner `SyncIO` will not execute." ++ diff --git a/core/src/test/scala/munit/CatsEffectSuiteSpec.scala b/core/src/test/scala/munit/CatsEffectSuiteSpec.scala index b1c62f2..6bdb186 100644 --- a/core/src/test/scala/munit/CatsEffectSuiteSpec.scala +++ b/core/src/test/scala/munit/CatsEffectSuiteSpec.scala @@ -31,9 +31,18 @@ class CatsEffectSuiteSpec extends CatsEffectSuite { test("times out".fail) { IO.sleep(1.second) } test("nested IO fail".fail) { IO(IO(1)) } + test("nested IO and ResourceIO fail".fail) { IO(IO(1).toResource) } test("nested IO and SyncIO fail".fail) { IO(SyncIO(1)) } test("nested IO and Future fail".fail) { IO(Future.successful(1)) } + + test("evaluates ResourceIO".fail) { IO.raiseError(new RuntimeException("hello")).toResource } + test("nested ResourceIO fail".fail) { IO(IO(1).toResource).toResource } + test("nested ResourceIO and IO".fail) { IO(IO(1)).toResource } + test("nested ResourceIO and SyncIO fail".fail) { IO(SyncIO(1)).toResource } + test("nested ResourceIO and Future fail".fail) { IO(Future.successful(1)).toResource } + test("nested SyncIO fail".fail) { SyncIO(SyncIO(1)) } test("nested SyncIO and IO fail".fail) { SyncIO(IO(1)) } + test("nested SyncIO and ResourceIO fail".fail) { SyncIO(IO(1).toResource) } test("nested SyncIO and Future fail".fail) { SyncIO(Future.successful(1)) } }