Skip to content

Commit fd20d6a

Browse files
authored
Merge pull request #616 from Dwolla/specs2
Abstract over Specs2 test result kind to enable more consistent support for ScalaCheck
2 parents 079114f + cec54d9 commit fd20d6a

File tree

10 files changed

+138
-19
lines changed

10 files changed

+138
-19
lines changed

core/shared/src/main/scala/cats/effect/testing/UnsafeRun.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ object UnsafeRun {
4141
override def unsafeToFuture[A](ioa: IO[A], timeout: Option[FiniteDuration]): Future[A] =
4242
timeout.fold(ioa)(ioa.timeout).unsafeToFuture()
4343
}
44+
45+
implicit val unsafeRunScalaFuture: UnsafeRun[scala.concurrent.Future] = new UnsafeRun[scala.concurrent.Future] {
46+
override def unsafeToFuture[AA](fa: scala.concurrent.Future[AA]): scala.concurrent.Future[AA] = fa
47+
}
4448
}

specs2/js-native/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect.testing.specs2
17+
package tests
1818

1919
trait CatsEffectSpecsPlatform { this: CatsEffectSpecs =>
2020
def platformSpecs = "really execute effects" in skipped

specs2/jvm/src/test/scala/cats/effect/testing/specs2/CatsEffectSpecsPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect.testing.specs2
17+
package tests
1818

1919
import cats.effect.IO
2020

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2020 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cats.effect.testing.specs2
18+
19+
import cats.*
20+
import cats.effect.testing.UnsafeRun
21+
import cats.syntax.all.*
22+
import org.specs2.execute.{AsResult, Result}
23+
24+
import scala.concurrent.{ExecutionContext, Future}
25+
26+
/**
27+
* `AsFutureResult` can be thought of an extension of `UnsafeRun[F].unsafeToFuture[A]` when
28+
* an `AsResult[A]` is available. This is necessary to support structures like
29+
* `org.scalacheck.effect.PropF[F]`, where it is not possible to write an instance of
30+
* `UnsafeRun[PropF]`, since `PropF#check` returns `Result` and
31+
* not `A`. (This doesn't work, even using e.g. `UnsafeRun[({ type λ[α] = PropF[F] })#λ]`.)
32+
*
33+
* This lets us abstract over different kinds of results, such as `A` (assuming an
34+
* `AsResult[A]`), `F[A]` (assuming an `UnsafeRun[F]` and `AsResult[A]`), or `PropF[F]`
35+
* (assuming an `UnsafeRun[F]`).
36+
*/
37+
trait AsFutureResult[T] {
38+
def asResult(t: => T): Future[Result]
39+
}
40+
41+
object AsFutureResult extends LowPriorityAsFutureResultInstances {
42+
def apply[T: AsFutureResult]: AsFutureResult[T] = implicitly
43+
44+
implicit def asResult[T: AsResult](implicit ec: ExecutionContext): AsFutureResult[T] =
45+
new AsFutureResult[T] {
46+
override def asResult(t: => T): Future[Result] =
47+
Future(AsResult[T](t))
48+
}
49+
50+
implicit def effectualResult[F[_]: Functor: UnsafeRun, T: AsResult]: AsFutureResult[F[T]] =
51+
new AsFutureResult[F[T]] {
52+
override def asResult(t: => F[T]): Future[Result] =
53+
UnsafeRun[F].unsafeToFuture(t.map(AsResult[T](_)))
54+
}
55+
}
56+
57+
sealed trait LowPriorityAsFutureResultInstances {
58+
implicit def effectualResultViaAsFutureResult[F[_]: UnsafeRun, T: AsFutureResult](implicit
59+
ec: ExecutionContext
60+
): AsFutureResult[F[T]] = new AsFutureResult[F[T]] {
61+
override def asResult(t: => F[T]): Future[Result] =
62+
UnsafeRun[F].unsafeToFuture(t).flatMap(AsFutureResult[T].asResult(_))
63+
}
64+
65+
implicit def effectualResultWithoutFunctor[F[_] : UnsafeRun, T: AsResult](implicit ec: ExecutionContext): AsFutureResult[F[T]] =
66+
new AsFutureResult[F[T]] {
67+
override def asResult(t: => F[T]): Future[Result] =
68+
UnsafeRun[F].unsafeToFuture(t).map(AsResult[T](_))
69+
}
70+
}

specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsEffect.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ package cats.effect.testing
1818
package specs2
1919

2020
import cats.effect.{MonadCancel, Resource}
21-
import cats.syntax.all._
22-
21+
import cats.syntax.all.*
2322
import org.specs2.execute.AsResult
2423
import org.specs2.specification.core.{AsExecution, Execution}
2524

26-
import scala.concurrent.duration._
25+
import scala.concurrent.duration.*
2726

28-
trait CatsEffect {
27+
trait CatsEffect extends LowPriorityAsExecutionInstances {
2928

3029
protected val Timeout: Duration = 10.seconds
3130
protected def finiteTimeout: Option[FiniteDuration] =
@@ -45,3 +44,12 @@ trait CatsEffect {
4544
effectAsExecution[F, R].execute(t.use(_.pure[F]))
4645
}
4746
}
47+
48+
sealed trait LowPriorityAsExecutionInstances { this: CatsEffect =>
49+
implicit def asFutureResultAsExecution[F[_], A](implicit AFR: AsFutureResult[F[A]]): AsExecution[F[A]] = new AsExecution[F[A]] {
50+
override def execute(t: => F[A]): Execution =
51+
Execution
52+
.withEnvAsync(_ => AsFutureResult[F[A]].asResult(t))
53+
.copy(timeout = finiteTimeout)
54+
}
55+
}

specs2/shared/src/main/scala/cats/effect/testing/specs2/CatsResource.scala

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package cats.effect.testing
1818
package specs2
1919

20-
import cats.effect._
21-
import cats.effect.syntax.all._
22-
import cats.syntax.all._
20+
import cats.effect.*
21+
import cats.effect.syntax.all.*
22+
import cats.syntax.all.*
23+
import org.specs2.execute.Result
2324
import org.specs2.specification.BeforeAfterAll
25+
import org.typelevel.scalaccompat.annotation.unused
2426

25-
import scala.concurrent.duration._
27+
import scala.concurrent.duration.*
2628

2729
abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll with CatsEffect {
2830

@@ -72,7 +74,7 @@ abstract class CatsResource[F[_]: Async: UnsafeRun, A] extends BeforeAfterAll wi
7274
shutdown = ().pure[F]
7375
}
7476

75-
def withResource[R](f: A => F[R]): F[R] =
77+
private[specs2] def withResource[R](f: A => F[R]): F[R] =
7678
gate match {
7779
case Some(g) =>
7880
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
8183
case None =>
8284
Spawn[F].cede >> withResource(f)
8385
}
86+
87+
def withResource[B: AsFutureResult](f: A => B): F[Result] =
88+
withResource { (a: A) =>
89+
Async[F].bracket(before(a)) { aAsModifiedByBefore =>
90+
Async[F].fromFuture {
91+
Sync[F].delay {
92+
AsFutureResult[B].asResult(f(aAsModifiedByBefore))
93+
}
94+
}
95+
}(after)
96+
}
97+
98+
/**
99+
* Override this to modify the resource before it is used, or to use the
100+
* resource to perform some setup work, prior to each test.
101+
*
102+
* The default implementation simply returns the resource unchanged.
103+
*
104+
* @param a the resource, having been acquired in the beforeAll phase
105+
* @return the modified resource, which will be passed to the test function
106+
*/
107+
def before(a: A): F[A] = a.pure[F]
108+
109+
/**
110+
* Override this to perform some cleanup after each test.
111+
*
112+
* The default implementation does nothing.
113+
*
114+
* @param a the resource, having been acquired in the beforeAll phase
115+
*/
116+
def after(@unused a: A): F[Unit] = ().pure[F]
84117
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect.testing.specs2
17+
package tests
1818

19+
import cats.effect.testing.specs2.*
1920
import cats.effect.{IO, Ref, Resource}
20-
import cats.syntax.all._
21+
import cats.syntax.all.*
2122
import org.specs2.mutable.Specification
2223

2324
class CatsEffectSpecs extends Specification with CatsEffect with CatsEffectSpecsPlatform {
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect.testing.specs2
17+
package tests
1818

19+
import cats.effect.testing.specs2.*
1920
import cats.effect.{IO, Resource}
2021
import org.specs2.execute.Result
2122
import org.specs2.mutable.SpecificationLike
@@ -32,7 +33,7 @@ class CatsResourceErrorSpecs
3233
Resource.eval(IO.raiseError(expectedException))
3334

3435
"cats resource support" should {
35-
"report failure when the resource acquisition fails" in withResource[Result] { _ =>
36+
"report failure when the resource acquisition fails" in withResource { (_: Unit) =>
3637
IO(failure("we shouldn't get here if an exception was raised"))
3738
}
3839
.recover[Result] {
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect
18-
package testing.specs2
17+
package tests
1918

19+
import cats.effect.*
20+
import cats.effect.testing.specs2.*
2021
import org.specs2.mutable.SpecificationLike
2122

22-
import scala.concurrent.duration._
23+
import scala.concurrent.duration.*
2324

2425
class CatsResourceParallelSpecs extends CatsResource[IO, Unit] with SpecificationLike {
2526
// *not* sequential
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
package cats.effect.testing.specs2
17+
package tests
1818

19+
import cats.effect.testing.specs2.*
1920
import cats.effect.{IO, Ref, Resource}
2021
import org.specs2.mutable.SpecificationLike
2122

0 commit comments

Comments
 (0)