diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bb8121 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.cache +/.classpath +/.project +/.settings +/classes/ +target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4f8df9 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +SmallCheck for Scala +==================== + +SmallCheck for Scala is a Scala library for property-based testing. It +is a port of the original Haskell library, +[SmallCheck](http://www.cs.york.ac.uk/fp/smallcheck). + +> Following the lead of **QuickCheck** (Claessen and Hughes 2000), … +> **SmallCheck** also [uses] type-based generators to obtain test-sets +> of finite values for which properties are checked, and report any +> counter-examples found. But instead of using a sample of randomly +> generated values they test properties for all values up to some +> limiting depth, progressively increasing this limit. + +(Runciman et al. 2008). + +The original SmallCheck is available on +[Hackage](http://hackage.haskell.org/package/smallcheck) +and is maintained by +[Roman Cheplyaka](http://github.com/feuerbach) +at +[http://github.com/feuerbach/smallcheck](http://github.com/feuerbach/smallcheck). + +The original property-based testing library for Haskell, +[QuickCheck](http://hackage.haskell.org/package/QuickCheck) +(Claessen and Hughes 2000), has also spawned a port written in Scala: +[Rickard Nilsson](http://github.com/rickynils)’s +[ScalaCheck](https://github.com/rickynils/scalacheck). SmallCheck for Scala +has followed the design of ScalaCheck. + +References +---------- + +Koen Claessen and John Hughes. + QuickCheck: a lightweight tool for random testing of Haskell programs. + In _Proceedings of the fifth ACM SIGPLAN international conference on Functional programming_, + ICFP ’00, pages 268–279. ACM, 2000. doi: + [10.1145/351240.351266](http://doi.acm.org/10.1145/351240.351266) + +Colin Runciman, Matthew Naylor, and Fredrik Lindblad. + SmallCheck and Lazy SmallCheck: automatic exhaustive testing for small values. + In _Proceedings of the first ACM SIGPLAN symposium on Haskell_, + Haskell ’08, pages 37–48. ACM, 2008. doi: + [10.1145/1411286.1411292](http://doi.acm.org/10.1145/1411286.1411292) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..4774b3f --- /dev/null +++ b/build.sbt @@ -0,0 +1,5 @@ +name := "smallcheck4scala" + +version := ".1" + +scalaVersion := "2.9.2" \ No newline at end of file diff --git a/src/main/scala/smallcheck/Drivers.scala b/src/main/scala/smallcheck/Drivers.scala new file mode 100644 index 0000000..dc83548 --- /dev/null +++ b/src/main/scala/smallcheck/Drivers.scala @@ -0,0 +1,88 @@ +package smallcheck + +/** + * Methods to invoke SmallCheck + */ +object Drivers { + import Property.{TestCase, Pass, Fail, Inappropriate} + + /** Run SmallCheck on a property from depth 0 to a given depth d */ + def smallCheck(d: Int, p: Property) = iterCheck(0, Some(d), p) + + /** Run SmallCheck on collection of properties from depth 0 to a given depth d */ + def smallCheck(d: Int, p: Properties) = iterCheck(0, Some(d), p) + + /** Run SmallCheck on a property at a given depth d */ + def depthCheck(d: Int, p: Property) = iterCheck(d, Some(d), p) + + /** Run SmallCheck on collection of properties at a given depth d */ + def depthCheck(d: Int, p: Properties) = iterCheck(d, Some(d), p) + + /** Run SmallCheck on a property interactively */ + def smallCheckI(p: Property) = iterCheck(0, None, p) + + /** Run SmallCheck on collection of properties */ + def smallCheckI(p: Properties) = iterCheck(0, None, p) + + private def iterCheck(dFrom: Int, dToOption: Option[Int], ps: Properties) { + def iter(d: Int) { + println("Depth "+d+":") + var ok = true + for ((name, p) <- ps.properties) { + println(name) + ok = ok & check(dToOption.isEmpty, 0, 0, true, p(d)) + } + dToOption match { + case None => + if (whenUserWishes(" Deeper")) iter(d+1) + case Some(dTo) => + if (ok && d < dTo) iter(d+1) + } + } + iter(dFrom) + } + + private def iterCheck(dFrom: Int, dToOption: Option[Int], p: Property) { + def iter(d: Int) { + println("Depth "+d+":") + val ok = check(dToOption.isEmpty, 0, 0, true, p(d)) + dToOption match { + case None => + if (whenUserWishes(" Deeper")) iter(d+1) + case Some(dTo) => + if (ok && d < dTo) iter(d+1) + } + } + iter(dFrom) + } + + private def check(interactive: Boolean, numTests: Int, numIgnored: Int, ok: Boolean, rs: Seq[TestCase]): Boolean = { + if (rs.isEmpty) { + print(" Completed "+numTests+" test(s)") + println(if (ok) " without failure." else ".") + if (numIgnored > 0) + println(" But "+numIgnored+" did not meet ==> condition.") + ok + } else { + rs.head match { + case TestCase(Inappropriate, _) => + check(interactive, numTests+1, numIgnored+1, ok, rs.tail) + case TestCase(Pass, _) => + check(interactive, numTests+1, numIgnored, ok, rs.tail) + case TestCase(Fail, args) => + println(" Failed test no. "+(numTests+1)+". Test values follow.") + args foreach { arg => println(" " + arg) } + if (interactive && whenUserWishes(" Continue")) + check(interactive, numTests+1, numIgnored, false, rs.tail) + else + false + } + } + } + + private def whenUserWishes(wish: String): Boolean = { + print(wish+"? ") + val reply = readLine() + reply.isEmpty() || reply == "y" + } +} diff --git a/src/main/scala/smallcheck/Properties.scala b/src/main/scala/smallcheck/Properties.scala new file mode 100644 index 0000000..ffbd69a --- /dev/null +++ b/src/main/scala/smallcheck/Properties.scala @@ -0,0 +1,25 @@ +package smallcheck +import scala.collection.mutable.ListBuffer + +/** + * A collection of properties, which itself is a property. + */ +class Properties(val name: String) extends Property { + import Property._ + + private val props = new ListBuffer[(String, Property)] + + private def oneProperty: Property = all((props.map(_._2)):_*) + + def apply(d: Int): Seq[TestCase] = oneProperty(d) + + def properties: Seq[(String, Property)] = props + + def include(ps: Properties) = for ((n, p) <- ps.properties) property(n) = p + + class PropertySpecifier() { + def update(propName: String, p: Property) = props += ((name+"."+propName, p)) + } + + lazy val property = new PropertySpecifier() +} diff --git a/src/main/scala/smallcheck/Property.scala b/src/main/scala/smallcheck/Property.scala new file mode 100644 index 0000000..e288e28 --- /dev/null +++ b/src/main/scala/smallcheck/Property.scala @@ -0,0 +1,431 @@ +package smallcheck +import scala.collection.mutable.ListBuffer + +/** + * A property generates a sequences of test cases for a given depth + */ +trait Property { + import Property._ + + def apply(d: Int): Seq[TestCase] + + /** + * Map the test cases generated by this property + */ + def map(f: TestCase => TestCase) = new Property { + def apply(d: Int) = + Property.this.apply(d).map(f) + } + + /** + * Map the test cases generated by this property to properties + */ + def flatMap(f: (TestCase) => Property) = new Property { + def apply(d: Int) = + Property.this.apply(d).flatMap(f(_).apply(d)) + } + + /** + * Filter (lazily) the test cases generated by this property + */ + def withFilter(p: (TestCase) => Boolean) = new WithFilter(p) + + class WithFilter(p: (TestCase) => Boolean) { + + /** map over a filter */ + def map[B](f: (TestCase) => TestCase) = new Property { + def apply(d: Int): Seq[TestCase] = { + val buffer = new ListBuffer[TestCase] + for (tc <- Property.this.apply(d)) + if (p(tc)) buffer += f(tc) + buffer.toSeq + } + } + + /** flatMap over a filter */ + def flatMap[B](f: (TestCase) => Property) = new Property { + def apply(d: Int): Seq[TestCase] = { + val buffer = new ListBuffer[TestCase] + for (tc <- Property.this.apply(d)) + if (p(tc)) buffer ++= f(tc)(d) + buffer.toSeq + } + } + + /** filter over a filter */ + def withFilter(q: TestCase => Boolean) = new WithFilter(x => p(x) && q(x)) + } + + /** + * Combine this property with that property + * using a function that combines the generated test cases + */ + def combine(that: Property)(f: (TestCase, TestCase) => TestCase) = + for (tcthis <- this; tcthat <- that) yield f(tcthis, tcthat) + + /** + * Combine this property with that property + * so that it holds if and only if both this and that property hold + */ + def &&(p: Property) = combine(p)(_ && _) + + /** + * Combine this property with that property + * so that it holds if and only if either this or that property holds + */ + def ||(p: Property) = combine(p)(_ || _) + + /** + * Combine this property with that property + * so that it holds if and only if this property implies that that property holds + */ + def ==>(that: => Property): Property = + this.flatMap { tcthis: TestCase => + if (tcthis.passed) { + that.map { tcthat: TestCase => + TestCase.merge(tcthis, tcthat, tcthat.result) + } + } else { + Property(tcthis.copy(result = Inappropriate)) + } + } + + /** + * Combine this property with that property + * so that it holds if an only if, this holds if and only if that holds + */ + def <=>(that: Property): Property = + for { + tcthis <- this + tcthat <- that + } yield TestCase.merge(tcthis, tcthat, if (tcthis.result == tcthat.result) Pass else Fail) +} + +/** + * Methods for building properties + */ +object Property { + + /** + * A testable is a function from a depth to a sequence of test cases + */ + type Testable = Function1[Int, Seq[TestCase]] + + object TestResult { + implicit def fromBool(b: Boolean): TestResult = if (b) Pass else Fail + } + + /** + * An algebraic data type for test results + */ + sealed abstract class TestResult { + /** A result is OK if it isn't a failure */ + def isOk: Boolean = this match { + case Fail => false + case _ => true + } + } + + /** A passing test result */ + case object Pass extends TestResult + + /** A failing test result */ + case object Fail extends TestResult + + /** + * An inappropriate test result, + * for when a precondition was unsatisfied + */ + case object Inappropriate extends TestResult + + /** Methods for building test cases */ + object TestCase { + def apply(result: TestResult) = new TestCase(result, Nil) + def merge(tc1: TestCase, tc2: TestCase, result: TestResult) = new TestCase(result, tc1.args ++ tc2.args) + } + + /** + * A test case records the result and the argument for a test + */ + case class TestCase(result: TestResult, args: Seq[String]) { + import TestCase.merge + + /** this test case's result is Ok */ + def resultIsOk: Boolean = result.isOk + + /** this test case passed */ + def passed: Boolean = result == Pass + + /** this test case failed */ + def failed: Boolean = result == Fail + + /** this test case was inapproriate */ + def inappropriate: Boolean = result == Inappropriate + + /** Return a copy of this test case with an additional argument */ + def addArg(s: String): TestCase = copy(args = args.+:(s)) + + /** Return a copy of this test case with an additional argument */ + def addArg(a: Any): TestCase = addArg(a.toString()) + + /** Combine this and that test case by conjunction*/ + def &&(that: TestCase) = (this.result, that.result) match { + case (Fail, _) => this + case (_, Fail) => that + + case (Inappropriate, _) => this + case (_, Inappropriate) => that + + case (Pass, Pass) => merge(this, that, Pass) + } + + /** Combine this and that test case by disjunction */ + def ||(that: TestCase) = (this.result, that.result) match { + case (Fail, Fail) => merge(this, that, Fail) + case (Fail, _) => that + case (_, Fail) => this + + case (Pass, _) => this + case (_, Pass) => that + + case (Inappropriate, Inappropriate) => merge(this, that, Inappropriate) + } + + /** Combine this and that test case by implication */ + def ==>(that: TestCase) = (this.result, that.result) match { + case (Fail, _) => merge(this, that, Inappropriate) + case (Inappropriate, _) => this + case (Pass, _) => merge(this, that, that.result) + } + } + + /** A boolean is a trivial testable value */ + implicit def testableBoolean(b: Boolean): Testable = {d: Int => Seq(TestCase(b, Nil))} + + /** A boolean is a trivial property */ + implicit def propBoolean(b: Boolean): Property = new Property { + def apply(d: Int): Seq[TestCase] = Seq(TestCase(b, Nil)) + } + + /** A property is isomorphic to a testable value */ + implicit def testableProp(p: Property): Testable = p.apply + + /** A property can be built from a testable value */ + def apply(f: Testable): Property = new Property { + def apply(d: Int): Seq[TestCase] = f(d) + } + + /** A property can be built from test cases */ + def apply(tc: TestCase*): Property = new Property { + def apply(d: Int): Seq[TestCase] = Seq(tc:_*) + } + + /** A property that holds if all the properties in a sequence hold */ + def all(ps: Property*): Property = ps.foldLeft(true:Property)(_ && _) + + /** A property that holds if at least one of the properties in a sequence holds */ + def atLeastOne(ps: Property*): Property = ps.foldLeft(false:Property)(_ || _) + + /** + * A property that holds + * for all inputs to a testable unary function + * from the given series + */ + def forAll[A,P](sa: Series[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = new Property { + def apply(d: Int): Seq[TestCase] = + for { + a <- sa(d) + p = f(a) + t = test(p) + tc <- t(d) + } yield tc.addArg(a) + } + + /** + * A property that holds + * for all inputs to a testable binary function + * from the given series + */ + def forAll[A,B,P](sa: Series[A], sb: Series[B]) + (f: (A,B) => P) + (implicit test: P => Testable): Property = + forAll(sa){a:A => forAll(sb){f(a, _:B)}} + + /** + * A property that holds + * for all inputs to a testable ternary function + * from the given series + */ + def forAll[A,B,C,P](sa: Series[A], sb: Series[B], sc: Series[C]) + (f: (A,B,C) => P) + (implicit test: P => Testable): Property = + forAll(sa){a:A => forAll(sb,sc){f(a, _:B, _:C)}} + + /** + * A property that holds + * for all inputs to a testable quaternary function + * from the given series + */ + def forAll[A,B,C,D,P](sa: Series[A], sb: Series[B], sc: Series[C], sd: Series[D]) + (f: (A,B,C,D) => P) + (implicit test: P => Testable): Property = + forAll(sa){a:A => forAll(sb,sc,sd){f(a, _:B, _:C, _:D)}} + + /** + * A property that holds + * for all inputs to a testable quinary function + * from the given series + */ + def forAll[A,B,C,D,E,P](sa: Series[A], sb: Series[B], sc: Series[C], sd: Series[D], se: Series[E]) + (f: (A,B,C,D,E) => P) + (implicit test: P => Testable): Property = + forAll(sa){a:A => forAll(sb,sc,sd,se){f(a, _:B, _:C, _:D, _:E)}} + + /** + * A property that holds + * for all inputs to a testable unary function + */ + def forAll[A,P](f: (A) => P) + (implicit test: P => Testable, s: Series[A]): Property = + forAll(s)(f)(test) + + /** + * A property that holds + * for all inputs to a testable binary function + */ + def forAll[A,B,P](f: (A,B) => P) + (implicit test: P => Testable, sa: Series[A], sb: Series[B]): Property = + forAll{a:A => forAll{f(a, _:B)}} + + /** + * A property that holds + * for all inputs to a testable ternary function + */ + def forAll[A,B,C,P](f: (A,B,C) => P) + (implicit test: P => Testable, sa: Series[A], sb: Series[B], sc: Series[C]): Property = + forAll{a:A => forAll{f(a, _:B, _:C)}} + + /** + * A property that holds + * for all inputs to a testable quaternary function + */ + def forAll[A,B,C,D,P](f: (A,B,C,D) => P) + (implicit test: P => Testable, sa: Series[A], sb: Series[B], sc: Series[C], sd: Series[D]): Property = + forAll{a:A => forAll{f(a, _:B, _:C, _:D)}} + + /** + * A property that holds + * for all inputs to a testable quinary function + */ + def forAll[A,B,C,D,E,P](f: (A,B,C,D,E) => P) + (implicit test: P => Testable, sa: Series[A], sb: Series[B], sc: Series[C], sd: Series[D], se: Series[E]): Property = + forAll{a:A => forAll{f(a, _:B, _:C, _:D, _:E)}} + + /** + * A property that holds + * for all inputs to a testable unary function + * from the given sequence + */ + def forAllElem[A,P](s: Seq[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = + forAll(Series.constant(s))(f)(test) + + private def existance[A,P](unique: Boolean, s: Series[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = new Property { + def apply(d: Int): Seq[TestCase] = { + val witnesses = for { + a <- s(d) + p = f(a) + t = test(p) + if t(d).foldLeft(true)((b, tc) => b && tc.resultIsOk) + } yield a.toString() + val valid = if (unique) witnesses.size == 1 else witnesses.nonEmpty + val arguments = if (valid) + Nil + else if (witnesses.isEmpty) + Seq("non-existence") + else + "non-uniqueness" :: witnesses.take(2).toList + Seq(TestCase(valid, arguments)) + } + } + + /** + * A property that holds + * for at least one input to a testable unary function + * from the given series + */ + def thereExists[A,P](s: Series[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = + existance(false, s)(f) + + /** + * A property that holds + * for exactly one input to a testable unary function + * from the given series + */ + def thereExists1[A,P](s: Series[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = + existance(true, s)(f) + + /** + * A property that holds + * for at least one input to a testable unary function + * from the given sequence + */ + def thereExistsElem[A,P](s: Seq[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = + existance(false, Series.constant(s))(f) + + /** + * A property that holds + * for exactly one input to a testable unary function + * from the given sequence + */ + def thereExists1Elem[A,P](s: Seq[A]) + (f: (A) => P) + (implicit test: P => Testable): Property = + existance(true, Series.constant(s))(f) + + /** + * A property that holds + * for at least one input to a testable unary function + */ + def exists[A,P](f: (A) => P) + (implicit test: P => Testable, s: Series[A]): Property = + thereExists(s)(f) + + /** + * A property that holds + * for exactly one input to a testable unary function + */ + def exists1[A,P](f: (A) => P) + (implicit test: P => Testable, s: Series[A]): Property = + thereExists1(s)(f) + + /** + * A property that holds + * for at least one input to a testable unary function + * at a depth given by a deepening function + */ + def existsDeeperBy[A,P](df: (Int) => Int) + (f: (A) => P) + (implicit test: P => Testable, s: Series[A]): Property = + thereExists(s.deepen(df))(f) + /** + * A property that holds + * for exactly one input to a testable unary function + * at a depth given by a deepening function + */ + def exists1DeeperBy[A,P](df: (Int) => Int) + (f: (A) => P) + (implicit test: P => Testable, s: Series[A]): Property = + thereExists1(s.deepen(df))(f) +} diff --git a/src/main/scala/smallcheck/Series.scala b/src/main/scala/smallcheck/Series.scala new file mode 100644 index 0000000..5544731 --- /dev/null +++ b/src/main/scala/smallcheck/Series.scala @@ -0,0 +1,207 @@ +package smallcheck + +/** + * A series generates a sequence of test values for a given depth + */ +sealed abstract class Series[A] extends Function1[Int, Seq[A]] { + + /** make the sum (union) of this series with that series */ + def ++(that: => Series[A]) = new Series[A] { + def apply(d: Int): Seq[A] = Series.this.apply(d) ++ that.apply(d) + } + + /** make the product of this series with that series */ + def **[B](that: Series[B]): Series[(A, B)] = + for (a <- Series.this; b <- that) yield (a, b) + + /** deepen this series with the transformation f */ + def deepen(f: (Int) => Int) = new Series[A] { + def apply(d: Int): Seq[A] = Series.this.apply(f(d)) + } + + /** map the output of this series */ + def map[B](f: (A) => B) = new Series[B] { + def apply(d: Int): Seq[B] = + Series.this.apply(d).map(f) + } + + /** flatMap the output of this series */ + def flatMap[B](f: (A) => Series[B]) = new Series[B] { + def apply(d: Int): Seq[B] = + Series.this.apply(d).flatMap(f(_).apply(d)) + } + + /** Filter (lazily) the output of this series */ + def withFilter(p: (A) => Boolean) = new WithFilter(p) + + class WithFilter(p: (A) => Boolean) { + import scala.collection.mutable.ListBuffer + + /** map over filter */ + def map[B](f: (A) => B) = new Series[B] { + def apply(d: Int): Seq[B] = { + val buffer = new ListBuffer[B] + for (tc <- Series.this.apply(d)) + if (p(tc)) buffer += f(tc) + buffer.toSeq + } + } + + /** flatMap over filter */ + def flatMap[B](f: (A) => Series[B]) = new Series[B] { + def apply(d: Int): Seq[B] = { + val buffer = new ListBuffer[B] + for (tc <- Series.this.apply(d)) + if (p(tc)) buffer ++= f(tc)(d) + buffer.toSeq + } + } + + /* filter over filter */ + def withFilter(q: A => Boolean) = new WithFilter(x => p(x) && q(x)) + } +} + +object Series { + + /** The wrap a series function as a Series object */ + def apply[A](f: (Int) => Seq[A]) = new Series[A] { + def apply(d: Int): Seq[A] = f.apply(d) + } + + /** The constant series for a sequence of values */ + def constant[A](s: Seq[A]) = new Series[A] { + def apply(d: Int): Seq[A] = s + } + + /** The series for a single value (nullary function) */ + def cons0[A](a: A) = new Series[A] { + def apply(d:Int): Seq[A] = Seq(a) + } + + /** The series for a unary function */ + def cons1[A,B](f: (A) => B)(implicit sa: Series[A]) = new Series[B] { + def apply(d: Int): Seq[B] = if (d>0) for (a <- sa(d-1)) yield f(a) else Nil + } + + /** The series for a binary function */ + def cons2[A,B,C](f: (A,B) => C)(implicit sa: Series[A], sb: Series[B]) = + new Series[C] { + def apply(d: Int): Seq[C] = + if (d>0) { + for { + a <- sa(d-1) + b <- sb(d-1) + } yield f(a,b) + } else { + Nil + } + } + + /** The series for a ternary function */ + def cons3[A,B,C,D](f: (A,B,C) => D) + (implicit sa: Series[A], sb: Series[B], sc: Series[C]) = + new Series[D] { + def apply(d: Int): Seq[D] = + if (d > 0) { + for { + a <- sa(d-1) + b <- sb(d-1) + c <- sc(d-1) + } yield f(a,b,c) + } else { + Nil + } + } + + /** The product of a series with itself */ + def double[A](implicit s: Series[A]): Series[(A, A)] = s ** s + + /** The series of Unit */ + implicit lazy val seriesUnit = new Series[Unit] { + def apply(d: Int): Seq[Unit] = Seq(()) + } + + /** The series of Boolean */ + implicit lazy val seriesBool = new Series[Boolean] { + def apply(d: Int): Seq[Boolean] = Seq(true, false) + } + + /** The series of Int */ + implicit lazy val seriesInt = new Series[Int] { + def apply(d: Int): Seq[Int] = for (i <- -d to d) yield i + } + + /** The series of Long */ + implicit lazy val seriesLong: Series[Long] = seriesInt.map(_.toLong) + + /** The series of Byte */ + implicit lazy val seriesByte = new Series[Byte] { + def apply(d: Int): Seq[Byte] = { + val d2 = scala.math.min(d, Byte.MaxValue) + for (i <- -d2 to d2) yield i.toByte + } + } + + /** The series of Short */ + implicit lazy val seresShort = new Series[Short] { + def apply(d: Int): Seq[Short] = { + val d2 = scala.math.min(d, Short.MaxValue) + for (i <- -d2 to d2) yield i.toShort + } + } + + /** The series of Double */ + implicit lazy val seriesDouble: Series[Double] = + for { + sig <- seriesInt + exp <- seriesInt + if sig % 2 == 1 || sig == 0 && exp == 0 + } yield sig.toDouble * scala.math.pow(2.0, exp.toDouble) + + /** The series of Float */ + implicit lazy val seriesFloat: Series[Float] = seriesDouble.map(_.toFloat) + + /** The series of Char */ + implicit lazy val serialChar = new Series[Char] { + def apply(d: Int): Seq[Char] = ('a' to 'z') take (d+1) + } + + /** The series of Option[A] for series of A */ + implicit def seriesOption[A](implicit s: Series[A]): Series[Option[A]] = + cons0(None:Option[A]) ++ cons1(Some(_:A)) + + /** The series of Either[A,B] for series of A and B */ + implicit def seriesEither[A,B](implicit + sa: Series[A], sb: Series[B] + ): Series[Either[A,B]] = cons1(Left(_:A):Either[A,B]) ++ cons1(Right(_:B)) + + /** The series of List[A] for series of A */ + implicit def seriesList[A](implicit s: Series[A]): Series[List[A]] = + cons0(List.empty[A]) ++ cons2((h:A, t:List[A]) => h :: t) + + /** The series of Stream[A] for series of A */ + implicit def seriesStream[A](implicit s: Series[A]): Series[Stream[A]] = + cons0(Stream.empty[A]) ++ cons2(Stream.cons(_:A,_: Stream[A])) + + /** The series of String */ + implicit def seriesString(implicit s: Series[List[Char]]): Series[String] = + s.map(_.mkString) + + /** The series of tuples */ + implicit def seriesTuple2[A,B](implicit + sa: Series[A], sb: Series[B] + ) = sa ** sb + + /** The series of triples */ + implicit def seriesTuple3[A,B,C](implicit + sa: Series[A], sb: Series[B], sc: Series[C] + ): Series[(A,B,C)] = + for (a <- sa; b <- sb; c <- sc) yield (a,b,c) + + /** The series of quadruples */ + implicit def seriesTuple4[A,B,C,D](implicit + sa: Series[A], sb: Series[B], sc: Series[C], sd: Series[D] + ): Series[(A,B,C,D)] = + for (a <- sa; b <- sb; c <- sc; d <- sd) yield (a,b,c,d) +} diff --git a/src/test/scala/smallcheck/ListProperties.scala b/src/test/scala/smallcheck/ListProperties.scala new file mode 100644 index 0000000..64375fd --- /dev/null +++ b/src/test/scala/smallcheck/ListProperties.scala @@ -0,0 +1,52 @@ +package smallcheck + +/** + * Examples from the original SmallCheck + */ +object ListProperties extends Properties("List properties") { + import Property._ + + property("rev-rev") = + forAll { (xs: Stream[Int]) => + xs == xs.reverse.reverse + } + + def isPrefix(xs: List[Int], ys: List[Int]): Boolean = (xs, ys) match { + case (Nil, _) => true + case (_, Nil) => false + case (x::xs, y::ys) => x == y || isPrefix(xs, ys) + } + + property("prefix-complete") = + forAll { (xs: List[Int], ys: List[Int]) => + isPrefix(xs, xs ++ ys) + } + + property("prefix-sound") = + forAll { (xs: List[Int], ys: List[Int]) => + isPrefix(xs, ys) ==> + exists { (zs: List[Int]) => ys == (xs ++ zs) } + } + + property("union-1") = + forAll { (xs: List[Boolean], ys: List[Boolean]) => + exists { (zs: List[Boolean]) => + forAll { (b: Boolean) => + zs.contains(b) == (xs.contains(b) || ys.contains(b)) + } + } + } + + property("union-2") = + forAll { (xs: Stream[Boolean], ys: Stream[Boolean]) => + existsDeeperBy(_*2) { (zs: Stream[Boolean]) => + forAll { (b: Boolean) => + zs.contains(b) == (xs.contains(b) || ys.contains(b)) + } + } + } + + def main(args: Array[String]): Unit = { + Drivers.smallCheckI(ListProperties) + } +} diff --git a/src/test/scala/smallcheck/NumberProperties.scala b/src/test/scala/smallcheck/NumberProperties.scala new file mode 100644 index 0000000..07507db --- /dev/null +++ b/src/test/scala/smallcheck/NumberProperties.scala @@ -0,0 +1,46 @@ +package smallcheck + +/** + * Examples from the original SmallCheck + */ +object NumberProperties extends Properties("Number properties") { + import Property._ + import Series.{seriesList} + + val primes: Stream[Int] = { + def sieve(s: Stream[Int]): Stream[Int] = s match { + case p #:: t => + p #:: sieve(for (x <- t if p*p > x || x % p > 0) yield x) + } + sieve(Stream.from(2)) + } + + val seriesNat = Series { (d: Int) => + for (i <- 0 to d) yield i + } + + val seriesPrimes = Series { (d: Int) => + primes.take(d) + } + + property("primes1") = + forAll(seriesNat) { n => + n > 1 ==> + forAll(seriesPrimes) { p => + p % n > 0 || p == n + } + } + + property("primes2") = + forAll(seriesNat) { n => + n > 0 ==> + thereExists1(seriesList(seriesNat)) { exps => + (exps.isEmpty || exps.last != 0) && + n == ((primes, exps).zipped map (scala.math.pow(_,_))).product.toInt + } + } + + def main(args: Array[String]): Unit = { + Drivers.smallCheckI(NumberProperties) + } +}