Skip to content

Instantly share code, notes, and snippets.

@mpilquist
Created March 17, 2023 00:32
Show Gist options
  • Save mpilquist/7dd30a44ca2a7fe0cd494d9b04e4f661 to your computer and use it in GitHub Desktop.
Save mpilquist/7dd30a44ca2a7fe0cd494d9b04e4f661 to your computer and use it in GitHub Desktop.
Example of integrating scalacheck-effect and scalatest
/*
* 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)
}
}
//> 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()
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