From 4590703cc301dae0117988eeabe08a6b330020b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Tue, 29 Dec 2020 21:42:34 +0100 Subject: [PATCH 1/7] Test that numeric shrinkers are acyclic, via a strict partial order We all know that we shrink towards 0 and from negative to positive. Formalizing this is simple and strengthens the #244 regression tests. --- .../org/scalacheck/ShrinkSpecification.scala | 128 +++++++++++++++++- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index 97c2a4543..f90e7438b 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -124,10 +124,26 @@ object ShrinkSpecification extends Properties("Shrink") { /* Ensure that shrink[T] terminates. (#244) * - * Let's say shrinking "terminates" when the stream of values - * becomes empty. We can empirically determine the longest possible - * sequence for a given type before termination. (Usually this - * involves using the type's MinValue.) + * Shrinks must be acyclic, otherwise the shrinking process loops. + * + * A cycle is a set of values $x_1, x_2, ..., x_n, x_{n+1} = x_1$ such + * that $shrink(x_i).contains(x_{i+1})$ for all i. If the shrinking to a + * minimal counterexample ever encounters a cycle, it will loop forever. + * + * To prove that a shrink is acyclic you can prove that all shrinks are + * smaller than the shrinkee, for some strict partial ordering (proof: by + * transitivity conclude that x_i < x_i which violates anti-reflexivity.) + * + * Shrinking of numeric types is ordered by magnitude and then sign, where + * positive goes before negative, i.e. x may shrink to -x when x < 0 < -x. + * + * For unsigned types (e.g. Char) this is the standard ordering (<). + * For signed types, m goes before n iff |m| < |n| or m = -n > 0. + * (Be careful about abs(MinValue) representation issues.) + * + * Also, for each shrinkee the stream of shrunk values must be finite. We + * can empirically determine the length of the longest possible stream for a + * given type. Usually this involves using the type's MinValue. * * For example, shrink(Byte.MinValue).toList gives us 15 values: * @@ -191,4 +207,108 @@ object ShrinkSpecification extends Properties("Shrink") { property("shrink(Duration.Undefined)") = Prop(Shrink.shrink(Duration.Undefined: Duration).isEmpty) + + // That was finiteness of a single step of shrinking. Now let's prove that + // you cannot shrink for infinitely many steps, by showing that shrinking + // always goes to smaller values, ordered by magnitude and then sign. + + def orderByMagnitudeAndSign[T]( + abs: T => T, + equiv: (T, T) => Boolean, + lt: (T, T) => Boolean, + zero: T, + n: T, + m: T + ): Boolean = lt(abs(m), abs(n)) || (lt(n, zero) && equiv(m, abs(n))) + + def fractionalMayShrinkTo[T: Fractional](n: T, m: T): Boolean = { + val fractional = implicitly[Fractional[T]] + import fractional.{abs, equiv, lt, zero} + orderByMagnitudeAndSign(abs, equiv, lt, zero, n, m) + } + + def rawIntegralMayShrinkTo[T: Integral](n: T, m: T): Boolean = { + val integral = implicitly[Integral[T]] + import integral.{abs, equiv, lt, zero} + orderByMagnitudeAndSign(abs, equiv, lt, zero, n, m) + } + + def integralMayShrinkTo[T: Integral: TwosComplement](n: T, m: T): Boolean = { + val lowerBound = implicitly[TwosComplement[T]].minValue + val integral = implicitly[Integral[T]] + import integral.{abs, equiv, lt, zero} + + // Note: abs(minValue) = minValue < 0 for two's complement signed types + require(equiv(lowerBound, abs(lowerBound))) + require(lt(abs(lowerBound), zero)) + + // Due to this algebraic issue, we have to special case `lowerBound` + if (n == lowerBound) m != lowerBound + else if (m == lowerBound) false + else rawIntegralMayShrinkTo(n, m) // simple algebra Just Works(TM) + } + + case class TwosComplement[T](minValue: T) + implicit val minByte: TwosComplement[Byte] = TwosComplement(Byte.MinValue) + implicit val minShort: TwosComplement[Short] = TwosComplement(Short.MinValue) + implicit val minInt: TwosComplement[Int] = TwosComplement(Integer.MIN_VALUE) + implicit val minLong: TwosComplement[Long] = TwosComplement(Long.MinValue) + + // Let's first verify that this is in fact a strict partial ordering. + property("integralMayShrinkTo is antireflexive") = + forAllNoShrink { (n: Int) => !integralMayShrinkTo(n, n) } + + val transitive = for { + a <- Arbitrary.arbitrary[Int] + b <- Arbitrary.arbitrary[Int] + if integralMayShrinkTo(a, b) + c <- Arbitrary.arbitrary[Int] + if integralMayShrinkTo(b, c) + } yield integralMayShrinkTo(a, c) + + property("integralMayShrinkTo is transitive") = + forAllNoShrink(transitive.retryUntil(Function.const(true)))(identity) + + // let's now show that shrinks are acyclic for integral types + + property("shrink[Byte] is acyclic") = forAllNoShrink { (n: Byte) => + shrink(n).forall(integralMayShrinkTo(n, _)) + } + + property("shrink[Short] is acyclic") = forAllNoShrink { (n: Short) => + shrink(n).forall(integralMayShrinkTo(n, _)) + } + + property("shrink[Char] is acyclic") = forAllNoShrink { (n: Char) => + shrink(n).forall(rawIntegralMayShrinkTo(n, _)) + } + + property("shrink[Int] is acyclic") = forAllNoShrink { (n: Int) => + shrink(n).forall(integralMayShrinkTo(n, _)) + } + + property("shrink[Long] is acyclic") = forAllNoShrink { (n: Long) => + shrink(n).forall(integralMayShrinkTo(n, _)) + } + + property("shrink[BigInt] is acyclic") = forAllNoShrink { (n: BigInt) => + shrink(n).forall(rawIntegralMayShrinkTo(n, _)) + } + + property("shrink[Float] is acyclic") = forAllNoShrink { (x: Float) => + shrink(x).forall(fractionalMayShrinkTo(x, _)) + } + + property("shrink[Double] is acyclic") = forAllNoShrink { (x: Double) => + shrink(x).forall(fractionalMayShrinkTo(x, _)) + } + + property("shrink[Duration] is acyclic") = forAllNoShrink { (x: Duration) => + shrink(x).forall(y => integralMayShrinkTo(x.toNanos, y.toNanos)) + } + + property("shrink[FiniteDuration] is acyclic") = + forAllNoShrink { (x: FiniteDuration) => + shrink(x).forall(y => integralMayShrinkTo(x.toNanos, y.toNanos)) + } } From ae498a3eb2ef944026de17eee528127493a6123b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Wed, 30 Dec 2020 01:37:14 +0100 Subject: [PATCH 2/7] Optimize shrinking of integral types So long as the test case keeps succeeding, shrink to the next query value of a binary search that searches for the smallest failing value. That is, try the midpoint between the most recent test failure and the previous shrunk value: x => x * (1 - 1/2^k) for natural numbers k. For example, `shrink(32).toList == List(0, 16, 24, 28, 30, 31)`. --- src/main/scala/org/scalacheck/Shrink.scala | 28 +++++++++---------- .../org/scalacheck/ShrinkSpecification.scala | 21 +++++++------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/main/scala/org/scalacheck/Shrink.scala b/src/main/scala/org/scalacheck/Shrink.scala index 63e714e83..fe5d9fae8 100644 --- a/src/main/scala/org/scalacheck/Shrink.scala +++ b/src/main/scala/org/scalacheck/Shrink.scala @@ -238,26 +238,26 @@ object Shrink extends ShrinkLowPriority with ShrinkVersionSpecific { } final class ShrinkIntegral[T](implicit ev: Integral[T]) extends Shrink[T] { - import ev.{ fromInt, gteq, quot, negate, equiv, zero, one } + import ev.{ fromInt, quot, negate, equiv, zero, one, lt, minus, times } val two = fromInt(2) - // see if T supports negative values or not. this makes some - // assumptions about how Integral[T] is defined, which work for - // Integral[Char] at least. we can't be sure user-defined - // Integral[T] instances will be reasonable. - val skipNegation = gteq(negate(one), one) + // We assume that if y > |x| then quot(x, y) == 0, and that 2^k > |x| for + // some value of k, unless 2^k overflows first (which we cope with). - // assumes x is non-zero. - private def halves(x: T): Stream[T] = { - val q = quot(x, two) - if (equiv(q, zero)) Stream(zero) - else if (skipNegation) q #:: halves(q) - else q #:: negate(q) #:: halves(q) + private def approachFromBelow(x: T, i: T): Stream[T] = { + val head = minus(x, quot(x, i)) // approximately x * (1 - 1/2^k) + if (equiv(head, x)) Stream.empty else head #:: { + val j = times(i, two) // i = 1, 2, ..., 2^step + if (lt(i, j)) approachFromBelow(x, j) else Stream.empty + } } - def shrink(x: T): Stream[T] = - if (equiv(x, zero)) Stream.empty[T] else halves(x) + def shrink(x: T): Stream[T] = { + lazy val approach = approachFromBelow(x, one) + if (lt(x, zero) && lt(zero, negate(x))) negate(x) #:: approach + else approach + } } final class ShrinkFractional[T](implicit ev: Fractional[T]) extends Shrink[T] { diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index f90e7438b..ecea72080 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -143,41 +143,42 @@ object ShrinkSpecification extends Properties("Shrink") { * * Also, for each shrinkee the stream of shrunk values must be finite. We * can empirically determine the length of the longest possible stream for a - * given type. Usually this involves using the type's MinValue. + * given type. Usually this involves using the type's MinValue in the case + * of fractional types, or MinValue + 1 for integral types. * - * For example, shrink(Byte.MinValue).toList gives us 15 values: + * For example, shrink(Byte.MinValue + 1).toList gives us 8 values: * - * List(-64, 64, -32, 32, -16, 16, -8, 8, -4, 4, -2, 2, -1, 1, 0) + * List(127, 0, -64, -96, -112, -120, -124, -126) * * Similarly, shrink(Double.MinValue).size gives us 2081. */ property("shrink[Byte].nonEmpty") = - forAllNoShrink((n: Byte) => Shrink.shrink(n).drop(15).isEmpty) + forAllNoShrink((n: Byte) => Shrink.shrink(n).drop(8).isEmpty) property("shrink[Char].nonEmpty") = forAllNoShrink((n: Char) => Shrink.shrink(n).drop(16).isEmpty) property("shrink[Short].nonEmpty") = - forAllNoShrink((n: Short) => Shrink.shrink(n).drop(31).isEmpty) + forAllNoShrink((n: Short) => Shrink.shrink(n).drop(16).isEmpty) property("shrink[Int].nonEmpty") = - forAllNoShrink((n: Int) => Shrink.shrink(n).drop(63).isEmpty) + forAllNoShrink((n: Int) => Shrink.shrink(n).drop(32).isEmpty) property("shrink[Long].nonEmpty") = - forAllNoShrink((n: Long) => Shrink.shrink(n).drop(127).isEmpty) + forAllNoShrink((n: Long) => Shrink.shrink(n).drop(64).isEmpty) property("shrink[Float].nonEmpty") = - forAllNoShrink((n: Float) => Shrink.shrink(n).drop(2081).isEmpty) + forAllNoShrink((n: Float) => Shrink.shrink(n).drop(289).isEmpty) property("shrink[Double].nonEmpty") = forAllNoShrink((n: Double) => Shrink.shrink(n).drop(2081).isEmpty) property("shrink[FiniteDuration].nonEmpty") = - forAllNoShrink((n: FiniteDuration) => Shrink.shrink(n).drop(2081).isEmpty) + forAllNoShrink((n: FiniteDuration) => Shrink.shrink(n).drop(64).isEmpty) property("shrink[Duration].nonEmpty") = - forAllNoShrink((n: Duration) => Shrink.shrink(n).drop(2081).isEmpty) + forAllNoShrink((n: Duration) => Shrink.shrink(n).drop(64).isEmpty) // make sure we handle sentinel values appropriately for Float/Double. From f5a3434c57c6c2ab9f384f9ae724b003965559b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Wed, 30 Dec 2020 16:31:44 +0100 Subject: [PATCH 3/7] Tell MiMa I've removed `org.scalacheck.ShrinkIntegral.skipNegation` --- project/MimaSettings.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index d094ec82c..d52005fe6 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -19,6 +19,7 @@ object MimaSettings { ) private def removedPrivateMethods = Seq( + "org.scalacheck.ShrinkIntegral.skipNegation" ) private def removedPrivateClasses = Seq( From 374db9fc86546b189f2e5ace1b3677306e4c7cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Sun, 28 Mar 2021 19:19:07 +0200 Subject: [PATCH 4/7] Add a comment clarifying acyclic/ordering tests of numeric shrinkers --- src/test/scala/org/scalacheck/ShrinkSpecification.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index ecea72080..ae3b4943c 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -212,6 +212,9 @@ object ShrinkSpecification extends Properties("Shrink") { // That was finiteness of a single step of shrinking. Now let's prove that // you cannot shrink for infinitely many steps, by showing that shrinking // always goes to smaller values, ordered by magnitude and then sign. + // This is a strict partial ordering, hence there can be no cycles. It is + // also a well-ordering on BigInt, and all other types we test are finite, + // hence there can be no infinite regress. def orderByMagnitudeAndSign[T]( abs: T => T, From 85c88eeda3e824557fc33c8f0f38852b8aa73e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Sun, 28 Mar 2021 19:20:12 +0200 Subject: [PATCH 5/7] Simplify Integral/Fractional tests by using common Numeric supertype --- .../org/scalacheck/ShrinkSpecification.scala | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index ae3b4943c..df0572148 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -216,25 +216,18 @@ object ShrinkSpecification extends Properties("Shrink") { // also a well-ordering on BigInt, and all other types we test are finite, // hence there can be no infinite regress. - def orderByMagnitudeAndSign[T]( - abs: T => T, - equiv: (T, T) => Boolean, - lt: (T, T) => Boolean, - zero: T, - n: T, - m: T - ): Boolean = lt(abs(m), abs(n)) || (lt(n, zero) && equiv(m, abs(n))) + def orderByMagnitudeAndSign[T: Numeric](n: T, m: T): Boolean = { + val num = implicitly[Numeric[T]] + import num.{abs, equiv, lt, zero} + lt(abs(m), abs(n)) || (lt(n, zero) && equiv(m, abs(n))) + } def fractionalMayShrinkTo[T: Fractional](n: T, m: T): Boolean = { - val fractional = implicitly[Fractional[T]] - import fractional.{abs, equiv, lt, zero} - orderByMagnitudeAndSign(abs, equiv, lt, zero, n, m) + orderByMagnitudeAndSign(n, m) } def rawIntegralMayShrinkTo[T: Integral](n: T, m: T): Boolean = { - val integral = implicitly[Integral[T]] - import integral.{abs, equiv, lt, zero} - orderByMagnitudeAndSign(abs, equiv, lt, zero, n, m) + orderByMagnitudeAndSign(n, m) } def integralMayShrinkTo[T: Integral: TwosComplement](n: T, m: T): Boolean = { From 294a250924e019c932d177d36fcf55340c6f7558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Sun, 28 Mar 2021 19:42:39 +0200 Subject: [PATCH 6/7] Inline away and rename acyclic `Shrink[Numeric]` test helper methods --- .../org/scalacheck/ShrinkSpecification.scala | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index df0572148..2f311de29 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -216,21 +216,14 @@ object ShrinkSpecification extends Properties("Shrink") { // also a well-ordering on BigInt, and all other types we test are finite, // hence there can be no infinite regress. - def orderByMagnitudeAndSign[T: Numeric](n: T, m: T): Boolean = { + def numericMayShrinkTo[T: Numeric](n: T, m: T): Boolean = { val num = implicitly[Numeric[T]] import num.{abs, equiv, lt, zero} lt(abs(m), abs(n)) || (lt(n, zero) && equiv(m, abs(n))) } - def fractionalMayShrinkTo[T: Fractional](n: T, m: T): Boolean = { - orderByMagnitudeAndSign(n, m) - } - - def rawIntegralMayShrinkTo[T: Integral](n: T, m: T): Boolean = { - orderByMagnitudeAndSign(n, m) - } - - def integralMayShrinkTo[T: Integral: TwosComplement](n: T, m: T): Boolean = { + def twosComplementMayShrinkTo[T: Integral: TwosComplement](n: T, m: T): Boolean = + { val lowerBound = implicitly[TwosComplement[T]].minValue val integral = implicitly[Integral[T]] import integral.{abs, equiv, lt, zero} @@ -242,7 +235,7 @@ object ShrinkSpecification extends Properties("Shrink") { // Due to this algebraic issue, we have to special case `lowerBound` if (n == lowerBound) m != lowerBound else if (m == lowerBound) false - else rawIntegralMayShrinkTo(n, m) // simple algebra Just Works(TM) + else numericMayShrinkTo(n, m) // simple algebra Just Works(TM) } case class TwosComplement[T](minValue: T) @@ -252,60 +245,60 @@ object ShrinkSpecification extends Properties("Shrink") { implicit val minLong: TwosComplement[Long] = TwosComplement(Long.MinValue) // Let's first verify that this is in fact a strict partial ordering. - property("integralMayShrinkTo is antireflexive") = - forAllNoShrink { (n: Int) => !integralMayShrinkTo(n, n) } + property("twosComplementMayShrinkTo is antireflexive") = + forAllNoShrink { (n: Int) => !twosComplementMayShrinkTo(n, n) } val transitive = for { a <- Arbitrary.arbitrary[Int] b <- Arbitrary.arbitrary[Int] - if integralMayShrinkTo(a, b) + if twosComplementMayShrinkTo(a, b) c <- Arbitrary.arbitrary[Int] - if integralMayShrinkTo(b, c) - } yield integralMayShrinkTo(a, c) + if twosComplementMayShrinkTo(b, c) + } yield twosComplementMayShrinkTo(a, c) - property("integralMayShrinkTo is transitive") = + property("twosComplementMayShrinkTo is transitive") = forAllNoShrink(transitive.retryUntil(Function.const(true)))(identity) // let's now show that shrinks are acyclic for integral types property("shrink[Byte] is acyclic") = forAllNoShrink { (n: Byte) => - shrink(n).forall(integralMayShrinkTo(n, _)) + shrink(n).forall(twosComplementMayShrinkTo(n, _)) } property("shrink[Short] is acyclic") = forAllNoShrink { (n: Short) => - shrink(n).forall(integralMayShrinkTo(n, _)) + shrink(n).forall(twosComplementMayShrinkTo(n, _)) } property("shrink[Char] is acyclic") = forAllNoShrink { (n: Char) => - shrink(n).forall(rawIntegralMayShrinkTo(n, _)) + shrink(n).forall(numericMayShrinkTo(n, _)) } property("shrink[Int] is acyclic") = forAllNoShrink { (n: Int) => - shrink(n).forall(integralMayShrinkTo(n, _)) + shrink(n).forall(twosComplementMayShrinkTo(n, _)) } property("shrink[Long] is acyclic") = forAllNoShrink { (n: Long) => - shrink(n).forall(integralMayShrinkTo(n, _)) + shrink(n).forall(twosComplementMayShrinkTo(n, _)) } property("shrink[BigInt] is acyclic") = forAllNoShrink { (n: BigInt) => - shrink(n).forall(rawIntegralMayShrinkTo(n, _)) + shrink(n).forall(numericMayShrinkTo(n, _)) } property("shrink[Float] is acyclic") = forAllNoShrink { (x: Float) => - shrink(x).forall(fractionalMayShrinkTo(x, _)) + shrink(x).forall(numericMayShrinkTo(x, _)) } property("shrink[Double] is acyclic") = forAllNoShrink { (x: Double) => - shrink(x).forall(fractionalMayShrinkTo(x, _)) + shrink(x).forall(numericMayShrinkTo(x, _)) } property("shrink[Duration] is acyclic") = forAllNoShrink { (x: Duration) => - shrink(x).forall(y => integralMayShrinkTo(x.toNanos, y.toNanos)) + shrink(x).forall(y => twosComplementMayShrinkTo(x.toNanos, y.toNanos)) } property("shrink[FiniteDuration] is acyclic") = forAllNoShrink { (x: FiniteDuration) => - shrink(x).forall(y => integralMayShrinkTo(x.toNanos, y.toNanos)) + shrink(x).forall(y => twosComplementMayShrinkTo(x.toNanos, y.toNanos)) } } From e330902780791f81edd36434dfe4a7bf297c5f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6lker?= Date: Sun, 28 Mar 2021 22:05:46 +0200 Subject: [PATCH 7/7] Improve shrinking of TwosComplement.minValue and test for tightness --- src/main/scala/org/scalacheck/Shrink.scala | 25 +++--- .../org/scalacheck/ShrinkSpecification.scala | 81 ++++++++++++++++++- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/main/scala/org/scalacheck/Shrink.scala b/src/main/scala/org/scalacheck/Shrink.scala index fe5d9fae8..5a45a3334 100644 --- a/src/main/scala/org/scalacheck/Shrink.scala +++ b/src/main/scala/org/scalacheck/Shrink.scala @@ -238,23 +238,28 @@ object Shrink extends ShrinkLowPriority with ShrinkVersionSpecific { } final class ShrinkIntegral[T](implicit ev: Integral[T]) extends Shrink[T] { - import ev.{ fromInt, quot, negate, equiv, zero, one, lt, minus, times } + import ev.{ fromInt, quot, negate, equiv, zero, lt, minus } val two = fromInt(2) - // We assume that if y > |x| then quot(x, y) == 0, and that 2^k > |x| for - // some value of k, unless 2^k overflows first (which we cope with). + // We shrink x to ceil(x * (1 - 1/2^i)) for i = 0,1,…. We also shrink x + // to -x if x < 0 < -x (implying x != MinValue for two's complement types). - private def approachFromBelow(x: T, i: T): Stream[T] = { - val head = minus(x, quot(x, i)) // approximately x * (1 - 1/2^k) - if (equiv(head, x)) Stream.empty else head #:: { - val j = times(i, two) // i = 1, 2, ..., 2^step - if (lt(i, j)) approachFromBelow(x, j) else Stream.empty - } + // We assume that x - ((((x/2)/2)/...)/2) = x for some repetition count; + // otherwise shrinking may diverge. It holds if x - 0 = x and 0 is closer + // to x/2 than to x and there are finitely many values y such that 0 is + // closer to y than to x: then the sequence x, x/2, (x/2)/2, ... eventually + // arrives at 0, but then (x - x/2/2/.../2) = x - 0 = x. + + private def bisectFromZeroToX(x: T, current: T): Stream[T] = { + val head = minus(x, current) + + if (equiv(head, x)) Stream.empty + else head #:: bisectFromZeroToX(x, quot(current, two)) } def shrink(x: T): Stream[T] = { - lazy val approach = approachFromBelow(x, one) + lazy val approach = bisectFromZeroToX(x, x) if (lt(x, zero) && lt(zero, negate(x))) negate(x) #:: approach else approach } diff --git a/src/test/scala/org/scalacheck/ShrinkSpecification.scala b/src/test/scala/org/scalacheck/ShrinkSpecification.scala index 2f311de29..d9cc51f0a 100644 --- a/src/test/scala/org/scalacheck/ShrinkSpecification.scala +++ b/src/test/scala/org/scalacheck/ShrinkSpecification.scala @@ -144,11 +144,11 @@ object ShrinkSpecification extends Properties("Shrink") { * Also, for each shrinkee the stream of shrunk values must be finite. We * can empirically determine the length of the longest possible stream for a * given type. Usually this involves using the type's MinValue in the case - * of fractional types, or MinValue + 1 for integral types. + * of fractional types, or MinValue for integral types. * - * For example, shrink(Byte.MinValue + 1).toList gives us 8 values: + * For example, shrink(Byte.MinValue).toList gives us 8 values: * - * List(127, 0, -64, -96, -112, -120, -124, -126) + * List(0, -64, -96, -112, -120, -124, -126, -127) * * Similarly, shrink(Double.MinValue).size gives us 2081. */ @@ -301,4 +301,79 @@ object ShrinkSpecification extends Properties("Shrink") { forAllNoShrink { (x: FiniteDuration) => shrink(x).forall(y => twosComplementMayShrinkTo(x.toNanos, y.toNanos)) } + + // Recursive integral shrinking stops at a success/failure boundary, + // i.e. some m such that m fails and m-1 succeeds if 0 < m and m+1 + // succeeds if m < 0, or shrinks to 0. + // + // Test that shrink(n) contains n-1 if positive or n+1 if negative. + // + // From this our conclusion follows: + // - If 0 < n and n fails and n-1 fails then we can shrink to n-1. + // - If n < 0 and n fails and n+1 fails then we can shrink to n+1. + // In neither case do we stop shrinking at n. + // + // Since shrinking only stops at failing values, we stop shrinking at: + // - Some n such that 0 < n and n fails and n-1 succeeds + // - Some n such that n < 0 and n fails and n+1 succeeds + // - 0 + // which is exactly what we wanted to conclude. + + def stepsByOne[T: Arbitrary: Numeric: Shrink]: Prop = { + val num = implicitly[Numeric[T]] + import num.{equiv, lt, negate, one, plus, zero} + val minusOne = negate(one) + + forAll { + (n: T) => (!equiv(n, zero)) ==> { + val delta = if (lt(n, zero)) one else minusOne + shrink(n).contains(plus(n, delta)) + } + } + } + + property("shrink[Byte](n).contains(n - |n|/n)") = stepsByOne[Byte] + property("shrink[Short](n).contains(n - |n|/n)") = stepsByOne[Short] + property("shrink[Char](n).contains(n - |n|/n)") = stepsByOne[Char] + property("shrink[Int](n).contains(n - |n|/n)") = stepsByOne[Int] + property("shrink[Long](n).contains(n - |n|/n)") = stepsByOne[Long] + property("shrink[BigInt](n).contains(n - |n|/n)") = stepsByOne[BigInt] + + // As a special case of the above, if n succeeds iff lo < n < hi for some + // pair of limits lo <= 0 <= hi, then shrinking stops at lo or hi. Let's + // test this concrete consequence. + + def minimalCounterexample[T: Shrink](ok: T => Boolean, x: T): T = + shrink(x).dropWhile(ok).headOption.fold(x)(minimalCounterexample(ok, _)) + + def findsBoundary[T: Arbitrary: Numeric: Shrink]: Prop = { + val num = implicitly[Numeric[T]] + import num.{lt, lteq, zero} + + def valid(lo: T, hi: T, start: T): Boolean = + lteq(lo, zero) && lteq(zero, hi) && (lteq(start, lo) || lteq(hi, start)) + + forAll(Arbitrary.arbitrary[(T, T, T)].retryUntil((valid _).tupled)) { + case (lo, hi, start) => valid(lo, hi, start) ==> { + val ok = (n: T) => lt(lo, n) && lt(n, hi) + val stop = minimalCounterexample[T](ok, start) + s"($lo, $hi, $start) => $stop" |: (stop == lo || stop == hi) + } + } + } + + property("shrink finds the exact boundary: Byte") = findsBoundary[Byte] + property("shrink finds the exact boundary: Short") = findsBoundary[Short] + property("shrink finds the exact boundary: Int") = findsBoundary[Int] + property("shrink finds the exact boundary: Long") = findsBoundary[Long] + property("shrink finds the exact boundary: BigInt") = findsBoundary[BigInt] + + // Unsigned types are one-sided. Test on the range (0 until limit). + property("shrink finds the exact boundary: Char") = forAll { + (a: Char, b: Char) => + val (limit, start) = (a min b, a max b) + require(limit <= start) + val result = minimalCounterexample[Char](_ < limit, start) + s"(${limit.toInt}, ${start.toInt}) => ${result.toInt}" |: result == limit + } }