Skip to content

Commit

Permalink
Fix ActionChain failure include never-executed actions since it uses …
Browse files Browse the repository at this point in the history
…wrong state object when creating transformations

This change caches the transformation if it accesses state.

fixes #426
  • Loading branch information
Vladimir Sitnikov committed Dec 12, 2022
1 parent 3de9ca9 commit 5b9c3b2
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import net.jqwik.api.*;
import net.jqwik.api.state.*;

import org.jetbrains.annotations.*;

class ShrinkableChainIteration<T> {
final Shrinkable<Transformer<T>> shrinkable;
private final Predicate<T> precondition;
private final @Nullable Transformer<T> transformer;
final boolean accessState;
final boolean changeState;

Expand All @@ -31,18 +34,21 @@ private ShrinkableChainIteration(
this.accessState = accessState;
this.changeState = changeState;
this.shrinkable = shrinkable;
// Transformer method might access state, so we need to cache the value here
// otherwise it might be evaluated with wrong state (e.g. after chain executes)
this.transformer = accessState ? shrinkable.value() : null;
}

@Override
public String toString() {
return String.format(
"Iteration[accessState=%s, changeState=%s, transformation=%s]",
accessState, changeState, shrinkable.value().transformation()
accessState, changeState, transformer().transformation()
);
}

boolean isEndOfChain() {
return shrinkable.value().equals(Transformer.END_OF_CHAIN);
return transformer().equals(Transformer.END_OF_CHAIN);
}

Optional<Predicate<T>> precondition() {
Expand All @@ -65,6 +71,6 @@ String transformation() {
}

Transformer<T> transformer() {
return shrinkable.value();
return transformer == null ? shrinkable.value() : transformer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,76 @@ ActionChainArbitrary<String> xOrFailing() {
.withMaxTransformations(30);
}

static class SetMutatingChainState {
final List<String> actualOps = new ArrayList<>();
final Set<Integer> set = new HashSet<>();

@Override
public String toString() {
return "set=" + set + ", actualOps=" + actualOps;
}
}

@Property
void chainActionsAreProperlyDescribedEvenAfterChainExecution(@ForAll("setMutatingChain") ActionChain<SetMutatingChainState> chain) {
SetMutatingChainState finalState = chain.run();

assertThat(chain.transformations())
.describedAs("chain.transformations() should be the same as the list of operations in finalState.actualOps, final state is %s", finalState.set)
.isEqualTo(finalState.actualOps);
}

@Provide
public ActionChainArbitrary<SetMutatingChainState> setMutatingChain() {
return
ActionChain
.startWith(SetMutatingChainState::new)
// This is an action that does not depend on the state to produce the transformation
.addAction(
1,
Action.just("clear anyway", state -> {
state.actualOps.add("clear anyway");
state.set.clear();
return state;
})
)
// Below actions depend on the state to derive the transformations
.addAction(
1,
(Action.Dependent<SetMutatingChainState>)
state ->
Arbitraries
.just(
state.set.isEmpty()
? Transformer.noop()
: Transformer.<SetMutatingChainState>mutate("clear " + state.set, set -> {
state.actualOps.add("clear " + set.set);
state.set.clear();
})
)
)
.addAction(
2,
(Action.Dependent<SetMutatingChainState>)
state ->
Arbitraries
.integers()
.between(1, 10)
.map(i -> {
if (state.set.contains(i)) {
return Transformer.noop();
} else {
return Transformer.mutate("add " + i + " to " + state.set, newState -> {
newState.actualOps.add("add " + i + " to " + newState.set);
newState.set.add(i);
});
}
}
)
)
.withMaxTransformations(5);
}

@Property
void chainChoosesBetweenTwoActions(@ForAll("xOrY") ActionChain<String> chain) {
String result = chain.run();
Expand Down

0 comments on commit 5b9c3b2

Please sign in to comment.