From a674d331cc367be868bcd2762dbf13d0278d990d Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 18 Sep 2026 07:52:17 +0200 Subject: [PATCH 1/2] Add pending tests for syncStep remainder losing onCancel Two IOSuite tests take the Left remainder from syncStep, cancel it, and assert that the Resource release ran. The release does not run today, so both are tagged .fail.pending until the interpreter is fixed. --- .../src/test/scala/cats/effect/IOSuite.scala | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/IOSuite.scala b/tests/shared/src/test/scala/cats/effect/IOSuite.scala index 4a4ecd5dd2..180db04630 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSuite.scala @@ -2759,6 +2759,40 @@ class IOSuite extends BaseScalaCheckSuite with DisciplineSuite with IOPlatformSu assertCompleteAsSync(sio.map(_.bimap(_ => (), _ => ())), Right(())) } + ticked( + "syncStep - remainder runs Resource release on self-cancelation" + .fail + .pending("syncStep strips onCancel from the remainder")) { implicit ticker => + var released = false + val io = Resource.make(IO.unit)(_ => IO { released = true }).use(_ => IO.canceled) + val rest = io.syncStep(Int.MaxValue).unsafeRunSync() match { + case Left(rest) => rest + case Right(a) => IO.pure(a) + } + assertSelfCancel(rest) + assert(released) + } + + ticked( + "syncStep - remainder runs Resource release on external cancelation" + .fail + .pending("syncStep strips onCancel from the remainder")) { implicit ticker => + var released = false + val io = Resource.make(IO.unit)(_ => IO { released = true }).use(_ => IO.never[Unit]) + val rest = io.syncStep(Int.MaxValue).unsafeRunSync() match { + case Left(rest) => rest + case Right(a) => IO.pure(a) + } + val test = for { + started <- Deferred[IO, Unit] + f <- (started.complete(()) *> rest).start + _ <- started.get + _ <- f.cancel + } yield () + assertCompleteAs(test, ()) + assert(released) + } + real("fiber repeated yielding test") { def yieldUntil(ref: Ref[IO, Boolean]): IO[Unit] = ref.get.flatMap(b => if (b) IO.unit else IO.cede *> yieldUntil(ref)) From ee95128af23b4c32b5f2978f22742c26fde3fe56 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 18 Sep 2026 11:24:40 +0200 Subject: [PATCH 2/2] Add pending test for syncStep remainder losing uncancelable Same shape as the onCancel tests: split IO.uncancelable(_ => IO.never) with syncStep and cancel the remainder from another fiber. The original blocks the cancel on the mask; the remainder lets it through. --- .../src/test/scala/cats/effect/IOSuite.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/IOSuite.scala b/tests/shared/src/test/scala/cats/effect/IOSuite.scala index 180db04630..637f457f6b 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSuite.scala @@ -2793,6 +2793,24 @@ class IOSuite extends BaseScalaCheckSuite with DisciplineSuite with IOPlatformSu assert(released) } + ticked( + "syncStep - remainder stays uncancelable" + .fail + .pending("syncStep strips uncancelable from the remainder")) { implicit ticker => + val io = IO.uncancelable(_ => IO.never[Unit]) + val rest = io.syncStep(Int.MaxValue).unsafeRunSync() match { + case Left(rest) => rest + case Right(a) => IO.pure(a) + } + val test = for { + started <- Deferred[IO, Unit] + f <- (started.complete(()) *> rest).start + _ <- started.get + _ <- f.cancel + } yield () + assertNonTerminate(test) + } + real("fiber repeated yielding test") { def yieldUntil(ref: Ref[IO, Boolean]): IO[Unit] = ref.get.flatMap(b => if (b) IO.unit else IO.cede *> yieldUntil(ref))