Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions core/src/main/scala/munit/CatsEffectSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand Down
44 changes: 42 additions & 2 deletions core/src/main/scala/munit/internal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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." ++
Expand All @@ -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))
Expand All @@ -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." ++
Expand Down
9 changes: 9 additions & 0 deletions core/src/test/scala/munit/CatsEffectSuiteSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }
}
Loading