diff --git a/specs2/jvm/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala b/specs2/jvm/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala index 4540839..0f6578c 100644 --- a/specs2/jvm/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala +++ b/specs2/jvm/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package cats.effect.testing.specs2 +package tests import cats.effect.IO diff --git a/specs2/shared/src/main/scala/cats/effect/testing/specs2/AsFutureResult.scala b/specs2/shared/src/main/scala/cats/effect/testing/specs2/AsFutureResult.scala new file mode 100644 index 0000000..f5e0133 --- /dev/null +++ b/specs2/shared/src/main/scala/cats/effect/testing/specs2/AsFutureResult.scala @@ -0,0 +1,70 @@ +/* + * 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.* +import cats.effect.testing.UnsafeRun +import cats.syntax.all.* +import org.specs2.execute.{AsResult, Result} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * `AsFutureResult` can be thought of an extension of `UnsafeRun[F].unsafeToFuture[A]` when + * an `AsResult[A]` is available. This is necessary to support structures like + * `org.scalacheck.effect.PropF[F]`, where it is not possible to write an instance of + * `UnsafeRun[PropF]`, since `PropF#check` returns `Result` and + * not `A`. (This doesn't work, even using e.g. `UnsafeRun[({ type λ[α] = PropF[F] })#λ]`.) + * + * This lets us abstract over different kinds of results, such as `A` (assuming an + * `AsResult[A]`), `F[A]` (assuming an `UnsafeRun[F]` and `AsResult[A]`), or `PropF[F]` + * (assuming an `UnsafeRun[F]`). + */ +trait AsFutureResult[T] { + def asResult(t: => T): Future[Result] +} + +object AsFutureResult extends LowPriorityAsFutureResultInstances { + def apply[T: AsFutureResult]: AsFutureResult[T] = implicitly + + implicit def asResult[T: AsResult](implicit ec: ExecutionContext): AsFutureResult[T] = + new AsFutureResult[T] { + override def asResult(t: => T): Future[Result] = + Future(AsResult[T](t)) + } + + implicit def effectualResult[F[_]: Functor: UnsafeRun, T: AsResult]: AsFutureResult[F[T]] = + new AsFutureResult[F[T]] { + override def asResult(t: => F[T]): Future[Result] = + UnsafeRun[F].unsafeToFuture(t.map(AsResult[T](_))) + } +} + +sealed trait LowPriorityAsFutureResultInstances { + implicit def effectualResultViaAsFutureResult[F[_]: UnsafeRun, T: AsFutureResult](implicit + ec: ExecutionContext + ): AsFutureResult[F[T]] = new AsFutureResult[F[T]] { + override def asResult(t: => F[T]): Future[Result] = + UnsafeRun[F].unsafeToFuture(t).flatMap(AsFutureResult[T].asResult(_)) + } + + implicit def effectualResultWithoutFunctor[F[_] : UnsafeRun, T: AsResult](implicit ec: ExecutionContext): AsFutureResult[F[T]] = + new AsFutureResult[F[T]] { + override def asResult(t: => F[T]): Future[Result] = + UnsafeRun[F].unsafeToFuture(t).map(AsResult[T](_)) + } +} diff --git a/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsEffect.scala b/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsEffect.scala index ef94ab7..14247c4 100644 --- a/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsEffect.scala +++ b/specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsEffect.scala @@ -18,14 +18,13 @@ package cats.effect.testing package specs2 import cats.effect.{MonadCancel, Resource} -import cats.syntax.all._ - +import cats.syntax.all.* import org.specs2.execute.AsResult import org.specs2.specification.core.{AsExecution, Execution} -import scala.concurrent.duration._ +import scala.concurrent.duration.* -trait CatsEffect { +trait CatsEffect extends LowPriorityAsExecutionInstances { protected val Timeout: Duration = 10.seconds protected def finiteTimeout: Option[FiniteDuration] = @@ -45,3 +44,12 @@ trait CatsEffect { effectAsExecution[F, R].execute(t.use(_.pure[F])) } } + +sealed trait LowPriorityAsExecutionInstances { this: CatsEffect => + implicit def asFutureResultAsExecution[F[_], A](implicit AFR: AsFutureResult[F[A]]): AsExecution[F[A]] = new AsExecution[F[A]] { + override def execute(t: => F[A]): Execution = + Execution + .withEnvAsync(_ => AsFutureResult[F[A]].asResult(t)) + .copy(timeout = finiteTimeout) + } +} 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 36efbb8..5e36b69 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,12 +17,14 @@ package cats.effect.testing package specs2 -import cats.effect._ -import cats.effect.syntax.all._ -import cats.syntax.all._ +import cats.effect.* +import cats.effect.syntax.all.* +import cats.syntax.all.* +import org.specs2.execute.Result import org.specs2.specification.BeforeAfterAll +import org.typelevel.scalaccompat.annotation.unused -import scala.concurrent.duration._ +import scala.concurrent.duration.* abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll with CatsEffect { @@ -72,7 +74,7 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi shutdown = ().pure[F] } - def withResource[R](f: A => F[R]): F[R] = + private[specs2] def withResource[R](f: A => F[R]): F[R] = gate match { case Some(g) => finiteResourceTimeout.foldl(g.get)(_.timeout(_)) *> Sync[F].delay(value.get).rethrow.flatMap(f) @@ -81,4 +83,35 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi case None => Spawn[F].cede >> withResource(f) } + + def withResource[B: AsFutureResult](f: A => B): F[Result] = + withResource { (a: A) => + Async[F].bracket(before(a)) { aAsModifiedByBefore => + Async[F].fromFuture { + Sync[F].delay { + AsFutureResult[B].asResult(f(aAsModifiedByBefore)) + } + } + }(after) + } + + /** + * Override this to modify the resource before it is used, or to use the + * resource to perform some setup work, prior to each test. + * + * The default implementation simply returns the resource unchanged. + * + * @param a the resource, having been acquired in the beforeAll phase + * @return the modified resource, which will be passed to the test function + */ + def before(a: A): F[A] = a.pure[F] + + /** + * Override this to perform some cleanup after each test. + * + * The default implementation does nothing. + * + * @param a the resource, having been acquired in the beforeAll phase + */ + def after(@unused a: A): F[Unit] = ().pure[F] } diff --git a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecs.scala b/specs2/shared/src/test/scala/tests/CatsEffectSpecs.scala similarity index 94% rename from specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecs.scala rename to specs2/shared/src/test/scala/tests/CatsEffectSpecs.scala index 9218cdb..976609f 100644 --- a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecs.scala +++ b/specs2/shared/src/test/scala/tests/CatsEffectSpecs.scala @@ -14,10 +14,11 @@ * limitations under the License. */ -package cats.effect.testing.specs2 +package tests +import cats.effect.testing.specs2.* import cats.effect.{IO, Ref, Resource} -import cats.syntax.all._ +import cats.syntax.all.* import org.specs2.mutable.Specification class CatsEffectSpecs extends Specification with CatsEffect with CatsEffectSpecsPlatform { diff --git a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala b/specs2/shared/src/test/scala/tests/CatsResourceErrorSpecs.scala similarity index 94% rename from specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala rename to specs2/shared/src/test/scala/tests/CatsResourceErrorSpecs.scala index 95a7eb0..ec8e15d 100644 --- a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceErrorSpecs.scala +++ b/specs2/shared/src/test/scala/tests/CatsResourceErrorSpecs.scala @@ -14,8 +14,9 @@ * limitations under the License. */ -package cats.effect.testing.specs2 +package tests +import cats.effect.testing.specs2.* import cats.effect.{IO, Resource} import org.specs2.execute.Result import org.specs2.mutable.SpecificationLike @@ -32,7 +33,7 @@ class CatsResourceErrorSpecs Resource.eval(IO.raiseError(expectedException)) "cats resource support" should { - "report failure when the resource acquisition fails" in withResource[Result] { _ => + "report failure when the resource acquisition fails" in withResource { (_: Unit) => IO(failure("we shouldn't get here if an exception was raised")) } .recover[Result] { diff --git a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceParallelSpecs.scala b/specs2/shared/src/test/scala/tests/CatsResourceParallelSpecs.scala similarity index 89% rename from specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceParallelSpecs.scala rename to specs2/shared/src/test/scala/tests/CatsResourceParallelSpecs.scala index 9f8f7b3..8114185 100644 --- a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceParallelSpecs.scala +++ b/specs2/shared/src/test/scala/tests/CatsResourceParallelSpecs.scala @@ -14,12 +14,13 @@ * limitations under the License. */ -package cats.effect -package testing.specs2 +package tests +import cats.effect.* +import cats.effect.testing.specs2.* import org.specs2.mutable.SpecificationLike -import scala.concurrent.duration._ +import scala.concurrent.duration.* class CatsResourceParallelSpecs extends CatsResource[IO, Unit] with SpecificationLike { // *not* sequential diff --git a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceSpecs.scala b/specs2/shared/src/test/scala/tests/CatsResourceSpecs.scala similarity index 96% rename from specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceSpecs.scala rename to specs2/shared/src/test/scala/tests/CatsResourceSpecs.scala index c7986fd..36a78a4 100644 --- a/specs2/shared/src/test/scala/cats/effect/testing/specs2/CatsResourceSpecs.scala +++ b/specs2/shared/src/test/scala/tests/CatsResourceSpecs.scala @@ -14,8 +14,9 @@ * limitations under the License. */ -package cats.effect.testing.specs2 +package tests +import cats.effect.testing.specs2.* import cats.effect.{IO, Ref, Resource} import org.specs2.mutable.SpecificationLike