Repository navigation
Fix propagation of ExitCase in Resource#{both,combineK}
#3307
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
Changes from all commits
8d70133
1a37a02
d82fba1
605dccf
d2d27d6
9e8d092
484a984
d1f0397
be51ea6
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 |
|---|---|---|
|
|
@@ -598,6 +598,84 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { | |
| leftReleased must beTrue | ||
| rightReleased must beTrue | ||
| } | ||
|
|
||
| "propagate the exit case" in { | ||
| import Resource.ExitCase | ||
|
|
||
| "use succesfully, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.both(Resource.unit).use(_ => IO.unit) must completeAs(()) | ||
| got mustEqual ExitCase.Succeeded | ||
| } | ||
|
|
||
| "use successfully, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.unit.both(r).use(_ => IO.unit) must completeAs(()) | ||
| got mustEqual ExitCase.Succeeded | ||
| } | ||
|
|
||
| "use errored, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.both(Resource.unit).use(_ => IO.raiseError(ex)) must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "use errored, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.unit.both(r).use(_ => IO.raiseError(ex)) must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "right errored, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.both(Resource.eval(IO.sleep(1.second) *> IO.raiseError(ex))).use_ must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "left errored, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.eval(IO.sleep(1.second) *> IO.raiseError(ex)).both(r).use_ must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "use canceled, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.both(Resource.unit).use(_ => IO.canceled) must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
|
|
||
| "use canceled, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.unit.both(r).use(_ => IO.canceled) must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
|
|
||
| "right canceled, test left" >> ticked { implicit ticker => | ||
|
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. What would happen if right was canceled and left errored out (and vice versa)? Would they both get
Member
Author
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.
Depends which happens first. If right cancels first, it's covered by this test. If left errors first it's covered by "left errored, test right" |
||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.both(Resource.eval(IO.sleep(1.second) *> IO.canceled)).use_ must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
|
|
||
| "left canceled, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.eval(IO.sleep(1.second) *> IO.canceled).both(r).use_ must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
| } | ||
| } | ||
|
|
||
| "releases both resources on combineK" in ticked { implicit ticker => | ||
|
|
@@ -642,6 +720,70 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { | |
| lhs eqv rhs | ||
| } | ||
| } | ||
|
|
||
| "propagate the exit case" in { | ||
| import Resource.ExitCase | ||
|
|
||
| "use succesfully, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.combineK(Resource.unit).use(_ => IO.unit) must completeAs(()) | ||
| got mustEqual ExitCase.Succeeded | ||
| } | ||
|
|
||
| "use errored, test left" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.combineK(Resource.unit).use(_ => IO.raiseError(ex)) must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "left errored, test left" >> ticked { implicit ticker => | ||
|
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. Would it make sense to add tests for "left canceled, test left/right" too? 🙂
Member
Author
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. If left cancels, no resources are acquired, so no finalizers are run. |
||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) *> | ||
| Resource.eval(IO.raiseError(ex)) | ||
| r.combineK(Resource.unit).use_ must completeAs(()) | ||
| got mustEqual ExitCase.Succeeded | ||
| } | ||
|
Comment on lines
+742
to
+749
Member
Author
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. This semantic is kind of counter-intuitive. Even though the left This is a by-product of the fact that the left 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. After mulling this over I think it makes sense and is expected, although I could be wrong (I went back and forth on this in my mind a couple times before settling on the conclusion). It would be consistent with what
Conversely, The fact that I hope it makes sense. In any case, whichever semantic will be introduced, it could be a good idea to document it in the scaladoc, especially if it seems to be counter-intuitive. 🙂 However, I'm not sure if any sort of consistency between 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.
One more thought regarding this: one could say that in the case of
Member
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 feel like this is somewhat similar to the following: case object TestException extends RuntimeException
Resource.raiseError[IO, Unit, Throwable](TestException)
.onFinalizeCase(IO.println(_))
.voidErrorDo we print out
Member
Author
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. The following program doesn't print. //> using lib "org.typelevel::cats-effect::3.4.2"
import cats.effect._
import cats.syntax.all._
object App extends App {
case object TestException extends RuntimeException
def run = Resource
.raiseError[IO, Unit, Throwable](TestException)
.onFinalizeCase(IO.println(_))
.voidError
.use_
}
Member
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 prints for me? (Resource.unit.onFinalizeCase(IO.println(_)) *> Resource.raiseError[IO, Unit, Throwable](TestException)).voidError.use_Prints (Resource.unit.onFinalizeCase(IO.println(_)) *> Resource.raiseError[IO, Unit, Throwable](TestException)).use_ Prints I think |
||
|
|
||
| "left errored, test right" >> ticked { implicit ticker => | ||
|
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. In case one of the Resources errors out during acqusition and the other one self-cancels, will the
Member
Author
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. If a resource errors out or cancels during acquisition, then it has no exit case because the finalizer never runs. |
||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource.eval(IO.raiseError(ex)).combineK(r).use_ must completeAs(()) | ||
| got mustEqual ExitCase.Succeeded | ||
| } | ||
|
|
||
| "left errored, use errored, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val ex = new Exception | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource | ||
| .eval(IO.raiseError(new Exception)) | ||
| .combineK(r) | ||
| .use(_ => IO.raiseError(ex)) must failAs(ex) | ||
| got mustEqual ExitCase.Errored(ex) | ||
| } | ||
|
|
||
| "use canceled, test left" >> ticked { implicit ticker => | ||
|
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. Would it make sense to add cases for "use errored/canceled, test right" (for when left Resource fails to acquire, but right one succedes)? 🙂
Member
Author
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. Yes it would! |
||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| r.combineK(Resource.unit).use(_ => IO.canceled) must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
|
|
||
| "left errored, use canceled, test right" >> ticked { implicit ticker => | ||
| var got: ExitCase = null | ||
| val r = Resource.onFinalizeCase(ec => IO { got = ec }) | ||
| Resource | ||
| .eval(IO.raiseError(new Exception)) | ||
| .combineK(r) | ||
| .use(_ => IO.canceled) must selfCancel | ||
| got mustEqual ExitCase.Canceled | ||
| } | ||
| } | ||
| } | ||
|
|
||
| "surround" should { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
Would it be a good idea to describe exit case propagation semantics in the scaladoc for the
Resource#both? 🙂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.
Yes, even in two weeks I've forgotten what it was 😂