Skip to content

Commit 7bb1a47

Browse files
authored
Merge pull request #911 from leviramsey/spin-again-like-we-did-last-summer
2 parents 8689ae7 + 58a8620 commit 7bb1a47

File tree

2 files changed

+68
-4
lines changed

2 files changed

+68
-4
lines changed

core/jvm/src/test/scala/org/scalacheck/TestSpecification.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,28 @@ object TestSpecification extends Properties("Test") {
190190
val p1 = Prop(unique.size > 1) :| s"saw $n duplicate values: $unique"
191191
p0 && p1
192192
}
193+
194+
property("initialSeed is used and then updated when varying RNG spins") = {
195+
val seed = rng.Seed.fromBase64("aaaaa_mr05Z_DCbd2PyUolC0h93iH1MQwIdnH2UuI4L=").get
196+
val gen = Gen.choose(Int.MinValue, Int.MaxValue)
197+
val expected = gen(Gen.Parameters.default, seed).get
198+
199+
val prms = Test.Parameters.default
200+
.withInitialSeed(Some(seed))
201+
.withMinSuccessfulTests(10)
202+
.withMaxRNGSpins(5)
203+
204+
var xs: List[Int] = Nil
205+
val prop = Prop.forAll(gen) { x =>
206+
xs = x :: xs
207+
true
208+
}
209+
210+
val res = Test.check(prms, prop)
211+
val n = xs.size
212+
val unique = xs.toSet
213+
val p0 = Prop(unique(expected)) :| s"did not see $expected in $unique"
214+
val p1 = Prop(unique.size > 1) :| s"saw $n duplicate values: $unique"
215+
p0 && p1
216+
}
193217
}

core/shared/src/main/scala/org/scalacheck/Test.scala

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ object Test {
125125
def withLegacyShrinking(b: Boolean): Parameters =
126126
cpy(useLegacyShrinking0 = b)
127127

128+
/** Maximum number of spins of the RNG to perform between checks.
129+
* Greater values will reduce reuse of values (with dimimishing returns)
130+
* for a given number of arguments to Prop.forAll tests. Greater values
131+
* will also generally lead to slower tests, so be careful.
132+
*/
133+
val maxRNGSpins: Int = 1
134+
135+
/** Set maximum RNG spins between checks */
136+
def withMaxRNGSpins(n: Int): Parameters =
137+
cpy(maxRNGSpins0 = n)
138+
128139
override def toString: String = {
129140
val sb = new StringBuilder
130141
sb.append("Parameters(")
@@ -137,7 +148,8 @@ object Test {
137148
sb.append(s"customClassLoader=$customClassLoader, ")
138149
sb.append(s"propFilter=$propFilter, ")
139150
sb.append(s"initialSeed=$initialSeed, ")
140-
sb.append(s"useLegacyShrinking=$useLegacyShrinking)")
151+
sb.append(s"useLegacyShrinking=$useLegacyShrinking, ")
152+
sb.append(s"maxRNGSpins=$maxRNGSpins)")
141153
sb.toString
142154
}
143155

@@ -152,7 +164,8 @@ object Test {
152164
customClassLoader0: Option[ClassLoader] = outer.customClassLoader,
153165
propFilter0: Option[String] = outer.propFilter,
154166
initialSeed0: Option[rng.Seed] = outer.initialSeed,
155-
useLegacyShrinking0: Boolean = outer.useLegacyShrinking
167+
useLegacyShrinking0: Boolean = outer.useLegacyShrinking,
168+
maxRNGSpins0: Int = outer.maxRNGSpins
156169
): Parameters =
157170
new Parameters {
158171
val minSuccessfulTests: Int = minSuccessfulTests0
@@ -165,6 +178,7 @@ object Test {
165178
val propFilter: Option[String] = propFilter0
166179
val initialSeed: Option[rng.Seed] = initialSeed0
167180
override val useLegacyShrinking: Boolean = useLegacyShrinking0
181+
override val maxRNGSpins: Int = maxRNGSpins0
168182
}
169183

170184
// no longer used, but preserved for binary compatibility
@@ -342,10 +356,17 @@ object Test {
342356
val help = "Disable legacy shrinking using Shrink instances"
343357
}
344358

359+
object OptMaxRNGSpins extends IntOpt {
360+
val default = 1
361+
val names = Set("maxRNGSpins")
362+
val help = "Maximum number of RNG spins to perform between checks"
363+
}
364+
345365
val opts = Set[Opt[_]](
346366
OptMinSuccess, OptMaxDiscardRatio, OptMinSize,
347367
OptMaxSize, OptWorkers, OptVerbosity,
348-
OptPropFilter, OptInitialSeed, OptDisableLegacyShrinking
368+
OptPropFilter, OptInitialSeed, OptDisableLegacyShrinking,
369+
OptMaxRNGSpins
349370
)
350371

351372
def parseParams(args: Array[String]): (Parameters => Parameters, List[String]) = {
@@ -369,6 +390,7 @@ object Test {
369390
}
370391

371392
val useLegacyShrinking0: Boolean = !optMap(OptDisableLegacyShrinking)
393+
val maxRNGSpins: Int = optMap(OptMaxRNGSpins)
372394
val params = { (p: Parameters) =>
373395
p.withMinSuccessfulTests(minSuccess0)
374396
.withMinSize(minSize0)
@@ -379,6 +401,7 @@ object Test {
379401
.withPropFilter(propFilter0)
380402
.withInitialSeed(initialSeed0)
381403
.withLegacyShrinking(useLegacyShrinking0)
404+
.withMaxRNGSpins(maxRNGSpins)
382405
}
383406
(params, us)
384407
}
@@ -405,6 +428,7 @@ object Test {
405428

406429
val iterations = Math.ceil(params.minSuccessfulTests / params.workers.toDouble)
407430
val sizeStep = (params.maxSize - params.minSize) / (iterations * params.workers)
431+
val maxSpinsBetween = params.maxRNGSpins.max(1)
408432
var stop = false
409433

410434
def workerFun(workerIdx: Int): Result = {
@@ -420,6 +444,22 @@ object Test {
420444
if (workerIdx == 0) seed0 else seed0.reseed(workerIdx.toLong)
421445
}
422446

447+
val spinner: () => Unit =
448+
if (maxSpinsBetween > 1) {
449+
() => {
450+
var slides = 1 + ((n + d) % maxSpinsBetween)
451+
452+
while (slides > 0) {
453+
seed = seed.slide
454+
slides -= 1
455+
}
456+
}
457+
} else {
458+
() => {
459+
seed = seed.slide
460+
}
461+
}
462+
423463
while(!stop && res == null && n < iterations) {
424464

425465
val count = workerIdx + (params.workers * (n + d))
@@ -429,7 +469,7 @@ object Test {
429469
.withInitialSeed(Some(seed))
430470
.withSize(size.round.toInt)
431471

432-
seed = seed.slide
472+
spinner()
433473

434474
val propRes = p(genPrms)
435475
if (propRes.collected.nonEmpty) {

0 commit comments

Comments
 (0)