-
Notifications
You must be signed in to change notification settings - Fork 526
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
Added TestControl
, with first-class support for time mocking
#2276
Added TestControl
, with first-class support for time mocking
#2276
Conversation
Published as |
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.
Very excited. Marking this as request changes but the changes are pretty minor.
Regarding the binary compatibility, because of the change in behavior, at least to me, the whole situation seems to give a false sense of security that this could be a drop in replacement.
Is there a way to make the private[testkit]
implementations delegate to the new implementations in a way that is semantically correct in the new model? For example, do the tick()
then advanceAndTick()
trick or something.
If the answer is no, maybe it would indeed be better to not guarantee binary compatibility at all and just advise people to upgrade, recompile, fix source changes and change to the new semantics at the same time. Of course, you're much more experienced than me in this regard, so I won't fight any final decision.
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/TestContext.scala
Outdated
Show resolved
Hide resolved
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/TestContext.scala
Show resolved
Hide resolved
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/TestContext.scala
Show resolved
Hide resolved
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/TestContext.scala
Outdated
Show resolved
Hide resolved
testkit/shared/src/main/scala/cats/effect/testkit/TestControl.scala
Outdated
Show resolved
Hide resolved
Noodled a bit on some Specs2 support built on top of this API: typelevel/cats-effect-testing#193 "execute a simple test with mock time" in {
var first = false
var second = false
var third = false
val wait = IO.sleep(1.hour)
val program = for {
_ <- IO { first = true }
_ <- wait
_ <- IO { second = true }
_ <- wait
_ <- IO { third = true }
} yield 42
program must execute { (control, result) =>
result() must beNone
first must beFalse
second must beFalse
third must beFalse
control.tick()
first must beTrue
second must beFalse
third must beFalse
control.advanceAndTick(1.hour)
first must beTrue
second must beTrue
third must beFalse
control.advanceAndTick(1.hour)
first must beTrue
second must beTrue
third must beTrue
result() must beSome(beRight(42))
}
} |
… spin as aggressively
* where the scheduler ticks see different task lists despite | ||
* identical configuration. | ||
*/ | ||
def apply(seed: String): TestContext = |
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.
Inclusion of a seed is a very good idea 👍
testkit/shared/src/main/scala/cats/effect/testkit/TestControl.scala
Outdated
Show resolved
Hide resolved
* rely on `realTime` and `monotonic`, either directly on `IO` or via the | ||
* typeclass abstractions. | ||
* | ||
* WARNING: ''Never'' use this runtime on programs which use the [[IO#evalOn]] |
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.
I think it's worth adding a sentence about behaviour of blocking
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.
Good call. To more directly answer, it behaves like JavaScript: the blocking pool and the compute pool are both TestContext.
testkit/shared/src/main/scala/cats/effect/testkit/TestControl.scala
Outdated
Show resolved
Hide resolved
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/TestContext.scala
Show resolved
Hide resolved
Bumping this a bit, @SystemFw. Any follow-ups needed here? Should we look at the munit integration first? |
Yeah munit integration would be good, and tbh I think it'd be great to have a snapshot to try it out. I'm happy with it though, I hadn't realised that wasn't clear |
Actually published a snapshot quite a while ago. 😃 That's what the Specs2 integration is based on. I'll dig into munit. |
New snapshot published as |
Odd failure:
|
Might not be an odd failure. The test uses |
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.
I am amazed by this PR and the scaladoc is very informative. Thank you for your effort. 🙏🏻
One question still remains, do people think we need to build a higher level ADT for making the different outcomes more explicit. I'm personally worried about Left(CancelationException)
not being great from an ergonomics point of view.
val test = { | ||
implicit val ticker = Ticker() | ||
|
||
val test = IO(implicitly[Arbitrary[IO[Int]]].arbitrary.sample.get).flatMap { io => | ||
IO.delay(io.eqv(io)) | ||
IO(implicitly[Arbitrary[IO[Int]]].arbitrary.sample.get).flatMap { io => | ||
IO.delay(io.eqv(io)) | ||
} |
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.
I'm pretty sure this change can be reverted.
I agree. Even |
I don't hate that. I'm not sure if Daniel considers it a solution if we only do a last-mile wrapping of the |
As long as cancellation is not surfaced as a cancellation exception, I'm happy 😉 |
Most definitely, I'm really not comfortable with that. |
Okay okay. 😛 I'll try the Option if Outcome approach… |
Well, remember that the UX here is writing assertions, and I think Outcome might actually be the worst UX, given that writing assertions that involve pattern matching is generally a bit annoying. In some ways the most immediate return type is actually P.S. I'm integrating this into |
See Discord for a long diatribe on why we replaced |
Published as |
Ok, so I've tried TL;DR: it's much better, but we should remove the Option from there as well. Note that I'm only talking
Hence, having |
I was thinking about it over the past couple days and came round to a similar opinion. I think this is probably the most compelling argument. Also I agree with the counterpoint on "bespokeness".
I think it gets even worse than that if someone writes their assertions within the So overall I think I'm in alignment. I'll make the change. |
Changes released in |
Final thoughts @SystemFw or others? |
Really excited this is pretty much done :) |
I actually really like the names. Honestly, The "Embed" suffix also works really well when comparing with the I'm bikeshedding. 😃 Names matter but probably not as much as I'm implicitly asserting here. I just rather like the current ones. |
That's ok, let's go ahead with the current ones :) |
I think this is a little stale now, and all comments have been addressed
Useless comment: just wanted to say I really like this change (i've "borrowed" it temporarily, pending a release of 3.3)! It's a huge improvement over the hand-rolling required in the past to use |
This is a really great addition!!! 🥇 |
Fixes #2220
Fixes #1678
Time mocking is back and better than ever! Yay!!! Thoughts very much welcome. Folks who have expressed an interest in this include @SystemFw, @ChristopherDavenport, @benhutchison, and many others. Now is the time to give feedback! I tried to be comprehensive in the
TestControl
scaladoc.Note that this also includes a binary-compatible (but source- and semantically-incompatible) reworking of
TestContext
. The previous API conflated too many concerns and was very error-prone. Time advancement and schedule ticking is now fully separated. Also, as a bonus, I found (and fixed!) a bug intickAll
which was causing all of our test suites to take dramatically longer than necessary, and now they run much faster both in CI and on my laptop. Drinks all around.The primary concept here is that
TestControl
represents a relatively low level mechanism for controlling the runtime. Higher level mechanisms should be built on top of this and integrated within specific downstream test frameworks (such as munit). Note that, just as before, there's still no substitute for running your program on the real runtime, and there are several programs which will deadlock onTestContext
but not on the real thing. However, for anything involvingsleep
, this kind of thing is basically indispensable.In theory this should be a lot more convenient than what we had to do in CE2 (creating artificial
Clock
instances), since the runtime is automatically threaded through at all points, meaning that you can just use the normalClock
andTemporal
instances and everything will just work out of the box. In fact, time mocking even works on the bareIO
functions (e.g.IO.sleep
,IO.monotonic
, andIO.realTime
).As a final goodie, I added support for configuring the source of randomness used in
TestContext
, similar to how ScalaCheck supports optionally setting the seed as a base64-encoded value. I would imagine that downstream test framework support will print out theseed
in any error messages, as well as provide an API for optionally configuring that seed and passing it to theTestControl
they use internally to run your examples.As I said, now is the time for feedback! This is a pretty meaningful API addition and I'd like to make sure we get it right.