From 7613376a622864269cdd18c904039c5cad8fd6fc Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Sun, 30 Jun 2024 21:07:49 +0100 Subject: [PATCH 1/7] Specilized API for IO --- .../src/main/scala/catcheffect/Catch.scala | 2 +- .../src/main/scala/catcheffect/IOCatch.scala | 20 ++++++ .../test/scala/catcheffect/CatchTest.scala | 2 +- .../test/scala/catcheffect/IOCatchTest.scala | 68 +++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 modules/core/src/main/scala/catcheffect/IOCatch.scala create mode 100644 modules/core/src/test/scala/catcheffect/IOCatchTest.scala diff --git a/modules/core/src/main/scala/catcheffect/Catch.scala b/modules/core/src/main/scala/catcheffect/Catch.scala index 9b00165..fd768f9 100644 --- a/modules/core/src/main/scala/catcheffect/Catch.scala +++ b/modules/core/src/main/scala/catcheffect/Catch.scala @@ -222,7 +222,7 @@ object Catch { |The handler was defined at $alloc""".stripMargin } - def ioCatch: IO[Catch[IO]] = + val catchForIO: IO[Catch[IO]] = LocalForIOLocal .localForIOLocalDefault(Vault.empty) .map(implicit loc => fromLocal[IO]) diff --git a/modules/core/src/main/scala/catcheffect/IOCatch.scala b/modules/core/src/main/scala/catcheffect/IOCatch.scala new file mode 100644 index 0000000..3168687 --- /dev/null +++ b/modules/core/src/main/scala/catcheffect/IOCatch.scala @@ -0,0 +1,20 @@ +package catcheffect + +import Catch.catchForIO +import cats.effect.* + +object IOCatch { + + def ioCatching[E]: IOCatchPartiallyApplied[E] = + new IOCatchPartiallyApplied[E] + + class IOCatchPartiallyApplied[E] { + def apply[A](f: Handle[IO, E] => IO[A]): IO[Either[E, A]] = { + catchForIO.flatMap { c => + c.use[E](f) + } + } + } + +} + diff --git a/modules/core/src/test/scala/catcheffect/CatchTest.scala b/modules/core/src/test/scala/catcheffect/CatchTest.scala index 6d967ae..b2fb3a4 100644 --- a/modules/core/src/test/scala/catcheffect/CatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/CatchTest.scala @@ -20,7 +20,7 @@ import cats.implicits._ import munit.CatsEffectSuite class CatchTest extends CatsEffectSuite { - val C = Catch.ioCatch.unsafeRunSync() + val C = Catch.catchForIO.unsafeRunSync() test("should be able to throw, catch and re-throw") { val program = C.use[String] { hs => diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala new file mode 100644 index 0000000..35d1945 --- /dev/null +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -0,0 +1,68 @@ +package catcheffect + +import catcheffect.Catch.RaisedWithoutHandler +import catcheffect.IOCatch.ioCatching +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 <- ioCatching[String](h => + for { + _ <- h.raise("error") + _ <- ref.set("should not be set") + } yield 1 + ) + _ = assertEquals(res, Left("error")) + _ <- assertIO(ref.get, "init") + } yield () + } + + test("can nest correctly") { + for { + res <- ioCatching[String](hs1 => + for { + _ <- ioCatching[String](hs2 => + hs1.raise("1") + ) + _ <- IO.raiseError(new AssertionError("should not reach this point")) + } yield () + ) + _ = assertEquals(res, Left("1")) + } yield res + } + + test("Can raise in an uncancellable region") { + for { + res <- ioCatching[String](h => + IO.uncancelable(_ => h.raise("oops")) + ) + _ = assertEquals(res, Left("oops")) + } yield () + } + + test("Can raise in an cancellable region when polled") { + for { + res <- ioCatching[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 <- ioCatching[String](h => + cached.complete(h).void + ) + _ = assertEquals(res, Right(())) + _ <- cached.get.flatMap(_.raise("hm")) + } yield ()).attempt.map { + case Left(_: RaisedWithoutHandler[_]) => () // succeed + case other => fail(s"Expected RaisedWithoutHandler, got $other") + } + } +} From a95ebd2e33ac0b8a119239eeefd6c25e1dc9a76b Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 16:21:31 +0100 Subject: [PATCH 2/7] Use IOCatch.apply instead of ioCatching. Revert rename of ioCatch (kept the val) --- modules/core/src/main/scala/catcheffect/Catch.scala | 2 +- .../core/src/main/scala/catcheffect/IOCatch.scala | 6 +++--- .../core/src/test/scala/catcheffect/CatchTest.scala | 2 +- .../src/test/scala/catcheffect/IOCatchTest.scala | 13 ++++++------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/modules/core/src/main/scala/catcheffect/Catch.scala b/modules/core/src/main/scala/catcheffect/Catch.scala index fd768f9..3d997da 100644 --- a/modules/core/src/main/scala/catcheffect/Catch.scala +++ b/modules/core/src/main/scala/catcheffect/Catch.scala @@ -222,7 +222,7 @@ object Catch { |The handler was defined at $alloc""".stripMargin } - val catchForIO: IO[Catch[IO]] = + val ioCatch: IO[Catch[IO]] = LocalForIOLocal .localForIOLocalDefault(Vault.empty) .map(implicit loc => fromLocal[IO]) diff --git a/modules/core/src/main/scala/catcheffect/IOCatch.scala b/modules/core/src/main/scala/catcheffect/IOCatch.scala index 3168687..be2c2da 100644 --- a/modules/core/src/main/scala/catcheffect/IOCatch.scala +++ b/modules/core/src/main/scala/catcheffect/IOCatch.scala @@ -1,16 +1,16 @@ package catcheffect -import Catch.catchForIO +import Catch.ioCatch import cats.effect.* object IOCatch { - def ioCatching[E]: IOCatchPartiallyApplied[E] = + def apply[E]: IOCatchPartiallyApplied[E] = new IOCatchPartiallyApplied[E] class IOCatchPartiallyApplied[E] { def apply[A](f: Handle[IO, E] => IO[A]): IO[Either[E, A]] = { - catchForIO.flatMap { c => + ioCatch.flatMap { c => c.use[E](f) } } diff --git a/modules/core/src/test/scala/catcheffect/CatchTest.scala b/modules/core/src/test/scala/catcheffect/CatchTest.scala index b2fb3a4..6d967ae 100644 --- a/modules/core/src/test/scala/catcheffect/CatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/CatchTest.scala @@ -20,7 +20,7 @@ import cats.implicits._ import munit.CatsEffectSuite class CatchTest extends CatsEffectSuite { - val C = Catch.catchForIO.unsafeRunSync() + val C = Catch.ioCatch.unsafeRunSync() test("should be able to throw, catch and re-throw") { val program = C.use[String] { hs => diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala index 35d1945..a8252ea 100644 --- a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -1,7 +1,6 @@ package catcheffect import catcheffect.Catch.RaisedWithoutHandler -import catcheffect.IOCatch.ioCatching import cats.effect.* import munit.CatsEffectSuite @@ -9,7 +8,7 @@ class IOCatchTest extends CatsEffectSuite { test("should abort execution as soon as raise is called") { for { ref <- Ref[IO].of("init") - res <- ioCatching[String](h => + res <- IOCatch[String](h => for { _ <- h.raise("error") _ <- ref.set("should not be set") @@ -22,9 +21,9 @@ class IOCatchTest extends CatsEffectSuite { test("can nest correctly") { for { - res <- ioCatching[String](hs1 => + res <- IOCatch[String](hs1 => for { - _ <- ioCatching[String](hs2 => + _ <- IOCatch[String](hs2 => hs1.raise("1") ) _ <- IO.raiseError(new AssertionError("should not reach this point")) @@ -36,7 +35,7 @@ class IOCatchTest extends CatsEffectSuite { test("Can raise in an uncancellable region") { for { - res <- ioCatching[String](h => + res <- IOCatch[String](h => IO.uncancelable(_ => h.raise("oops")) ) _ = assertEquals(res, Left("oops")) @@ -45,7 +44,7 @@ class IOCatchTest extends CatsEffectSuite { test("Can raise in an cancellable region when polled") { for { - res <- ioCatching[String](h => + res <- IOCatch[String](h => IO.uncancelable(poll => poll(h.raise("oops"))) ) _ = assertEquals(res, Left("oops")) @@ -55,7 +54,7 @@ class IOCatchTest extends CatsEffectSuite { test("Fail to raise if the Raise instance is used outside of its original fiber") { (for { cached <- Deferred[IO, Raise[IO, String]] - res <- ioCatching[String](h => + res <- IOCatch[String](h => cached.complete(h).void ) _ = assertEquals(res, Right(())) From a4271fdebd87364a2c257bf0ad24da3cc5fa2ba1 Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 16:24:22 +0100 Subject: [PATCH 3/7] Add headers --- .../core/src/main/scala/catcheffect/IOCatch.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/core/src/main/scala/catcheffect/IOCatch.scala b/modules/core/src/main/scala/catcheffect/IOCatch.scala index be2c2da..2edb9be 100644 --- a/modules/core/src/main/scala/catcheffect/IOCatch.scala +++ b/modules/core/src/main/scala/catcheffect/IOCatch.scala @@ -1,3 +1,18 @@ +/* + * 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 From af717458db0f3d6634e61e824df9c954b81a68de Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 16:50:43 +0100 Subject: [PATCH 4/7] Update uncancelable test to handle race-y condition --- .../test/scala/catcheffect/IOCatchTest.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala index a8252ea..c5ed106 100644 --- a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -1,6 +1,6 @@ package catcheffect -import catcheffect.Catch.RaisedWithoutHandler +import catcheffect.Catch.{RaisedInUncancellable, RaisedWithoutHandler} import cats.effect.* import munit.CatsEffectSuite @@ -33,13 +33,20 @@ class IOCatchTest extends CatsEffectSuite { } yield res } - test("Can raise in an uncancellable region") { - for { + 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")) + IO.uncancelable(_ => h.raise("oops").void) ) - _ = assertEquals(res, Left("oops")) - } yield () + // 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") { From 356e75b364921f862a4dc6e833d8ace2bf26f5f4 Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 16:52:24 +0100 Subject: [PATCH 5/7] fmt --- .../src/main/scala/catcheffect/IOCatch.scala | 7 ++--- .../test/scala/catcheffect/IOCatchTest.scala | 30 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/modules/core/src/main/scala/catcheffect/IOCatch.scala b/modules/core/src/main/scala/catcheffect/IOCatch.scala index 2edb9be..d69fc37 100644 --- a/modules/core/src/main/scala/catcheffect/IOCatch.scala +++ b/modules/core/src/main/scala/catcheffect/IOCatch.scala @@ -19,10 +19,10 @@ 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 => @@ -30,6 +30,5 @@ object IOCatch { } } } - -} +} diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala index c5ed106..b0da8ec 100644 --- a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -18,57 +18,49 @@ class IOCatchTest extends CatsEffectSuite { _ <- assertIO(ref.get, "init") } yield () } - + test("can nest correctly") { for { res <- IOCatch[String](hs1 => for { - _ <- IOCatch[String](hs2 => - hs1.raise("1") - ) + _ <- IOCatch[String](hs2 => hs1.raise("1")) _ <- IO.raiseError(new AssertionError("should not reach this point")) } 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, + // 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) - ) + 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")) + _ = assertEquals(res, Left("oops")) } yield ()).recoverWith { case _: RaisedInUncancellable[_] => IO.unit // succeed - case e => IO.raiseError(e) + 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"))) - ) + 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 - ) + res <- IOCatch[String](h => cached.complete(h).void) _ = assertEquals(res, Right(())) _ <- cached.get.flatMap(_.raise("hm")) } yield ()).attempt.map { case Left(_: RaisedWithoutHandler[_]) => () // succeed - case other => fail(s"Expected RaisedWithoutHandler, got $other") + case other => fail(s"Expected RaisedWithoutHandler, got $other") } } } From e14414dcbcf2694dd4802263fb45e98126a801ed Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 17:01:28 +0100 Subject: [PATCH 6/7] headers for test file --- .../src/test/scala/catcheffect/IOCatchTest.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala index b0da8ec..f7efad2 100644 --- a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -1,3 +1,18 @@ +/* + * 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} From 9be2bc6ef4256cdbba8aebc10724f3ea9ceb3472 Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 2 Jul 2024 20:40:57 +0100 Subject: [PATCH 7/7] fix warnings --- modules/core/src/test/scala/catcheffect/IOCatchTest.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala index f7efad2..139574a 100644 --- a/modules/core/src/test/scala/catcheffect/IOCatchTest.scala +++ b/modules/core/src/test/scala/catcheffect/IOCatchTest.scala @@ -25,7 +25,7 @@ class IOCatchTest extends CatsEffectSuite { ref <- Ref[IO].of("init") res <- IOCatch[String](h => for { - _ <- h.raise("error") + _ <- h.raise("error").void _ <- ref.set("should not be set") } yield 1 ) @@ -38,8 +38,8 @@ class IOCatchTest extends CatsEffectSuite { for { res <- IOCatch[String](hs1 => for { - _ <- IOCatch[String](hs2 => hs1.raise("1")) - _ <- IO.raiseError(new AssertionError("should not reach this point")) + _ <- IOCatch[String](_ => hs1.raise("1")) + _ <- IO.raiseError(new AssertionError("should not reach this point")).void } yield () ) _ = assertEquals(res, Left("1")) @@ -72,7 +72,7 @@ class IOCatchTest extends CatsEffectSuite { cached <- Deferred[IO, Raise[IO, String]] res <- IOCatch[String](h => cached.complete(h).void) _ = assertEquals(res, Right(())) - _ <- cached.get.flatMap(_.raise("hm")) + _ <- cached.get.flatMap(_.raise("hm")).void } yield ()).attempt.map { case Left(_: RaisedWithoutHandler[_]) => () // succeed case other => fail(s"Expected RaisedWithoutHandler, got $other")