Skip to content

Commit

Permalink
Specilized API for IO (#7)
Browse files Browse the repository at this point in the history
* Specilized API for IO

* Use IOCatch.apply instead of ioCatching. Revert rename of ioCatch (kept the val)

* Add headers

* Update uncancelable test to handle race-y condition

* fmt

* headers for test file

* fix warnings
  • Loading branch information
jatcwang authored Jul 3, 2024
1 parent 302429a commit 8cc63d7
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 1 deletion.
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/catcheffect/Catch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ object Catch {
|The handler was defined at $alloc""".stripMargin
}

def ioCatch: IO[Catch[IO]] =
val ioCatch: IO[Catch[IO]] =
LocalForIOLocal
.localForIOLocalDefault(Vault.empty)
.map(implicit loc => fromLocal[IO])
Expand Down
34 changes: 34 additions & 0 deletions modules/core/src/main/scala/catcheffect/IOCatch.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 Valdemar Grange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package catcheffect

import Catch.ioCatch
import cats.effect.*

object IOCatch {

def apply[E]: IOCatchPartiallyApplied[E] =
new IOCatchPartiallyApplied[E]

class IOCatchPartiallyApplied[E] {
def apply[A](f: Handle[IO, E] => IO[A]): IO[Either[E, A]] = {
ioCatch.flatMap { c =>
c.use[E](f)
}
}
}

}
81 changes: 81 additions & 0 deletions modules/core/src/test/scala/catcheffect/IOCatchTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024 Valdemar Grange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package catcheffect

import catcheffect.Catch.{RaisedInUncancellable, RaisedWithoutHandler}
import cats.effect.*
import munit.CatsEffectSuite

class IOCatchTest extends CatsEffectSuite {
test("should abort execution as soon as raise is called") {
for {
ref <- Ref[IO].of("init")
res <- IOCatch[String](h =>
for {
_ <- h.raise("error").void
_ <- ref.set("should not be set")
} yield 1
)
_ = assertEquals(res, Left("error"))
_ <- assertIO(ref.get, "init")
} yield ()
}

test("can nest correctly") {
for {
res <- IOCatch[String](hs1 =>
for {
_ <- IOCatch[String](_ => hs1.raise("1"))
_ <- IO.raiseError(new AssertionError("should not reach this point")).void
} yield ()
)
_ = assertEquals(res, Left("1"))
} yield res
}

test("Raising in an uncancellable region may correctly raise the error or throw RaisedInUncancellable") {
// There's a small race condition where cancellation from outside will win against the IO.cancel call,
// therefore RaisedInUncancellable isn't thrown. Unfortunately it doesn't seem like we can detect
// whether the current scope is cancellable without actually triggering cancellation
(for {
res <- IOCatch[String](h => IO.uncancelable(_ => h.raise("oops").void))
// If it did raise error instead of throwing RaisedInUncancellable, at least make sure it's raised correctly
_ = assertEquals(res, Left("oops"))
} yield ()).recoverWith {
case _: RaisedInUncancellable[_] => IO.unit // succeed
case e => IO.raiseError(e)
}
}

test("Can raise in an cancellable region when polled") {
for {
res <- IOCatch[String](h => IO.uncancelable(poll => poll(h.raise("oops"))))
_ = assertEquals(res, Left("oops"))
} yield ()
}

test("Fail to raise if the Raise instance is used outside of its original fiber") {
(for {
cached <- Deferred[IO, Raise[IO, String]]
res <- IOCatch[String](h => cached.complete(h).void)
_ = assertEquals(res, Right(()))
_ <- cached.get.flatMap(_.raise("hm")).void
} yield ()).attempt.map {
case Left(_: RaisedWithoutHandler[_]) => () // succeed
case other => fail(s"Expected RaisedWithoutHandler, got $other")
}
}
}

0 comments on commit 8cc63d7

Please sign in to comment.