|
| 1 | +/* |
| 2 | + * Copyright 2021 Typelevel |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package munit.internal |
| 18 | + |
| 19 | +import cats.effect.{IO, SyncIO} |
| 20 | +import cats.syntax.all._ |
| 21 | +import scala.concurrent.Future |
| 22 | + |
| 23 | +private[munit] object NestingChecks { |
| 24 | + // MUnit works by automatically chaining value transforms of shape `Any => Future[Any]`, |
| 25 | + // and we rely on this behavior to chain our `IO ~> Future` transform into the rest of MUnit. |
| 26 | + // |
| 27 | + // Unfortunately, this has an unforeseen consequence in CatsEffectSuite: |
| 28 | + // if you return `IO[IO[A]]` by accident, for example by using `map` instead of `flatMap`, |
| 29 | + // MUnit will execute both the inner and outer `IO` by applying our `IO` transform twice. |
| 30 | + // |
| 31 | + // This breaks the `IO` mental model, and can lead to very surprising behaviour, e.g see: |
| 32 | + // https://github.com/typelevel/munit-cats-effect/issues/159. |
| 33 | + // |
| 34 | + // This method checks for such a case, and fails the test with an actionable message. |
| 35 | + def checkNestingIO(fa: IO[_]): IO[Any] = { |
| 36 | + def err(msg: String) = IO.raiseError[Any](new Exception(msg)) |
| 37 | + |
| 38 | + fa.flatMap { |
| 39 | + case _: IO[_] => |
| 40 | + err( |
| 41 | + "your test returns an `IO[IO[_]]`, which means the inner `IO` will not execute." ++ |
| 42 | + " Call `.flatten` if you want it to execute, or `.void` if you want to discard it" |
| 43 | + ) |
| 44 | + case _: SyncIO[_] => |
| 45 | + err( |
| 46 | + "your test returns an `IO[SyncIO[_]]`, which means the inner `SyncIO` will not execute." ++ |
| 47 | + " Call `.flatMap(_.to[IO]) if you want it to execute, or `.void` if you want to discard it" |
| 48 | + ) |
| 49 | + case _: Future[_] => |
| 50 | + err( |
| 51 | + "your test returns an `IO[Future[_]]`, which means the inner `Future` might not execute." ++ |
| 52 | + " Surround it with `IO.fromFuture` if you want it to execute, or call `.void` if you want to discard it" |
| 53 | + ) |
| 54 | + case v => v.pure[IO] |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + // same as above, but for SyncIO |
| 59 | + def checkNestingSyncIO(fa: SyncIO[_]): SyncIO[Any] = { |
| 60 | + def err(msg: String) = SyncIO.raiseError[Any](new Exception(msg)) |
| 61 | + |
| 62 | + fa.flatMap { |
| 63 | + case _: IO[_] => |
| 64 | + err( |
| 65 | + "your test returns a `SyncIO[IO[_]]`, which means the inner `IO` will not execute." ++ |
| 66 | + " Call `.to[IO].flatten` if you want it to execute, or `.void` if you want to discard it" |
| 67 | + ) |
| 68 | + case _: SyncIO[_] => |
| 69 | + err( |
| 70 | + "your test returns a `SyncIO[SyncIO[_]]`, which means the inner `SyncIO` will not execute." ++ |
| 71 | + " Call `.flatten` if you want it to execute, or `.void` if you want to discard it" |
| 72 | + ) |
| 73 | + case _: Future[_] => |
| 74 | + err( |
| 75 | + "your test returns a `SyncIO[Future[_]]`, which means the inner `Future` might not execute." ++ |
| 76 | + " Change it to `IO.fromFuture(yourTest.to[IO])` if you want it to execute, or call `.void` if you want to discard it" |
| 77 | + ) |
| 78 | + case v => v.pure[SyncIO] |
| 79 | + } |
| 80 | + } |
| 81 | +} |
0 commit comments