diff --git a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/CheckConfig.scala b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/CheckConfig.scala index 95dd13f8..767c303d 100644 --- a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/CheckConfig.scala +++ b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/CheckConfig.scala @@ -28,15 +28,21 @@ case class CheckConfig private ( /** * The proportion of values discarded by the generator allowed before the test * is considered failed. + * + * A value is considered discarded if the generator outputs `None`. The + * generator must not discard more values than the number of successful runs, + * so this ratio is a proportion of [[minimumSuccessful]]. */ def withMaximumDiscardRatio(maximumDiscardRatio: Int) = copy( maximumDiscardRatio = maximumDiscardRatio ) + /** The [[org.scalacheck.Gen.Parameters.size]] of the generator. */ def withMaximumGeneratorSize(maximumGeneratorSize: Int) = copy( maximumGeneratorSize = maximumGeneratorSize ) + /** The number of concurrent runs. */ def withPerPropertyParallelism(perPropertyParallelism: Int) = copy( perPropertyParallelism = perPropertyParallelism ) diff --git a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala index 0427052a..60d0e61d 100644 --- a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala +++ b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala @@ -7,6 +7,8 @@ import scala.util.control.NoStackTrace import org.scalacheck.rng.Seed import org.scalacheck.{ Arbitrary, Gen } import cats.data.Validated +import cats.effect.Concurrent +import cats.MonadThrow trait Checkers { self: EffectSuiteAux => @@ -20,7 +22,7 @@ trait Checkers { f andThen (b => Prop[F, B].lift(b)) } - // Configuration for property-based tests + /** Configuration for all property-based tests in this suite. */ def checkConfig: CheckConfig = CheckConfig.default class PartiallyAppliedForall(config: CheckConfig) { @@ -43,9 +45,6 @@ trait Checkers { B: PropF]( f: (A1, A2, A3) => B)( implicit loc: SourceLocation): F[Expectations] = { - implicit val tuple3Show: Show[(A1, A2, A3)] = { - case (a1, a2, a3) => s"(${a1.show},${a2.show},${a3.show})" - } forall(implicitly[Arbitrary[(A1, A2, A3)]].arbitrary)(liftProp( f.tupled)) } @@ -58,10 +57,6 @@ trait Checkers { B: PropF ](f: (A1, A2, A3, A4) => B)( implicit loc: SourceLocation): F[Expectations] = { - implicit val tuple3Show: Show[(A1, A2, A3, A4)] = { - case (a1, a2, a3, a4) => - s"(${a1.show},${a2.show},${a3.show},${a4.show})" - } forall(implicitly[Arbitrary[(A1, A2, A3, A4)]].arbitrary)( liftProp(f.tupled)) } @@ -75,10 +70,6 @@ trait Checkers { B: PropF ](f: (A1, A2, A3, A4, A5) => B)( implicit loc: SourceLocation): F[Expectations] = { - implicit val tuple3Show: Show[(A1, A2, A3, A4, A5)] = { - case (a1, a2, a3, a4, a5) => - s"(${a1.show},${a2.show},${a3.show},${a4.show},${a5.show})" - } forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5)]].arbitrary)( liftProp(f.tupled)) } @@ -93,10 +84,6 @@ trait Checkers { B: PropF ](f: (A1, A2, A3, A4, A5, A6) => B)( implicit loc: SourceLocation): F[Expectations] = { - implicit val tuple3Show: Show[(A1, A2, A3, A4, A5, A6)] = { - case (a1, a2, a3, a4, a5, a6) => - s"(${a1.show},${a2.show},${a3.show},${a4.show},${a5.show},${a6.show})" - } forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5, A6)]].arbitrary)( liftProp(f.tupled)) } @@ -106,6 +93,59 @@ trait Checkers { forall_(gen, liftProp(f)) private def forall_[A: Show](gen: Gen[A], f: A => F[Expectations])( + implicit loc: SourceLocation): F[Expectations] = + Helpers.forall(config, gen, f) + } + + object forall extends PartiallyAppliedForall(checkConfig) { + + /** Configuration for this specific property assertion. */ + def withConfig(config: CheckConfig) = new PartiallyAppliedForall(config) + } +} + +object Checkers { + // These allow us to define functions that go from F[Expectations] + trait Prop[F[_], A] { + def lift(a: A): F[Expectations] + } + + object Prop { + def apply[F[_], B](implicit ev: Prop[F, B]): Prop[F, B] = ev + + implicit def wrap[F[_]: Applicative]: Prop[F, Expectations] = + new Prop[F, Expectations] { + def lift(a: Expectations): F[Expectations] = Applicative[F].pure(a) + } + + implicit def unwrapped[F[_], FE]( + implicit ev: FE <:< F[Expectations]): Prop[F, FE] = + new Prop[F, FE] { + def lift(a: FE): F[Expectations] = ev(a) + } + } + + private def failureMessage(ith: Int, seed: Seed, input: String): String = + s"""Property test failed on try $ith with seed ${seed} and input $input. + |You can reproduce this by adding the following override to your suite: + | + |override def checkConfig = super.checkConfig.withInitialSeed($seed.toOption)""".stripMargin + + private[scalacheck] class PropertyTestError( + ith: Int, + seed: Seed, + input: String, + cause: Throwable) + extends RuntimeException(failureMessage(ith, seed, input), cause) + with NoStackTrace + + object Helpers { + + /** Runs assertions in a property test concurrently. */ + def forall[F[_]: Defer: Concurrent, A: Show]( + config: CheckConfig, + gen: Gen[A], + f: A => F[Expectations])( implicit loc: SourceLocation): F[Expectations] = { val params = Gen.Parameters.default.withNoInitialSeed.withSize( config.maximumGeneratorSize) @@ -130,37 +170,33 @@ trait Checkers { .map { status => status.endResult(config) } } - private def seedStream(initial: Seed): fs2.Stream[F, Seed] = - fs2.Stream.iterate[F, Seed](initial)(_.slide) - } - - object forall extends PartiallyAppliedForall(checkConfig) { - def withConfig(config: CheckConfig) = new PartiallyAppliedForall(config) - } - - private def testOne[T: Show]( - gen: Gen[T], - f: T => F[Expectations])( - params: Gen.Parameters, - seed: Seed): F[TestResult] = { - Defer[F](self.effect).defer { - gen(params, seed) - .traverse(x => f(x).attempt.map(x -> _)) - .map { (x: Option[(T, Either[Throwable, Expectations])]) => - x match { - case Some((_, Right(ex))) if ex.run.isValid => TestResult.Success - case Some((t, Right(ex))) => TestResult.Failure(t.show, ex) - case Some((t, Left(exception: ExpectationFailed))) => - TestResult.Failure(t.show, - Expectations(Validated.invalidNel(exception))) - case Some((t, Left(other))) => TestResult.Exception(t.show, other) - case None => TestResult.Discard + private def testOne[F[_]: Defer: MonadThrow, T: Show]( + gen: Gen[T], + f: T => F[Expectations])( + params: Gen.Parameters, + seed: Seed): F[TestResult] = { + Defer[F].defer { + gen(params, seed) + .traverse(x => f(x).attempt.map(x -> _)) + .map { (x: Option[(T, Either[Throwable, Expectations])]) => + x match { + case Some((_, Right(ex))) if ex.run.isValid => TestResult.Success + case Some((t, Right(ex))) => TestResult.Failure(t.show, ex) + case Some((t, Left(exception: ExpectationFailed))) => + TestResult.Failure( + t.show, + Expectations(Validated.invalidNel(exception))) + case Some((t, Left(other))) => TestResult.Exception(t.show, other) + case None => TestResult.Discard + } } - } + } } - } - private[scalacheck] case class Status[T]( + private def seedStream[F[_]](initial: Seed): fs2.Stream[F, Seed] = + fs2.Stream.iterate[F, Seed](initial)(_.slide) + } + private case class Status[T]( succeeded: Int, discarded: Int, failure: Option[Expectations] @@ -198,28 +234,6 @@ trait Checkers { def start[T] = Status[T](0, 0, None) } -} - -object Checkers { - trait Prop[F[_], A] { - def lift(a: A): F[Expectations] - } - - object Prop { - def apply[F[_], B](implicit ev: Prop[F, B]): Prop[F, B] = ev - - implicit def wrap[F[_]: Applicative]: Prop[F, Expectations] = - new Prop[F, Expectations] { - def lift(a: Expectations): F[Expectations] = Applicative[F].pure(a) - } - - implicit def unwrapped[F[_], FE]( - implicit ev: FE <:< F[Expectations]): Prop[F, FE] = - new Prop[F, FE] { - def lift(a: FE): F[Expectations] = ev(a) - } - } - private sealed trait TestResult private object TestResult { case object Success extends TestResult @@ -229,17 +243,4 @@ object Checkers { case class Exception(input: String, error: Throwable) extends TestResult } - private def failureMessage(ith: Int, seed: Seed, input: String): String = - s"""Property test failed on try $ith with seed ${seed} and input $input. - |You can reproduce this by adding the following override to your suite: - | - |override def checkConfig = super.checkConfig.withInitialSeed($seed.toOption)""".stripMargin - - private class PropertyTestError( - ith: Int, - seed: Seed, - input: String, - cause: Throwable) - extends RuntimeException(failureMessage(ith, seed, input), cause) - with NoStackTrace }