Created
March 17, 2023 00:32
-
-
Save mpilquist/7dd30a44ca2a7fe0cd494d9b04e4f661 to your computer and use it in GitHub Desktop.
Example of integrating scalacheck-effect and scalatest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2001-2013 Artima, Inc. | |
* | |
* 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.scalatestplus.scalacheck | |
import org.scalactic.{FailureMessages => _, Resources => _, UnquotedString => _, _} | |
import NameUtil.getSimpleNameOfAnObjectsClass | |
import org.scalacheck.Prop | |
import org.scalacheck.Prop.Arg | |
import org.scalacheck.Test | |
import org.scalacheck.util.Pretty | |
import org.scalatest.Assertion | |
import scala.collection.mutable | |
//import org.scalatest.Expectation | |
import org.scalatest.Fact | |
import org.scalatest.Succeeded | |
import org.scalatest.exceptions.StackDepthException | |
import org.scalatestplus.scalacheck.FailureMessages.decorateToStringValue | |
import org.scalatest.exceptions.GeneratorDrivenPropertyCheckFailedException | |
import org.scalatest.exceptions.StackDepth | |
import scala.util.Try | |
import scala.annotation.tailrec | |
/** | |
* Supertrait for <code>CheckerAsserting</code> typeclasses, which are used to implement and determine the result | |
* type of [[org.scalatest.prop.GeneratorDrivenPropertyChecks GeneratorDrivenPropertyChecks]]'s <code>apply</code> and <code>forAll</code> method. | |
* | |
* <p> | |
* Currently, an [[org.scalatest.prop.GeneratorDrivenPropertyChecks GeneratorDrivenPropertyChecks]] expression will have result type <code>Assertion</code>, if the function passed has result type <code>Assertion</code>, | |
* else it will have result type <code>Unit</code>. | |
* </p> | |
*/ | |
trait CheckerAsserting2[T] { | |
/** | |
* The result type of the <code>check</code> method. | |
*/ | |
type Result | |
def succeed(result: T): (Boolean, Option[Throwable]) | |
/** | |
* Perform the property check using the given <code>Prop</code> and <code>Test.Parameters</code>. | |
* | |
* @param p the <code>Prop</code> to be used to check | |
* @param prms the <code>Test.Parameters</code> to be used to check | |
* @param prettifier the <code>Prettifier</code> to be used to prettify error message | |
* @param pos the <code>Position</code> of the caller site | |
* @param argNames the list of argument names | |
* @return the <code>Result</code> of the property check. | |
*/ | |
def check(p: Prop, prms: Test.Parameters, prettifier: Prettifier, pos: source.Position, argNames: Option[List[String]] = None): Result | |
def convertTestResult(r: Test.Result, prms: Test.Parameters, prettifier: Prettifier, pos: source.Position, argNames: Option[List[String]] = None): Result | |
} | |
/** | |
* Class holding lowest priority <code>CheckerAsserting</code> implicit, which enables [[org.scalatest.prop.GeneratorDrivenPropertyChecks GeneratorDrivenPropertyChecks]] expressions that have result type <code>Unit</code>. | |
*/ | |
abstract class UnitCheckerAsserting2 { | |
/** | |
* Abstract subclass of <code>CheckerAsserting</code> that provides the bulk of the implementations of <code>CheckerAsserting</code> | |
* <code>check</code> method. | |
*/ | |
/* protected[scalatest]*/ abstract class CheckerAssertingImpl[T] extends CheckerAsserting2[T] { | |
import CheckerAsserting2._ | |
/** | |
* Check the given <code>Prop</code> and <code>Test.Parameters</code> by calling [[http://www.scalacheck.org ScalaCheck]]'s <code>Test.check</code>. | |
* If the check succeeds, call <code>indicateSuccess</code>, else call <code>indicateFailure</code>. | |
* | |
* | |
* @param p the <code>Prop</code> to be used to check | |
* @param prms the <code>Test.Parameters</code> to be used to check | |
* @param prettifier the <code>Prettifier</code> to be used to prettify error message | |
* @param pos the <code>Position</code> of the caller site | |
* @param argNames the list of argument names | |
* @return the <code>Result</code> of the property check. | |
*/ | |
def check(p: Prop, prms: Test.Parameters, prettifier: Prettifier, pos: source.Position, argNames: Option[List[String]] = None): Result = { | |
convertTestResult(Test.check(prms, p), prms, prettifier, pos, argNames) | |
} | |
// This method was extracted from the body of `check`. | |
def convertTestResult(result: Test.Result, prms: Test.Parameters, prettifier: Prettifier, pos: source.Position, argNames: Option[List[String]] = None): Result = { | |
if (!result.passed) { | |
val (args, labels) = argsAndLabels(result) | |
(result.status: @unchecked) match { | |
case Test.Exhausted => | |
val failureMsg = | |
if (result.succeeded == 1) | |
FailureMessages.propCheckExhaustedAfterOne(prettifier, result.discarded) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse("") | |
else | |
FailureMessages.propCheckExhausted(prettifier, result.succeeded, result.discarded) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse("") | |
indicateFailure( | |
sde => failureMsg, | |
failureMsg, | |
args, | |
labels, | |
None, | |
pos | |
) | |
case Test.Failed(scalaCheckArgs, scalaCheckLabels) => | |
val stackDepth = 1 | |
indicateFailure( | |
sde => FailureMessages.propertyException(prettifier, UnquotedString(sde.getClass.getSimpleName)) + "\n" + | |
( sde.failedCodeFileNameAndLineNumberString match { case Some(s) => " (" + s + ")"; case None => "" }) + "\n" + | |
" " + FailureMessages.propertyFailed(prettifier, result.succeeded) + "\n" + | |
( | |
sde match { | |
case sd: StackDepth if sd.failedCodeFileNameAndLineNumberString.isDefined => | |
" " + FailureMessages.thrownExceptionsLocation(prettifier, UnquotedString(sd.failedCodeFileNameAndLineNumberString.get)) + "\n" | |
case _ => "" | |
} | |
) + | |
" " + FailureMessages.occurredOnValues() + "\n" + | |
prettyArgs(getArgsWithSpecifiedNames(argNames, scalaCheckArgs), prettifier) + "\n" + | |
" )" + | |
getLabelDisplay(scalaCheckLabels) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse(""), | |
FailureMessages.propertyFailed(prettifier, result.succeeded) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse(""), | |
scalaCheckArgs, | |
scalaCheckLabels.toList, | |
None, | |
pos | |
) | |
case Test.PropException(scalaCheckArgs, e, scalaCheckLabels) => | |
indicateFailure( | |
sde => FailureMessages.propertyException(prettifier, UnquotedString(e.getClass.getSimpleName)) + "\n" + | |
" " + FailureMessages.thrownExceptionsMessage(prettifier, if (e.getMessage == null) "None" else UnquotedString(e.getMessage)) + "\n" + | |
( | |
e match { | |
case sd: StackDepth if sd.failedCodeFileNameAndLineNumberString.isDefined => | |
" " + FailureMessages.thrownExceptionsLocation(prettifier, UnquotedString(sd.failedCodeFileNameAndLineNumberString.get)) + "\n" | |
case _ => "" | |
} | |
) + | |
" " + FailureMessages.occurredOnValues() + "\n" + | |
prettyArgs(getArgsWithSpecifiedNames(argNames, scalaCheckArgs), prettifier) + "\n" + | |
" )" + | |
getLabelDisplay(scalaCheckLabels) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse(""), | |
FailureMessages.propertyException(prettifier, UnquotedString(e.getClass.getName)) + prms.initialSeed.map(s => "\n" + FailureMessages.initSeed(prettifier, longSeed(s.toBase64))).getOrElse(""), | |
scalaCheckArgs, | |
scalaCheckLabels.toList, | |
Some(e), | |
pos | |
) | |
} | |
} else indicateSuccess(FailureMessages.propertyCheckSucceeded()) | |
} | |
private[scalacheck] def indicateSuccess(message: => String): Result | |
private[scalacheck] def indicateFailure(messageFun: StackDepthException => String, undecoratedMessage: => String, scalaCheckArgs: List[Any], scalaCheckLabels: List[String], optionalCause: Option[Throwable], pos: source.Position): Result | |
} | |
/** | |
* Provides support of [[org.scalatest.enablers.CheckerAsserting CheckerAsserting]] for Unit. Do nothing when the check succeeds, | |
* but throw [[org.scalatest.exceptions.GeneratorDrivenPropertyCheckFailedException GeneratorDrivenPropertyCheckFailedException]] | |
* when check fails. | |
*/ | |
implicit def assertingNatureOfT[T]: CheckerAsserting2[T] { type Result = Unit } = | |
new CheckerAssertingImpl[T] { | |
type Result = Unit | |
def succeed(result: T) = (true, None) | |
private[scalacheck] def indicateSuccess(message: => String): Unit = () | |
private[scalacheck] def indicateFailure(messageFun: StackDepthException => String, undecoratedMessage: => String, scalaCheckArgs: List[Any], scalaCheckLabels: List[String], optionalCause: Option[Throwable], pos: source.Position): Unit = { | |
throw new GeneratorDrivenPropertyCheckFailedException( | |
messageFun, | |
optionalCause, | |
pos, | |
None, | |
undecoratedMessage, | |
scalaCheckArgs, | |
None, | |
scalaCheckLabels.toList | |
) | |
} | |
} | |
} | |
/** | |
* Abstract class that in the future will hold an intermediate priority <code>CheckerAsserting</code> implicit, which will enable inspector expressions | |
* that have result type <code>Expectation</code>, a more composable form of assertion that returns a result instead of throwing an exception when it fails. | |
*/ | |
abstract class ExpectationCheckerAsserting2 extends UnitCheckerAsserting2 { | |
/*implicit def assertingNatureOfExpectation(implicit prettifier: Prettifier): CheckerAsserting[Expectation] { type Result = Expectation } = { | |
new CheckerAssertingImpl[Expectation] { | |
type Result = Expectation | |
def succeed(result: Expectation) = (result.isYes, result.cause) | |
private[scalacheck] def indicateSuccess(message: => String): Expectation = Fact.Yes(message)(prettifier) | |
private[scalacheck] def indicateFailure(messageFun: StackDepthException => String, undecoratedMessage: => String, scalaCheckArgs: List[Any], scalaCheckLabels: List[String], optionalCause: Option[Throwable], pos: source.Position): Expectation = { | |
val gdpcfe = | |
new GeneratorDrivenPropertyCheckFailedException( | |
messageFun, | |
optionalCause, | |
pos, | |
None, | |
undecoratedMessage, | |
scalaCheckArgs, | |
None, | |
scalaCheckLabels.toList | |
) | |
val message: String = gdpcfe.getMessage | |
Fact.No(message)(prettifier) | |
} | |
} | |
}*/ | |
} | |
/** | |
* Companion object to <code>CheckerAsserting</code> that provides two implicit providers, a higher priority one for passed functions that have result | |
* type <code>Assertion</code>, which also yields result type <code>Assertion</code>, and one for any other type, which yields result type <code>Unit</code>. | |
*/ | |
object CheckerAsserting2 extends ExpectationCheckerAsserting2 { | |
/** | |
* Provides support of [[org.scalatest.enablers.CheckerAsserting CheckerAsserting]] for Assertion. Returns [[org.scalatest.Succeeded Succeeded]] when the check succeeds, | |
* but throw [[org.scalatest.exceptions.GeneratorDrivenPropertyCheckFailedException GeneratorDrivenPropertyCheckFailedException]] | |
* when check fails. | |
*/ | |
implicit def assertingNatureOfAssertion: CheckerAsserting2[Assertion] { type Result = Assertion } = { | |
new CheckerAssertingImpl[Assertion] { | |
type Result = Assertion | |
def succeed(result: Assertion) = (true, None) | |
private[scalacheck] def indicateSuccess(message: => String): Assertion = Succeeded | |
private[scalacheck] def indicateFailure(messageFun: StackDepthException => String, undecoratedMessage: => String, scalaCheckArgs: List[Any], scalaCheckLabels: List[String], optionalCause: Option[Throwable], pos: source.Position): Assertion = { | |
throw new GeneratorDrivenPropertyCheckFailedException( | |
messageFun, | |
optionalCause, | |
pos, | |
None, | |
undecoratedMessage, | |
scalaCheckArgs, | |
None, | |
scalaCheckLabels.toList | |
) | |
} | |
} | |
} | |
private[scalacheck] def getArgsWithSpecifiedNames(argNames: Option[List[String]], scalaCheckArgs: List[Arg[Any]]) = { | |
if (argNames.isDefined) { | |
// length of scalaCheckArgs should equal length of argNames | |
val zipped = argNames.get zip scalaCheckArgs | |
zipped map { case (argName, arg) => arg.copy(label = argName) } | |
} | |
else | |
scalaCheckArgs | |
} | |
private[scalacheck] def getLabelDisplay(labels: Set[String]): String = | |
if (labels.size > 0) | |
"\n " + (if (labels.size == 1) Resources.propCheckLabel() else Resources.propCheckLabels()) + "\n" + labels.map(" " + _).mkString("\n") | |
else | |
"" | |
private[scalacheck] def argsAndLabels(result: Test.Result): (List[Any], List[String]) = { | |
val (scalaCheckArgs, scalaCheckLabels) = | |
result.status match { | |
case Test.Proved(args) => (args.toList, List()) | |
case Test.Failed(args, labels) => (args.toList, labels.toList) | |
case Test.PropException(args, _, labels) => (args.toList, labels.toList) | |
case _ => (List(), List()) | |
} | |
val args: List[Any] = for (scalaCheckArg <- scalaCheckArgs.toList) yield scalaCheckArg.arg | |
// scalaCheckLabels is a Set[String], I think | |
val labels: List[String] = for (scalaCheckLabel <- scalaCheckLabels.iterator.toList) yield scalaCheckLabel | |
(args, labels) | |
} | |
// TODO: Internationalize these, and make them consistent with FailureMessages stuff (only strings get quotes around them, etc.) | |
private[scalacheck] def prettyTestStats(result: Test.Result, prettifier: Prettifier) = result.status match { | |
case Test.Proved(args) => | |
"OK, proved property: \n" + prettyArgs(args, prettifier) | |
case Test.Passed => | |
"OK, passed " + result.succeeded + " tests." | |
case Test.Failed(args, labels) => | |
"Falsified after " + result.succeeded + " passed tests:\n" + prettyLabels(labels) + prettyArgs(args, prettifier) | |
case Test.Exhausted => | |
"Gave up after only " + result.succeeded + " passed tests. " + | |
result.discarded + " tests were discarded." | |
case Test.PropException(args, e, labels) => | |
FailureMessages.propertyException(prettifier, UnquotedString(e.getClass.getSimpleName)) + "\n" + prettyLabels(labels) + prettyArgs(args, prettifier) | |
} | |
private[scalacheck] def prettyLabels(labels: Set[String]) = { | |
if (labels.isEmpty) "" | |
else if (labels.size == 1) "Label of failing property: " + labels.iterator.next + "\n" | |
else "Labels of failing property: " + labels.mkString("\n") + "\n" | |
} | |
// | |
// If scalacheck arg contains a type that | |
// decorateToStringValue processes, then let | |
// decorateToStringValue handle it. Otherwise use its | |
// prettyArg method to generate the display string. | |
// | |
// Passes 0 as verbosity value to prettyArg function. | |
// | |
private[scalacheck] def decorateArgToStringValue(arg: Arg[_], prettifier: Prettifier): String = | |
arg.arg match { | |
case null => decorateToStringValue(prettifier, arg.arg) | |
case _: Unit => decorateToStringValue(prettifier, arg.arg) | |
case _: String => decorateToStringValue(prettifier, arg.arg) | |
case _: Char => decorateToStringValue(prettifier, arg.arg) | |
case _: Array[_] => decorateToStringValue(prettifier, arg.arg) | |
case _: mutable.WrappedArray[_] => decorateToStringValue(prettifier, arg.arg) | |
case a if ArrayHelper.isArrayOps(arg.arg) => decorateToStringValue(prettifier, arg.arg) | |
case _: Many[_] => decorateToStringValue(prettifier, arg.arg) | |
case _: scala.collection.GenMap[_, _] => decorateToStringValue(prettifier, arg.arg) | |
case _: Iterable[_] => decorateToStringValue(prettifier, arg.arg) | |
case _: java.util.Collection[_] => decorateToStringValue(prettifier, arg.arg) | |
case _: java.util.Map[_, _] => decorateToStringValue(prettifier, arg.arg) | |
case p: Product if p.productArity > 0 => decorateToStringValue(prettifier, arg.arg) | |
case _ => arg.prettyArg(new Pretty.Params(0)) | |
} | |
private[scalacheck] def prettyArgs(args: List[Arg[_]], prettifier: Prettifier) = { | |
val strs = for((a, i) <- args.zipWithIndex) yield ( | |
" " + | |
(if (a.label == "") "arg" + i else a.label) + | |
" = " + decorateArgToStringValue(a, prettifier) + (if (i < args.length - 1) "," else "") + | |
(if (a.shrinks > 0) " // " + a.shrinks + (if (a.shrinks == 1) " shrink" else " shrinks") else "") | |
) | |
strs.mkString("\n") | |
} | |
def longSeed(s: String): Long = { | |
def fail(s: String): Nothing = throw new IllegalArgumentException(s) | |
def dec(c: Char): Long = | |
if ('A' <= c && c <= 'Z') (c - 'A').toLong | |
else if ('a' <= c && c <= 'z') ((c - 'a') + 26).toLong | |
else if ('0' <= c && c <= '9') ((c - '0') + 52).toLong | |
else if (c == '-') 62L | |
else if (c == '_') 63L | |
else fail(s"illegal Base64 character: $c") | |
val longs = new Array[Long](4) | |
@tailrec def decode(x: Long, shift: Int, i: Int, j: Int): Long = | |
if (i >= 43) { | |
longs(1) | |
} else { | |
val b = dec(s.charAt(i)) | |
if (shift < 58) { | |
decode(x | (b << shift), shift + 6, i + 1, j) | |
} else { | |
longs(j) = x | (b << shift) | |
val sh = 64 - shift | |
decode(b >>> sh, 6 - sh, i + 1, j + 1) | |
} | |
} | |
if (s.length != 44) fail(s"wrong Base64 length: $s") | |
if (s.charAt(43) != '=') fail(s"wrong Base64 format: $s") | |
if (s.charAt(42) == '=') fail(s"wrong Base64 format: $s") | |
decode(0L, 0, 0, 0) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> using lib "org.typelevel::scalacheck-effect:2.0.0-M2" | |
//> using lib "org.scalatest::scalatest:3.2.15" | |
//> using lib "org.scalatestplus::scalacheck-1-17:3.2.15.0" | |
package org.scalatestplus.scalacheck | |
import org.scalactic.Prettifier | |
import org.scalactic.source.Position | |
import org.scalatest.{Assertions, Suite} | |
import org.scalatest.compatible.Assertion | |
import org.scalatest.funsuite.AsyncFunSuite | |
import org.scalacheck.effect.PropF | |
import scala.concurrent.Future | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import org.scalacheck.{Arbitrary, Prop, Gen, Shrink, Test} | |
import org.scalacheck.util.Pretty | |
import cats.MonadError | |
import language.implicitConversions | |
trait ScalacheckEffectSupport extends ScalaCheckPropertyChecks { | |
self: Suite => | |
trait ToFuture[F[_]] { | |
def unsafeRunAsync[A](fa: F[A]): Future[A] | |
} | |
object ToFuture { | |
implicit object FutureToFuture extends ToFuture[Future] { | |
def unsafeRunAsync[A](fa: Future[A]): Future[A] = fa | |
} | |
} | |
def forAllF[F[_], T1, P]( | |
g1: Gen[T1] | |
)( | |
f: T1 => P | |
)(implicit | |
toProp: P => PropF[F], | |
F: MonadError[F, Throwable], | |
runner: ToFuture[F], | |
s1: Shrink[T1], | |
pp1: T1 => Pretty, | |
asserting: CheckerAsserting2[Assertion] { type Result = Assertion }, | |
config: PropertyCheckConfiguration, | |
prettifier: Prettifier, | |
pos: Position | |
): Future[Assertion] = checkPropF(PropF.forAllF(g1)(f)) | |
def forAllF[F[_], T1, P]( | |
f: T1 => P | |
)(implicit | |
toProp: P => PropF[F], | |
F: MonadError[F, Throwable], | |
runner: ToFuture[F], | |
a1: Arbitrary[T1], | |
s1: Shrink[T1], | |
pp1: T1 => Pretty, | |
asserting: CheckerAsserting2[Assertion] { type Result = Assertion }, | |
config: PropertyCheckConfiguration, | |
prettifier: Prettifier, | |
pos: Position | |
): Future[Assertion] = forAllF(a1.arbitrary)(f) | |
protected def checkPropF[F[_]](p: PropF[F])(implicit | |
F: MonadError[F, Throwable], | |
runner: ToFuture[F], | |
asserting: CheckerAsserting2[Assertion] { type Result = Assertion }, | |
config: PropertyCheckConfiguration, | |
prettifier: org.scalactic.Prettifier, | |
pos: org.scalactic.source.Position | |
): Future[Assertion] = { | |
val params = getScalaCheckParams(Nil, config) | |
val result: Future[Test.Result] = runner.unsafeRunAsync(p.check(params)) | |
result.map(asserting.convertTestResult(_, params, prettifier, pos)) | |
} | |
implicit def effectOfAssertionToPropF[F[_]]( | |
fa: F[Assertion] | |
)(implicit F: MonadError[F, Throwable]): PropF[F] = | |
F.void(fa) | |
} | |
class ExampleTest extends AsyncFunSuite with ScalacheckEffectSupport { | |
test("example of a failing test") { | |
forAllF { (x: Int) => | |
Future(x).map(res => assert(res != -res)) | |
} | |
} | |
} | |
@main def myApp = ExampleTest().execute() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ExampleTest: | |
- example of a failing test *** FAILED *** | |
TestFailedException was thrown during property evaluation. | |
Message: 0 equaled 0 | |
Location: (eff.scala:91) | |
Occurred when passed generated values ( | |
arg0 = 0 // 1 shrink | |
) | |
Init Seed: 3855502898383720312 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment