Skip to content

Commit

Permalink
introduce AsFutureResult typeclass for Specs2, to abstract over the k…
Browse files Browse the repository at this point in the history
…ind of test results

This lets `CatsResource#withResource` accept test implementations
that return kinds like `*` (`A`), `* -> *` (`F[A]`), and
`(* -> *) -> *` (`org.scalacheck.effect.PropF[F]`).
  • Loading branch information
bpholt committed Dec 18, 2024
1 parent a694c09 commit cec54d9
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package cats.effect.testing.specs2
package tests

trait CatsEffectSpecsPlatform { this: CatsEffectSpecs =>
def platformSpecs = "really execute effects" in skipped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package cats.effect.testing.specs2
package tests

import cats.effect.IO

Expand Down
Original file line number Diff line number Diff line change
@@ -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](_))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
Expand All @@ -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]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit cec54d9

Please sign in to comment.