Skip to content
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

Cross-platform suite-local fixtures #104

Merged
merged 6 commits into from
Sep 16, 2021

Conversation

armanbilge
Copy link
Member

@armanbilge armanbilge commented Jul 11, 2021

This PR introduces a ResourceSuiteLocalDeferredFixture[A]. It has identical semantics to JVM-only ResourceSuiteLocalFixture[A] (i.e., allocate once per suite) but instead of providing A provides an IO[A] that is semantically blocked until the resource is allocated. This allows the fixture to be used on both JVM and JS.


override def afterAll(): Unit = {
assertEquals(acquired, 1)
// assertEquals(released, 1) // Release is async, no way to check?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is really my only question here. since there's no way to enforce backpressure between the release of the suite-local fixture and the acquisition of the next, you could run into some weird interactions between different suites. for example, if your fixture is a network server that binds to the same port. suite A might not release its port before the suite B attempts to acquire

I think this is still really useful for javascript to have, so this may just need to be a tradeoff that we warn users about

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is still really useful for javascript to have, so this may just need to be a tradeoff that we warn users about

I'm mindful of this issue too, but exactly as you said. For plenty of non-interacting resources should be fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's particularly evil, but this problem could be solved via some sort of global semaphore right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like it should work, but it would inhibit parallel suite execution (only the suites which use this feature) for users who opt in. which again may be fine and just something we warn users about

the fixtures do have names, so ideally you could split the global semaphore by name, but that relies on the assumption that interacting resources have the same name

Copy link
Member Author

@armanbilge armanbilge Jul 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but on JS we don't have parallel execution anyway. So maybe the trick is to use this implementation for JS (plus some sort of global semaphore), your blocking implementation for JVM, and just provide them under the same interface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should call it UnsafeResourceSuiteLocalFixture to emphasize these hazards.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im still a bit confused :)

We have a working ResourceSuiteLocalFixture for the JVM only. As far as I understand, you want to add similar functionality but for JS?

I don't really know anything about ScalaJS and how to create libraries for it, but I don't get why we need to create something that works on both the JVM and JS?
Because, this shared logic on the JVM, doesn't really work as one would expect. And since we already have something that works the JVM, why add something shared?

So in my mind, that has no JS knowledge, can this logic simply be added to the JS part of this library? Or is there some value in having something shared?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions!

Or is there some value in having something shared

Yes, absolutely. If it is not shared, then if you want to test your code on both JVM and JS you'd have to write your tests twice, separately for each platform. For a concrete example of this consider the http4s test suite which now needs to support both JVM and JS platforms where in fact I've already been using this construct:
https://github.com/http4s/http4s/blob/main/testing/shared/src/test/scala/org/http4s/Http4sSuite.scala#L68

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I would indeed go for the UnsafeResourceSuiteLocalFixture :)

Does the CatsEffectFunFixtures not give you the functionality you desire?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CatsEffectFunFixtures does work on JVM and JS, but it allocates/tears down the resource once per test. The SuiteLocal fixtures allocate/tear down once per suite.

@armanbilge
Copy link
Member Author

@danicheg would you have a moment to review this please? I'd find this incredibly useful.

@armanbilge
Copy link
Member Author

@milanvdm @danicheg thank you both for your thoughtful reviews, I've pushed some changes which hopefully address both of your concerns.

  1. Renamed ResourceSuiteLocalDeferredFixture to UnsafeResourceSuiteLocalDeferredFixture. Furthermore, added a detailed comment explaining its semantics and why it is unsafe.
  2. Platformed the implementation for JVM and JS, so that on JVM it uses unsafeRunSync() and only uses unsafeRunAndForget() for JS. TBH I'm not convinced this really buys us anything since the caveats still apply to JS platform, but at least it may make things a bit better on JVM.

Let me know what you think!

Copy link
Member

@danicheg danicheg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You did a huge job @armanbilge 👍🏻
Sorry, but I was out from your discussion with @milanvdm at some point.
But as I get the root problem is that ResourceSuiteLocalFixture currently not working on JS correctly. And adding the ResourceSuiteLocalDeferredFixture would mean that ResourceSuiteLocalFixture will still not working on JS.
Maybe it's possible to change ResourceSuiteLocalFixture to have a working version both on JVM and JS? For example, we could put implement details of munit.CatsEffectFixtures.ResourceSuiteLocalFixture#apply for JVM and JS separately.
So we would have one suite to work with resource fixtures both on JVM and JS.
What do you folks think?

@armanbilge
Copy link
Member Author

armanbilge commented Sep 15, 2021

@danicheg Unfortunately, it is not possible to implement ResourceSuiteLocalFixture fixture on JS, even if we implement it separately. The reason is because a Resource[IO, T] allocates a T asynchronously but the fixture setup must run synchronounously. The reason this works on JVM is because you can use unsafeRunSync() to wait for T: https://github.com/typelevel/munit-cats-effect/blob/main/common/jvm/src/main/scala/munit/CatsEffectFixtures.scala#L41

On JS, there is only one thread and therefore no unsafeRunSync() available. So, it is impossible to allocate a T synchronously. So, the trick I used here, is instead of providing a T, to provide an IO[T] that is backed by a Deferred[IO, T]. This is possible, because although you cannot allocate T synchronously, you can allocate Deferred[IO, T] synchronously and then provide an IO[T] that will semantically block until the deferred is set with the allocated T.

Notice that this requires making a source-incompatible change to the signature. Which is obviously not an impossible thing to do, but it would be painful for any downstream currently using ResourceSuiteLocalFixture.

If we really want to fix all this craziness: would require a PR to munit itself to add support for asynchronous fixtures. That would solve all the problems discussed here. See scalameta/munit#186. If you are interested in solving that issue, that would be really amazing!! Especially since so many Typelevel projects are using both CE and munit.

@danicheg
Copy link
Member

@armanbilge thanks for the detailed answer. What about using cats.effect.IO#unsafeRunAsync instead of unsafeRunSync for JS? I've sketched the implementation of ResourceSuiteLocalFixture for JS: https://gist.github.com/danicheg/be9ed79cd6f5764899e7cc16fe23e48c
That seems semantic is the same as for ResourceSuiteLocalDeferredFixture. Or maybe I missed something.

@armanbilge
Copy link
Member Author

Cool, thanks for giving it a try!! Have you tried running it against the test suite? 😉

My concern is that there is no guarantee when unsafeRunAsync will complete. So, a test may start while value is still None.

@armanbilge
Copy link
Member Author

Also, you might find the implementation of unsafeRunAndForget interesting :)
https://github.com/typelevel/cats-effect/blob/series/3.x/core/shared/src/main/scala/cats/effect/IO.scala#L709

@danicheg
Copy link
Member

@armanbilge yep, the tests were passed ._. But I'm not sure if this is legal though.

@armanbilge
Copy link
Member Author

Sounds like the test suite is not rigorous enough! 😬 We should fix that. For example, try using your fixture to allocate this resource: Resource.eval(IO.pure("hello").delayBy(1.second)).

@danicheg
Copy link
Member

danicheg commented Sep 15, 2021

@armanbilge sorry, I've double-checked it and tests didn't run at all. To be honest, they are not running and for the current version of ResourceSuiteLocalFixture for JS 🤷🏻

sbt:munit-cats-effect-3> testOnly munit.CatsEffectFixturesSpec
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for Test / testOnly

@armanbilge
Copy link
Member Author

That's probably because ResourceSuiteLocalFixture is in the JVM-only source directory?

@danicheg
Copy link
Member

@armanbilge yep, it is. So, version with unsafeRunAsync fails on the same case of afterAll as version on Deffered - assertEquals(released, 1). But adding delay has lead to instantiation problems. So, you are definitely right.

@armanbilge
Copy link
Member Author

@danicheg thanks for looking into that. So, it definitely sounds like the current test suite is not sufficient then. I'll push a fix for that.

@armanbilge
Copy link
Member Author

@danicheg I added an IO.sleep to the fixture tests so that they properly test for asynchronous fixture allocation. Should be good to go!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants