diff --git a/build.sbt b/build.sbt index a713d18..fb70683 100644 --- a/build.sbt +++ b/build.sbt @@ -4,16 +4,13 @@ ThisBuild / developers += tlGitHubDev("mpilquist", "Michael Pilquist") ThisBuild / startYear := Some(2021) ThisBuild / crossScalaVersions := List("3.3.4", "2.12.20", "2.13.15") -ThisBuild / tlVersionIntroduced := Map("3" -> "1.0.2") lazy val root = tlCrossRootProject.aggregate(core, munit) lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "scalacheck-effect", - tlFatalWarnings := false - ) - .settings( + tlVersionIntroduced := Map("3" -> "1.0.2"), libraryDependencies ++= List( "org.scalacheck" %%% "scalacheck" % "1.17.1", "org.typelevel" %%% "cats-core" % "2.11.0" @@ -23,12 +20,24 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) lazy val munit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "scalacheck-effect-munit", - testFrameworks += new TestFramework("munit.Framework") + testFrameworks += new TestFramework("munit.Framework"), + tlVersionIntroduced := Map("3" -> "1.0.2"), + libraryDependencies ++= List( + "org.scalameta" %%% "munit-scalacheck" % "1.0.0-M11", + "org.typelevel" %%% "cats-effect" % "3.5.7" % Test + ) ) .dependsOn(core) + +lazy val specs2 = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( + name := "scalacheck-effect-specs2", + tlVersionIntroduced := Map("3" -> "2.0.0-M3", "2.13" -> "2.0.0-M3", "2.12" -> "2.0.0-M3"), + startYear := Some(2024), libraryDependencies ++= List( - "org.scalameta" %%% "munit-scalacheck" % "1.0.0-M11", - "org.typelevel" %%% "cats-effect" % "3.5.7" % Test + "org.specs2" %%% "specs2-scalacheck" % "4.20.5", + "org.typelevel" %%% "cats-effect-testing-specs2" % "1.6.0-2-9144a42-SNAPSHOT", + "org.typelevel" %%% "cats-effect" % "3.5.7" ) ) + .dependsOn(core) diff --git a/core/shared/src/main/scala/org/scalacheck/effect/PropF.scala b/core/shared/src/main/scala/org/scalacheck/effect/PropF.scala index 2724ae6..c3e7d5a 100644 --- a/core/shared/src/main/scala/org/scalacheck/effect/PropF.scala +++ b/core/shared/src/main/scala/org/scalacheck/effect/PropF.scala @@ -17,12 +17,11 @@ package org.scalacheck.effect import scala.collection.immutable.Stream -import scala.collection.immutable.Stream.#:: - import cats.MonadError -import cats.implicits._ +import cats.syntax.all.* import org.scalacheck.{Arbitrary, Gen, Prop, Shrink, Test} import org.scalacheck.util.{FreqMap, Pretty} +import org.typelevel.scalaccompat.annotation.* /** An effectful property. * @@ -527,6 +526,8 @@ object PropF { a8.arbitrary )(f) + @nowarn213("""msg=(?:class|object) Stream in package (?:scala\.collection\.)?immutable is deprecated \(since 2.13.0\): Use LazyList \(which is fully lazy\) instead of Stream \(which has a lazy tail only\)""") + @nowarn3("""msg=(?:class|object) Stream in package (?:scala\.collection\.)?immutable is deprecated since 2\.13\.0: Use LazyList \(which is fully lazy\) instead of Stream \(which has a lazy tail only\)""") def forAllShrinkF[F[_], T, P]( gen: Gen[T], shrink: T => Stream[T] @@ -538,6 +539,8 @@ object PropF { pp: T => Pretty ): PropF[F] = PropF[F] { prms0 => + import scala.collection.immutable.Stream.#:: + val (prms, seed) = Prop.startSeed(prms0) val gr = gen.doApply(prms, seed) val labels = gr.labels.mkString(",") diff --git a/specs2/shared/src/main/scala/org/scalacheck/effect/specs2/Specs2ScalaCheckEffect.scala b/specs2/shared/src/main/scala/org/scalacheck/effect/specs2/Specs2ScalaCheckEffect.scala new file mode 100644 index 0000000..1761523 --- /dev/null +++ b/specs2/shared/src/main/scala/org/scalacheck/effect/specs2/Specs2ScalaCheckEffect.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2024 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 org.scalacheck.effect +package specs2 + +import cats.* +import cats.effect.testing.UnsafeRun +import cats.effect.testing.specs2.{AsFutureResult, CatsEffect} +import cats.syntax.all.* +import org.scalacheck.effect.PropF.* +import org.scalacheck.rng.Seed +import org.scalacheck.util.{FreqMap, Pretty} +import org.scalacheck.{Gen, Test} +import org.specs2.ScalaCheck +import org.specs2.execute.{Result, *} +import org.specs2.scalacheck.Parameters +import org.specs2.scalacheck.PrettyDetails.collectDetails + +import scala.annotation.tailrec +import scala.concurrent.* +import scala.util.control.NonFatal + +trait Specs2ScalaCheckEffect extends ScalaCheck { this: CatsEffect => + @tailrec + private def specs2ResultToPropF[F[_]: MonadThrow]( + result: Result + ): org.scalacheck.effect.PropF[F] = + result match { + case Success(_, _) => passed[F] + case Skipped(_, _) => undecided + case Pending(_) => undecided + case Failure(_, _, _, _) => falsified + case Error(_, t) => exception(t) + case DecoratedResult(_, r) => specs2ResultToPropF(r) + } + + implicit def propFAsFutureResult[F[_]: Functor: UnsafeRun](implicit + initialParameters: Parameters, + prettyFreqMap: FreqMap[Set[Any]] => Pretty + ): AsFutureResult[PropF[F]] = + new AsFutureResult[PropF[F]] { + override def asResult(t: => PropF[F]): Future[Result] = { + // copied and slightly tweaked from + // https://github.com/etorreborre/specs2/blob/8a259bf12a2b35d9cd389e607379df740e64c8bd/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala#L45-L51 + lazy val (parameters, initialSeed) = initialParameters.testParameters.initialSeed match { + case Some(sd) => (initialParameters, sd) + case None => + val sd = Seed.random() + (initialParameters.copy(seed = Option(sd)), sd) + } + + UnsafeRun[F].unsafeToFuture { + t.check(parameters.testParameters, Gen.Parameters.default.withInitialSeed(initialSeed)) + .map { result => + // copied and slightly tweaked from + // https://github.com/etorreborre/specs2/blob/8a259bf12a2b35d9cd389e607379df740e64c8bd/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala#L58-L104 + val prettyTestResult = prettyResult(result, parameters, initialSeed, prettyFreqMap)( + parameters.prettyParams + ) + val testResult = if (parameters.prettyParams.verbosity == 0) "" else prettyTestResult + + result match { + case Test.Result(Test.Passed, succeeded, _, _, _) => + Success(prettyTestResult, testResult, succeeded) + + case Test.Result(Test.Proved(_), succeeded, _, _, _) => + Success(prettyTestResult, testResult, succeeded) + + case Test.Result(Test.Exhausted, _, _, _, _) => + Failure(prettyTestResult) + + case Test.Result(Test.Failed(_, _), _, _, fq, _) => + new Failure(prettyTestResult, details = collectDetails(fq)) { + // the location is already included in the failure message + override def location = "" + } + + case Test.Result(Test.PropException(args, ex, labels), _, _, _, _) => + ex match { + case FailureException(f) => + // in that case we want to represent a normal failure + val failedResult = + prettyResult( + result.copy(status = Test.Failed(args, labels)), + parameters, + initialSeed, + prettyFreqMap + )(parameters.prettyParams) + Failure( + failedResult + "\n> " + f.message, + details = f.details, + stackTrace = f.stackTrace + ) + + case DecoratedResultException(DecoratedResult(_, f)) => + // in that case we want to represent a normal failure + val failedResult = + prettyResult( + result.copy(status = Test.Failed(args, labels)), + parameters, + initialSeed, + prettyFreqMap + )(parameters.prettyParams) + f.updateMessage(failedResult + "\n>\n" + f.message) + + case e: AssertionError => + val failedResult = prettyResult( + result.copy(status = Test.Failed(args, labels)), + parameters, + initialSeed, + prettyFreqMap + )(parameters.prettyParams) + Failure( + failedResult + "\n> " + e.getMessage, + stackTrace = e.getStackTrace.toList + ) + + case SkipException(s) => s + case PendingException(p) => p + case NonFatal(t) => Error(prettyTestResult + showCause(t), t) + case _ => throw ex + } + } + } + } + } + } + + implicit def effectOfAsResultToPropF[F[_]: MonadThrow, R: AsResult](fu: F[R]): PropF[F] = + Suspend { + val asResult = Functor[F].map(fu)(AsResult[R](_)) + Functor[F].map(asResult)(specs2ResultToPropF(_)) + } + +}