Skip to content

Commit dde403d

Browse files
authored
Merge pull request #739 from jonaskoelker/ensure-preconditions-when-shrinking-commands-actions
Ensure preconditions are satisified when shrinking Commands actions
2 parents ab29704 + 300d9cd commit dde403d

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package org.scalacheck.commands
2+
3+
import org.scalacheck.Prop.forAll
4+
import org.scalacheck.rng.Seed
5+
import org.scalacheck.{Arbitrary, Gen, Prop, Properties, Shrink}
6+
import scala.reflect.ClassTag
7+
import scala.util.{Success, Try}
8+
9+
object CommandsShrinkSpecification extends Properties("Commands Shrinking") {
10+
11+
property("Shrunk command sequences always satisfy preconditions") = {
12+
forAll(Arbitrary.arbitrary[Long]) {
13+
case seedValue =>
14+
val seed = Seed(seedValue)
15+
val parameters = Gen.Parameters.default.withInitialSeed(seed)
16+
val result = BoundedQueueSpec.property(1)(parameters)
17+
18+
// If the property fails shrinking will kick in. If shrinking does
19+
// not guarantee that preconditions are satisfied and the shrunk
20+
// command sequence contains the Dequeue command, we will invoke
21+
// Nil.tail which will raise UnsupportedOperationException.
22+
result.status match {
23+
case Prop.Exception(e: UnsupportedOperationException) => false
24+
case _ => true
25+
}
26+
}
27+
}
28+
29+
class BoundedQueue[Element: ClassTag](val capacity: Int) {
30+
require(capacity >= 0)
31+
32+
var buffer = new Array[Element](capacity + 1)
33+
var reads = 0
34+
var writes = 0
35+
36+
def size: Int = {
37+
(writes + buffer.length - reads) % buffer.length
38+
}
39+
40+
def enqueue(element: Element): Unit = {
41+
require(size < capacity)
42+
buffer(writes) = element
43+
writes += 1
44+
// writes %= buffer.length // deliberately broken to provoke failure
45+
}
46+
47+
def dequeue(): Element = {
48+
require(size > 0)
49+
val index = reads
50+
reads += 1
51+
reads %= buffer.length
52+
buffer(index)
53+
}
54+
}
55+
56+
object BoundedQueueSpec extends Commands {
57+
type Element = Byte
58+
59+
type Sut = BoundedQueue[Element]
60+
61+
case class State(capacity: Int, elements: Seq[Element])
62+
63+
case object Capacity extends Command {
64+
type Result = Int
65+
def run(sut: Sut): Result = sut.capacity
66+
def nextState(state: State): State = state
67+
def preCondition(state: State): Boolean = true
68+
def postCondition(state: State, result: Try[Result]): Prop =
69+
result == Success(state.capacity)
70+
}
71+
72+
case object Size extends Command {
73+
type Result = Int
74+
def run(sut: Sut): Result = sut.size
75+
def nextState(state: State): State = state
76+
def preCondition(state: State): Boolean = true
77+
def postCondition(state: State, result: Try[Result]): Prop =
78+
result == Success(state.elements.length)
79+
}
80+
81+
case class Enqueue(element: Element) extends UnitCommand {
82+
def run(sut: Sut): Unit =
83+
sut.enqueue(element)
84+
def nextState(state: State): State =
85+
State(state.capacity, state.elements ++ List(element))
86+
def preCondition(state: State): Boolean =
87+
state.elements.length < state.capacity
88+
def postCondition(state: State, success: Boolean): Prop =
89+
success
90+
}
91+
92+
case object Dequeue extends Command {
93+
type Result = Element
94+
def run(sut: Sut): Result =
95+
sut.dequeue()
96+
def nextState(state: State): State =
97+
State(state.capacity, state.elements.tail)
98+
def preCondition(state: State): Boolean =
99+
state.elements.nonEmpty
100+
def postCondition(state: State, result: Try[Result]): Prop =
101+
result == Success(state.elements.head)
102+
}
103+
104+
def genInitialState: Gen[State] =
105+
Gen.choose(10, 20).map(State(_, Nil))
106+
107+
def genCommand(state: State): Gen[Command] =
108+
Gen.oneOf(
109+
Gen.const(Capacity),
110+
Gen.const(Size),
111+
Gen.const(Dequeue),
112+
Arbitrary.arbitrary[Element].map(Enqueue(_))
113+
).retryUntil(_.preCondition(state), 100)
114+
115+
def newSut(state: State): Sut =
116+
new BoundedQueue(state.capacity)
117+
118+
def initialPreCondition(state: State): Boolean =
119+
state.capacity > 0 && state.elements.isEmpty
120+
121+
override def shrinkState: Shrink[State] =
122+
Shrink.xmap[(Int, Seq[Element]), State](
123+
{ case (capacity, elems) => State(capacity, elems take capacity) },
124+
{ case State(capacity, elems) => (capacity, elems) }
125+
).suchThat(_.capacity >= 0)
126+
127+
def canCreateNewSut(
128+
newState: State,
129+
initSuts: Traversable[State],
130+
runningSuts: Traversable[Sut]
131+
): Boolean =
132+
true
133+
134+
def destroySut(sut: Sut): Unit =
135+
()
136+
}
137+
}

src/main/scala/org/scalacheck/commands/Commands.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,34 @@ trait Commands {
276276

277277
Shrink.shrinkWithOrig[State](as.s)(shrinkState) flatMap { state =>
278278
shrinkedCmds.map(_.copy(s = state))
279+
} map { as => ensurePreconditions(as) }
280+
}
281+
282+
private def ensurePreconditions(a: Actions): Actions = {
283+
def filterCommandSequence(s: State, commands: Commands): Commands =
284+
commands match {
285+
case cmd :: cmds =>
286+
if (cmd.preCondition(s))
287+
cmd :: filterCommandSequence(cmd.nextState(s), cmds)
288+
else
289+
filterCommandSequence(s, cmds)
290+
case Nil => Nil
291+
}
292+
293+
val filteredSequentialCommands = filterCommandSequence(a.s, a.seqCmds)
294+
val stateAfterSequentialCommands = filteredSequentialCommands
295+
.foldLeft(a.s) { case (st, cmd) => cmd.nextState(st) }
296+
297+
val filteredParallelCommands = a.parCmds.map(commands => {
298+
filterCommandSequence(stateAfterSequentialCommands, commands)
299+
}).filter(_.nonEmpty)
300+
301+
filteredParallelCommands match {
302+
case List(singleThreadedContinuation) =>
303+
val seqCmds = filteredSequentialCommands ++ singleThreadedContinuation
304+
Actions(a.s, seqCmds, Nil)
305+
case _ =>
306+
Actions(a.s, filteredSequentialCommands, filteredParallelCommands)
279307
}
280308
}
281309

0 commit comments

Comments
 (0)