All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hedgehog.core.PropertyT.scala Maven / Gradle / Ivy

The newest version!
package hedgehog.core

import hedgehog._
import hedgehog.predef._

case class Name(value: String)

object Name {

  // Yes, ugly, but makes for a nicer API
  implicit def Name2String(s: String): Name =
    Name(s)
}

sealed trait Log
case class ForAll(name: Name, value: String) extends Log
case class Info(value: String) extends Log
case class Error(value: Exception) extends Log

object Log {

  implicit def String2Log(s: String): Log =
    Info(s)
}

/** A record containing the details of a test run. */
case class Journal(
    logs: List[Log]
  , coverage: Coverage[Cover]
  ) {

  def ++(o: Journal): Journal =
    Journal(logs ++ o.logs, Coverage.union(coverage, o.coverage)(_ ++ _))

  def log(log: Log): Journal =
    copy(logs = logs ++ List(log))
}

object Journal {

  def empty: Journal =
    Journal(Nil, Coverage.empty)
}

case class PropertyConfig(
    testLimit: SuccessCount
  , discardLimit: DiscardCount
  , shrinkLimit: ShrinkLimit
  )

object PropertyConfig {

  def default: PropertyConfig =
    PropertyConfig(SuccessCount(100), DiscardCount(100), ShrinkLimit(1000))
}

case class PropertyT[A](
    run: GenT[(Journal, Option[A])]
  ) {

  def map[B](f: A => B): PropertyT[B] =
    copy(run = run.map(x =>
      try {
        x.copy(_2 = x._2.map(f))
      } catch {
        // Forgive me, I'm assuming this breaks the Functor laws
        // If there's a better and non-law breaking of handling this we should remove this
        case e: Exception =>
          (x._1.log(Error(e)), None)
      }
    ))

  def flatMap[B](f: A => PropertyT[B]): PropertyT[B] =
    copy(run = run.flatMap(x =>
      x._2.fold(
       GenT.GenApplicative.point((x._1, Option.empty[B]))
      )(a =>
        try {
          f(a).run.map(y => (x._1 ++ y._1, y._2))
        } catch {
          // Forgive me, I'm assuming this breaks the Monad laws
          // If there's a better and non-law breaking of handling this we should remove this
          case e: Exception =>
            Gen.constant((x._1.log(Error(e)), None))
        }
      )
    ))

  /** Records the proportion of tests which satisfy a given condition. */
  def cover(minimum: CoverPercentage, name: LabelName, covered: A => Cover): PropertyT[A] =
    flatMap(a =>
      propertyT.cover(Label(name, minimum, covered(a)))
        .map(_ => a)
    )

  /** Records the proportion of tests which satisfy a given condition. */
  def classify(name: LabelName, covered: A => Cover): PropertyT[A] =
    cover(0, name, covered)

  /**
   * Add a label for each test run.
   * It produces a table showing the percentage of test runs that produced each label.
   */
  def label(name: LabelName): PropertyT[A] =
    cover(0, name, _ => true)

  /** Like 'label', but uses the `toString` value as the label. */
  def collect: PropertyT[A] =
    flatMap(a =>
      propertyT.cover(Label(a.toString, 0, Cover.Cover))
        .map(_ => a)
    )
}

object PropertyT {

  implicit val PropertyMonad: Monad[PropertyT] =
    new Monad[PropertyT] {
      override def map[A, B](fa: PropertyT[A])(f: A => B): PropertyT[B] =
        fa.map(f)
      override def point[A](a: => A): PropertyT[A] =
        propertyT.hoist((Journal.empty, a))
      override def ap[A, B](fa: => PropertyT[A])(f: => PropertyT[A => B]): PropertyT[B] =
        PropertyT(Applicative.zip(fa.run, f.run)
          .map { case ((l1, oa), (l2, oab)) => (l2 ++ l1, oab.flatMap(y => oa.map(y(_)))) }
        )
      override def bind[A, B](fa: PropertyT[A])(f: A => PropertyT[B]): PropertyT[B] =
        fa.flatMap(f)
    }
}

trait PropertyTReporting {

  @annotation.tailrec
  final def takeSmallestG[A, B](n: ShrinkCount, slimit: ShrinkLimit, t: Tree[A])(p: A => Boolean)(e: (ShrinkCount, A) => B): B = {
    if (n.value < slimit.value && p(t.value)) {
      findMap(t.children.value)(m => if (p(m.value)) some(m) else Option.empty) match {
        case None =>
          e(n, t.value)

        case Some(m) =>
          takeSmallestG(n.inc, slimit, m)(p)(e)
      }
    } else {
      e(n, t.value)
    }
  }

  def takeSmallest(n: ShrinkCount, slimit: ShrinkLimit, t: Tree[Option[(Journal, Option[Result])]]): Status =
    takeSmallestG(n, slimit, t) {
      case None =>
        false

      case Some((_, r)) =>
        r.forall(!_.success)
    } {
      case (_, None) =>
        Status.gaveUp

      case (n, Some((j, r))) =>
        if (r.forall(!_.success))
          Status.failed(n, j.logs ++ r.map(_.logs).getOrElse(Nil))
        else
          Status.ok
    }

  def takeSmallestExample(n: ShrinkCount, slimit: ShrinkLimit, name: LabelName, t: Tree[Option[(Journal, Option[Result])]]): List[Log] =
    takeSmallestG(n, slimit, t) {
      case None =>
        false

      case Some((j, r)) =>
        r.exists(_.success) && Coverage.covers(j.coverage, name)
    } {
      case (_, None) =>
        Nil

      case (_, Some((j, _))) =>
        j.logs
    }

  def report(config: PropertyConfig, size0: Option[Size], seed0: Seed, p: PropertyT[Result]): Report = {
    // Increase the size proportionally to the number of tests to ensure better coverage of the desired range
    val sizeInc = Size(Math.max(1, Size.max / config.testLimit.value))
    // Start the size at whatever remainder we have to ensure we run with "max" at least once
    val sizeInit = Size((Size.max % Math.min(config.testLimit.value, Size.max)) + sizeInc.value)
    @annotation.tailrec
    def loop(successes: SuccessCount, discards: DiscardCount, size: Size, seed: Seed, coverage: Coverage[CoverCount], examples: Examples): Report =
      if (successes.value >= config.testLimit.value)
        // we've hit the test limit
        Coverage.split(coverage, successes) match {
          case (_, Nil) =>
            if (examples.examples.exists(_._2.isEmpty))
              Report(successes, discards, coverage, examples, Status.failed(ShrinkCount(0), List("Insufficient examples.")))
            else
              Report(successes, discards, coverage, examples, OK)
          case _ =>
            Report(successes, discards, coverage, examples, Status.failed(ShrinkCount(0), List("Insufficient coverage.")))
        }
      else if (discards.value >= config.discardLimit.value)
        Report(successes, discards, coverage, examples, GaveUp)
      else {
        val x =
          try {
            p.run.run(size, seed)
          } catch {
            case e: Exception =>
              Property.error(e).run.run(size, seed)
          }
        val t = x.map(_._2)
        x.value._2 match {
          case None =>
            loop(successes, discards.inc, size.incBy(sizeInc), x.value._1, coverage, examples)

          case Some((j, r)) =>
            if (r.forall(!_.success)) {
              Report(successes, discards, coverage, examples, takeSmallest(ShrinkCount(0), config.shrinkLimit, t))
            } else {
              val coverage2 = Coverage.union(Coverage.count(j.coverage), coverage)(_ + _)
              val examples2 =
                Examples.addTo(examples, Coverage.labels(j.coverage)) { name =>
                  if (Coverage.covers(j.coverage, name))
                    takeSmallestExample(ShrinkCount(0), config.shrinkLimit, name, t)
                  else
                    Nil
                }
              loop(successes.inc, discards, size.incBy(sizeInc), x.value._1, coverage2, examples2)
            }
        }
      }
    loop(SuccessCount(0), DiscardCount(0), size0.getOrElse(sizeInit), seed0, Coverage.empty, Examples.empty)
  }

  def recheck(config: PropertyConfig, size: Size, seed: Seed)(p: PropertyT[Result]): Report =
    report(config.copy(testLimit = SuccessCount(1)), Some(size), seed, p)
}

/**********************************************************************/
// Reporting

/** The numbers of times a property was able to shrink after a failing test. */
case class ShrinkCount(value: Int) {

  def inc: ShrinkCount =
    ShrinkCount(value + 1)
}

/** The number of shrinks to try before giving up on shrinking. */
case class ShrinkLimit(value: Int)

/** The number of tests a property ran successfully. */
case class SuccessCount(value: Int) {

  def inc: SuccessCount =
    SuccessCount(value + 1)
}

object SuccessCount {

  implicit def Int2SuccessCount(i: Int): SuccessCount =
    SuccessCount(i)
}

/** The number of tests a property had to discard. */
case class DiscardCount(value: Int) {

  def inc: DiscardCount =
    DiscardCount(value + 1)
}

/**
 * The status of a property test run.
 *
 * In the case of a failure it provides the seed used for the test, the
 * number of shrinks, and the execution log.
 */
sealed trait Status
case class Failed(shrinks: ShrinkCount, log: List[Log]) extends Status
case object GaveUp extends Status
case object OK extends Status

object Status {

  def failed(count: ShrinkCount, log: List[Log]): Status =
    Failed(count, log)

  val gaveUp: Status =
    GaveUp

  val ok: Status =
    OK
}

case class Report(tests: SuccessCount, discards: DiscardCount, coverage: Coverage[CoverCount], examples: Examples, status: Status)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy