22
33import java .util .*;
44import java .util .function .*;
5+ import java .util .stream .*;
56
67import net .jqwik .api .*;
78import net .jqwik .api .state .*;
89
9- import org .jetbrains .annotations .*;
10-
1110class 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}
0 commit comments