-
Notifications
You must be signed in to change notification settings - Fork 91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cats-effect support for FunSuite and FunFixtures #134
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package munit | ||
|
||
import cats.effect.{IO, Resource} | ||
import cats.syntax.flatMap._ | ||
|
||
import scala.concurrent.Promise | ||
|
||
trait CatsEffectFunFixtures extends FunFixtures { self: CatsEffectSuite => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if we could somehow support suite-local and test-local resources via I am concerned it would have to be a breaking change unless we introduce something like I personally prefer This doesn't have to be blocking for this PR, I'm just thinking out loud here. We could go ahead and merge this change for now and add |
||
|
||
object CatsEffectFixture { | ||
|
||
def fromResource[T]( | ||
resource: Resource[IO, T] | ||
): FunFixture[T] = fromResource( | ||
resource, | ||
(_, _) => IO.pure(()), | ||
_ => IO.pure(()) | ||
) | ||
|
||
def fromResource[T]( | ||
resource: Resource[IO, T], | ||
setup: (TestOptions, T) => IO[Unit], | ||
teardown: T => IO[Unit] | ||
): FunFixture[T] = { | ||
val promise = Promise[IO[Unit]]() | ||
|
||
FunFixture.async( | ||
setup = { testOptions => | ||
implicit val ec = munitExecutionContext | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK/expected that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any issue with the parasitic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, as it turns out, this causes deadlock so you are forced to override it. It also doesn't make for a great default imho (at least not for cats-effect), because the failure mode is "oh no there is a deadlock", and you don't expect it to come from your test framework so it can be baffling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SystemFw Could you please open an issue here? https://github.com/typelevel/munit-cats-effect/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's where I'm coming from typelevel/munit-cats-effect#65 I don't expect munit to take any action at this point (I would have opened a PR), but I thought it was interesting to share if someone else comes across the discussion :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, disregard my comment then :) |
||
|
||
val resourceEffect = resource.allocated | ||
val setupEffect = | ||
resourceEffect | ||
.map { | ||
case (t, release) => | ||
promise.success(release) | ||
t | ||
} | ||
.flatTap(t => setup(testOptions, t)) | ||
|
||
setupEffect.unsafeToFuture() | ||
}, | ||
teardown = { argument: T => | ||
implicit val cs = munitContextShift | ||
|
||
teardown(argument) | ||
.flatMap(_ => IO.fromFuture(IO(promise.future)).flatten) | ||
.unsafeToFuture() | ||
} | ||
) | ||
} | ||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package munit | ||
|
||
import cats.effect.{ContextShift, IO, Timer} | ||
|
||
abstract class CatsEffectSuite extends FunSuite { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still have some open questions on this before I can continue :) |
||
|
||
def munitContextShift: ContextShift[IO] = | ||
IO.contextShift(munitExecutionContext) | ||
|
||
def munitTimer: Timer[IO] = | ||
IO.timer(munitExecutionContext) | ||
|
||
override def munitValueTransforms: List[ValueTransform] = | ||
super.munitValueTransforms ++ List(munitIOTransform) | ||
|
||
final def munitIOTransform: ValueTransform = | ||
new ValueTransform("IO", { case e: IO[_] => e.unsafeToFuture() }) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package munit | ||
|
||
import cats.effect.{IO, Resource} | ||
|
||
import scala.concurrent.{Future, Promise} | ||
|
||
class IOFixtureSuite extends CatsEffectSuite with CatsEffectFunFixtures { | ||
val latch: Promise[Unit] = Promise[Unit] | ||
var completedFromTest: Option[Boolean] = None | ||
var completedFromTeardown: Option[Boolean] = None | ||
|
||
var completedFromResourceAcquire: Option[Boolean] = None | ||
var completedFromResourceRelease: Option[Boolean] = None | ||
|
||
val latchOnTeardown: FunFixture[String] = | ||
CatsEffectFixture.fromResource[String]( | ||
resource = Resource.make[IO, String]( | ||
IO { | ||
completedFromResourceAcquire = Some(true) | ||
"test" | ||
} | ||
)(_ => | ||
IO { | ||
completedFromResourceRelease = Some(true) | ||
} | ||
), | ||
setup = { (_: TestOptions, _: String) => | ||
IO { | ||
completedFromResourceAcquire = Some(false) | ||
} | ||
}, | ||
teardown = { _: String => | ||
IO { | ||
completedFromResourceRelease = Some(false) | ||
completedFromTeardown = Some(latch.trySuccess(())); | ||
} | ||
} | ||
) | ||
|
||
override def afterAll(): Unit = { | ||
// resource was created before setup | ||
assertEquals(completedFromResourceAcquire, Some(false)) | ||
// resource was released after teardown | ||
assertEquals(completedFromResourceRelease, Some(true)) | ||
// promise was completed first by the test | ||
assertEquals(completedFromTest, Some(true)) | ||
// and then there was a completion attempt by the teardown | ||
assertEquals(completedFromTeardown, Some(false)) | ||
} | ||
|
||
latchOnTeardown.test("teardown runs only after test completes") { _ => | ||
import scala.concurrent.ExecutionContext.Implicits.global | ||
Future { | ||
// Simulate some work here, which increases the certainty that this test | ||
// will fail by design and not by lucky scheduling if the happens-before | ||
// relationship between the test and teardown is removed. | ||
Thread.sleep(50) | ||
completedFromTest = Some(latch.trySuccess(())) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package munit | ||
|
||
import cats.effect.IO | ||
|
||
import scala.concurrent.duration._ | ||
|
||
class IOSuite extends CatsEffectSuite { | ||
|
||
test("nested fail".fail) { | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.flatMap { _ => | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.flatMap { _ => | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.map(_ => assertEquals(false, true)) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
test("nested success") { | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.flatMap { _ => | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.flatMap { _ => | ||
IO { | ||
IO.sleep(2.millis)(munitTimer) | ||
.map(_ => assertEquals(true, true)) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep the tests project unchanged and introduce instead new source directories like
src/test/non-native
for the parts of the cross-build that cats-effect supports. Use.jvmConfigure(_.dependsOn(munitCatsEffect)).jsConfigure(_.dependsOn(munitCatsEffect))
to skip the native dependencyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also fine to have a
src/test/cats-effect
directory if that simplifies things. I want to avoid multiple test projects because it's easiest to reason about the build when you can runtestsJVM/test
to run all tests.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I completely understand what you mean :)
I currently have created a new directory
munit-cats-effect
at the root of the project.Would you like to have a new directory in the
tests
folder? Or where do you see this newnon-native
source directory in the project?