From 4341ae4ca64aa848427463b78030c501f4c379ee Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Wed, 20 Nov 2024 12:07:01 -0600 Subject: [PATCH 1/2] add test to demonstrate hang When using CatsResource with Specs2, if the resource acquisition raises an exception, the tests will never start and the exception will never be reported. --- .../specs2/CatsResourceErrorSpecs.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala diff --git a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala b/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala new file mode 100644 index 00000000..95a7eb01 --- /dev/null +++ b/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Typelevel + * + * 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 cats.effect.testing.specs2 + +import cats.effect.{IO, Resource} +import org.specs2.execute.Result +import org.specs2.mutable.SpecificationLike + +case class BlowUpResourceException() extends RuntimeException("boom") + +class CatsResourceErrorSpecs + extends CatsResource[IO, Unit] + with SpecificationLike { + + private val expectedException = BlowUpResourceException() + + val resource: Resource[IO, Unit] = + Resource.eval(IO.raiseError(expectedException)) + + "cats resource support" should { + "report failure when the resource acquisition fails" in withResource[Result] { _ => + IO(failure("we shouldn't get here if an exception was raised")) + } + .recover[Result] { + case ex: RuntimeException => + ex must beEqualTo(expectedException) + } + } +} From 4a70db2ce83dd9a318a70560b50c3474c77e81b6 Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Wed, 20 Nov 2024 12:09:10 -0600 Subject: [PATCH 2/2] tweak Specs2 CatsResource to rethrow any exceptions raised during resource acquisition --- .../cats/effect/testing/specs2/CatsResource.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsResource.scala b/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsResource.scala index 6ebc1022..36efbb83 100644 --- a/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsResource.scala +++ b/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsResource.scala @@ -17,9 +17,9 @@ package cats.effect.testing package specs2 -import cats.effect.{Async, Deferred, Resource, Spawn, Sync} +import cats.effect._ +import cats.effect.syntax.all._ import cats.syntax.all._ - import org.specs2.specification.BeforeAfterAll import scala.concurrent.duration._ @@ -39,7 +39,7 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi // but it does work on scalajs @volatile private var gate: Option[Deferred[F, Unit]] = None - private var value: Option[A] = None + private var value: Option[Either[Throwable, A]] = None private var shutdown: F[Unit] = ().pure[F] override def beforeAll(): Unit = { @@ -49,7 +49,7 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi gate = Some(d) } - pair <- resource.allocated + pair <- resource.attempt.allocated (a, shutdownAction) = pair _ <- Sync[F] delay { @@ -75,7 +75,7 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi def withResource[R](f: A => F[R]): F[R] = gate match { case Some(g) => - g.get *> Sync[F].delay(value.get).flatMap(f) + finiteResourceTimeout.foldl(g.get)(_.timeout(_)) *> Sync[F].delay(value.get).rethrow.flatMap(f) // specs2's runtime should prevent this case case None =>