Skip to content

Commit 1436ad9

Browse files
author
Vladimir Sitnikov
committed
Cache several shrink resultsfor every chain step, so it does not re-access state model state during shrink phase
This might take slightly more memory and produce suboptimal shrinks, however makes shrink possible for state-accessing transformations. Fixes #428
1 parent 5b9c3b2 commit 1436ad9

File tree

2 files changed

+95
-15
lines changed

2 files changed

+95
-15
lines changed

engine/src/main/java/net/jqwik/engine/properties/state/ShrinkableChainIteration.java

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,63 @@
22

33
import java.util.*;
44
import java.util.function.*;
5+
import java.util.stream.*;
56

67
import net.jqwik.api.*;
78
import net.jqwik.api.state.*;
89

9-
import org.jetbrains.annotations.*;
10-
1110
class ShrinkableChainIteration<T> {
11+
// Larger values might improve shrink quality, however, they increase the shrink space, so it might increase shrink duration
12+
private final static int NUM_SAMPLES_IN_EAGER_CHAIN_SHRINK = Integer.getInteger("jqwik.eagerChainShrinkSamples", 2);
13+
14+
static class ShrinkableWithEagerValue<T> implements Shrinkable<T> {
15+
protected final Shrinkable<T> base;
16+
private final ShrinkingDistance distance;
17+
final T value;
18+
19+
ShrinkableWithEagerValue(Shrinkable<T> base) {
20+
this.base = base;
21+
this.distance = base.distance();
22+
this.value = base.value();
23+
}
24+
25+
@Override
26+
public T value() {
27+
return value;
28+
}
29+
30+
@Override
31+
public Stream<Shrinkable<T>> shrink() {
32+
return base.shrink();
33+
}
34+
35+
@Override
36+
public ShrinkingDistance distance() {
37+
return distance;
38+
}
39+
}
40+
41+
static class EagerShrinkable<T> extends ShrinkableWithEagerValue<T> {
42+
private final List<Shrinkable<T>> shrinkResults;
43+
44+
EagerShrinkable(Shrinkable<T> base, int numSamples) {
45+
super(base);
46+
this.shrinkResults =
47+
base.shrink()
48+
.sorted(Comparator.comparing(Shrinkable::distance))
49+
.limit(numSamples)
50+
.map(ShrinkableWithEagerValue::new)
51+
.collect(Collectors.toList());
52+
}
53+
54+
@Override
55+
public Stream<Shrinkable<T>> shrink() {
56+
return shrinkResults.stream();
57+
}
58+
}
59+
1260
final Shrinkable<Transformer<T>> shrinkable;
1361
private final Predicate<T> precondition;
14-
private final @Nullable Transformer<T> transformer;
1562
final boolean accessState;
1663
final boolean changeState;
1764

@@ -33,10 +80,17 @@ private ShrinkableChainIteration(
3380
this.precondition = precondition;
3481
this.accessState = accessState;
3582
this.changeState = changeState;
36-
this.shrinkable = shrinkable;
37-
// Transformer method might access state, so we need to cache the value here
38-
// otherwise it might be evaluated with wrong state (e.g. after chain executes)
39-
this.transformer = accessState ? shrinkable.value() : null;
83+
// When the shrinkable does not access state, we could just use it as is for ".value()", and ".shrink()"
84+
// If we get LazyShrinkable here, it means we are in a shrinking phase, so we know ".shrink()" will be called only
85+
// in case the subsequent execution fails. So we can just keep LazyShrinkable as is
86+
// Otherwise, we need to eagerly evaluate the shrinkables to since the state might change by appyling subsequent transformers,
87+
// so we won't be able to access the state anymore.
88+
// See https://github.com/jlink/jqwik/issues/428
89+
if (!accessState || shrinkable instanceof ShrinkableChainIteration.ShrinkableWithEagerValue) {
90+
this.shrinkable = shrinkable;
91+
} else {
92+
this.shrinkable = new EagerShrinkable<>(shrinkable, NUM_SAMPLES_IN_EAGER_CHAIN_SHRINK);
93+
}
4094
}
4195

4296
@Override
@@ -71,6 +125,6 @@ String transformation() {
71125
}
72126

73127
Transformer<T> transformer() {
74-
return transformer == null ? shrinkable.value() : transformer;
128+
return shrinkable.value();
75129
}
76130
}

engine/src/test/java/net/jqwik/engine/properties/state/ActionChainArbitraryTests.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ ActionChainArbitrary<String> xOrFailing() {
121121

122122
static class SetMutatingChainState {
123123
final List<String> actualOps = new ArrayList<>();
124+
boolean hasPrints;
124125
final Set<Integer> set = new HashSet<>();
125126

126127
@Override
@@ -131,6 +132,14 @@ public String toString() {
131132

132133
@Property
133134
void chainActionsAreProperlyDescribedEvenAfterChainExecution(@ForAll("setMutatingChain") ActionChain<SetMutatingChainState> chain) {
135+
chain = chain.withInvariant(
136+
state -> {
137+
if (state.hasPrints) {
138+
assertThat(state.actualOps).hasSizeLessThan(5);
139+
}
140+
}
141+
);
142+
134143
SetMutatingChainState finalState = chain.run();
135144

136145
assertThat(chain.transformations())
@@ -168,7 +177,7 @@ public ActionChainArbitrary<SetMutatingChainState> setMutatingChain() {
168177
)
169178
)
170179
.addAction(
171-
2,
180+
4,
172181
(Action.Dependent<SetMutatingChainState>)
173182
state ->
174183
Arbitraries
@@ -177,16 +186,33 @@ public ActionChainArbitrary<SetMutatingChainState> setMutatingChain() {
177186
.map(i -> {
178187
if (state.set.contains(i)) {
179188
return Transformer.noop();
180-
} else {
181-
return Transformer.mutate("add " + i + " to " + state.set, newState -> {
182-
newState.actualOps.add("add " + i + " to " + newState.set);
183-
newState.set.add(i);
184-
});
185189
}
190+
return Transformer.mutate("add " + i + " to " + state.set, newState -> {
191+
newState.actualOps.add("add " + i + " to " + newState.set);
192+
newState.set.add(i);
193+
});
194+
}
195+
)
196+
)
197+
.addAction(
198+
2,
199+
(Action.Dependent<SetMutatingChainState>)
200+
state ->
201+
state.set.isEmpty() ? Arbitraries.just(Transformer.noop()) :
202+
Arbitraries
203+
.of(state.set)
204+
.map(i -> {
205+
if (!state.set.contains(i)) {
206+
throw new IllegalStateException("The set does not contain " + i + ", current state is " + state);
207+
}
208+
return Transformer.mutate("print " + i + " from " + state.set, newState -> {
209+
newState.actualOps.add("print " + i + " from " + newState.set);
210+
newState.hasPrints = true;
211+
});
186212
}
187213
)
188214
)
189-
.withMaxTransformations(5);
215+
.withMaxTransformations(7);
190216
}
191217

192218
@Property

0 commit comments

Comments
 (0)