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

Implement test timeouts #233

Merged
merged 3 commits into from
Sep 7, 2022
Merged

Conversation

armanbilge
Copy link
Member

FTR, MUnit already supports timeouts, but it comes with caveats:

  1. It doesn't .cancel the IO, so finalizers are not run. Indeed, the IO doesn't even stop running, the test suite just continues!
  2. It doesn't work on Scala Native, or even Scala.js (?) (see Update fs2-core, fs2-io to 3.2.13 in series/0.23 http4s/http4s#6652 (comment)).

By implementing first-class support for timeouts in munit-cats-effect we can solve both of these problems.

Comment on lines -31 to +35
implicit def munitIoRuntime: IORuntime = IORuntime.global
@deprecated("Use munitIORuntime", "2.0.0")
def munitIoRuntime: IORuntime = IORuntime.global
implicit def munitIORuntime: IORuntime = munitIoRuntime: @nowarn
Copy link
Member Author

Choose a reason for hiding this comment

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

This wasn't necessary, but seemed better for consistency.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for being grumpy, but I don't quite get these changes. Could you please elaborate on what we're about to achieve?

Copy link
Member Author

Choose a reason for hiding this comment

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

No problem, this one is maybe controversial :) I was just feeling grumpy about munitIoRuntime instead of munitIORuntime, it was an eyesore for me.

Especially now that I add munitIOTimeout. Maybe it should be munitIoTimeout instead?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, Geez, I didn't recognize these IO -> Io changes. Shame on me 🙈. Although, I'm happy with the IO suffixes because we're referring to cats.effect.IO.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, right, I prefer IO too, so I changed from Io -> IO :)

Comment on lines 39 to 42
def munitIOTimeout: Duration = 30.seconds

// buys us a 1s window to cancel the IO, before munit cancels the Future
override def munitTimeout: Duration = munitIOTimeout + 1.second
Copy link
Member Author

Choose a reason for hiding this comment

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

Bit of trickiness here: an IO may still hang on cancellation, for various reasons. So we do still want the munit timeout in effect too, as a backup. 1 second seemed like a reasonable default to wait for cancellation before taking more drastic measures.

Copy link
Member

Choose a reason for hiding this comment

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

It's a great note! I'd love to move it to scaladoc of munitTimeout. Also, it could be worth noting that IO can be uncancelable in its nature.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! Good point, let me add some scaladocs.

Comment on lines +53 to +60
// TODO cleanup after CE 3.4.0 is released
val fd = Some(munitIOTimeout).collect { case fd: FiniteDuration => fd }
val timedIO = fd.fold(unnestedIO) { duration =>
unnestedIO.timeoutTo(
duration,
IO.raiseError(new TimeoutException(s"test timed out after $duration"))
)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

In CE 3.4.0 we'll be able to directly pass a non-finite Duration to timeoutTo.

Copy link
Member

@valencik valencik left a comment

Choose a reason for hiding this comment

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

Nice how small of an addition this is.

I wonder if this behaviour is worth documenting somewhere? (not necessarily in this PR)

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.

Excellent find, @armanbilge.

Comment on lines -31 to +35
implicit def munitIoRuntime: IORuntime = IORuntime.global
@deprecated("Use munitIORuntime", "2.0.0")
def munitIoRuntime: IORuntime = IORuntime.global
implicit def munitIORuntime: IORuntime = munitIoRuntime: @nowarn
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for being grumpy, but I don't quite get these changes. Could you please elaborate on what we're about to achieve?

override def munitIOTimeout = 100.millis
override def munitTimeout = Int.MaxValue.nanos // so only our timeout is in effect

test("times out".fail) { IO.sleep(1.second) }
Copy link
Member

Choose a reason for hiding this comment

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

Probably we could intercept here the timeout exception to make the test suite more precise. WDYT?

Copy link
Member Author

@armanbilge armanbilge Sep 7, 2022

Choose a reason for hiding this comment

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

Oh sorry, I missed this comment. Actually this doesn't work: the .intercept would apply only to the IO.sleep(1.second), which does not throw an exception. The timeout exception is being raised outside of the user-written test, so there is no way we can intercept it in a user-written test.

Comment on lines 39 to 42
def munitIOTimeout: Duration = 30.seconds

// buys us a 1s window to cancel the IO, before munit cancels the Future
override def munitTimeout: Duration = munitIOTimeout + 1.second
Copy link
Member

Choose a reason for hiding this comment

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

It's a great note! I'd love to move it to scaladoc of munitTimeout. Also, it could be worth noting that IO can be uncancelable in its nature.

Comment on lines 39 to 57
/** The timeout for [[cats.effect.IO IO]]-based tests. When it expires it will gracefully cancel
* the fiber running the test and invoke any finalizers.
*
* Note that the fiber may still hang while running finalizers or even be uncancelable. In this
* case the [[munitTimeout]] will take effect, with the caveat that the hanging fiber will be
* leaked.
*/
def munitIOTimeout: Duration = 30.seconds

/** The overall timeout applicable to all tests in the suite, including those written in terms of
* [[scala.concurrent.Future Future]] or synchronous code. This is implemented by the MUnit
* framework itself.
*
* When this timeout expires, the suite will proceed without waiting for cancelation of the test
* or even attempting to cancel it. For that reason it is recommended to set this to a greater
* value than [[munitIOTimeout]], which performs graceful cancelation of
* [[cats.effect.IO IO]]-based tests. The default grace period for cancelation is 1 second.
*/
override def munitTimeout: Duration = munitIOTimeout + 1.second
Copy link
Member Author

Choose a reason for hiding this comment

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

@danicheg wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

This is outstanding 👍🏻

@armanbilge armanbilge merged commit 7588d4d into typelevel:main Sep 7, 2022
mzuehlke added a commit to mzuehlke/munit-cats-effect that referenced this pull request May 24, 2024
CE 3.4 no longer requires to convert `munitIOTimeout` into a `FiniteDuration`

See: typelevel#233
@mzuehlke mzuehlke mentioned this pull request May 24, 2024
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.

3 participants