net.jqwik.engine.properties.state.ShrinkableChain Maven / Gradle / Ivy
The newest version!
package net.jqwik.engine.properties.state;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.stream.*;
import net.jqwik.engine.support.*;
import org.jspecify.annotations.*;
import org.opentest4j.*;
import net.jqwik.api.*;
import net.jqwik.api.Tuple.*;
import net.jqwik.api.state.*;
import net.jqwik.engine.*;
public class ShrinkableChain implements Shrinkable> {
public static final int MAX_TRANSFORMER_TRIES = 1000;
private final long randomSeed;
private final Supplier extends T> initialSupplier;
private final Function super Random, ? extends Transformation> transformationGenerator;
private final int maxTransformations;
private final int genSize;
private final List> iterations;
private final Supplier extends ChangeDetector super T>> changeDetectorSupplier;
public ShrinkableChain(
long randomSeed,
Supplier extends T> initialSupplier,
Function super Random, ? extends Transformation> transformationGenerator,
Supplier extends ChangeDetector super T>> changeDetectorSupplier,
int maxTransformations,
int genSize
) {
this(randomSeed, initialSupplier, transformationGenerator, changeDetectorSupplier, maxTransformations, genSize, new ArrayList<>());
}
private ShrinkableChain(
long randomSeed, Supplier extends T> initialSupplier,
Function super Random, ? extends Transformation> transformationGenerator,
Supplier extends ChangeDetector super T>> changeDetectorSupplier,
int maxTransformations,
int genSize,
List> iterations
) {
this.randomSeed = randomSeed;
this.initialSupplier = initialSupplier;
this.transformationGenerator = transformationGenerator;
this.changeDetectorSupplier = changeDetectorSupplier;
this.maxTransformations = maxTransformations;
this.genSize = genSize;
this.iterations = iterations;
}
@Override
public Chain value() {
return new ChainInstance();
}
@Override
public Stream>> shrink() {
return new ShrinkableChainShrinker<>(this, iterations, maxTransformations).shrink();
}
ShrinkableChain cloneWith(List> shrunkIterations, int newMaxSize) {
return new ShrinkableChain<>(
randomSeed,
initialSupplier,
transformationGenerator,
changeDetectorSupplier,
newMaxSize,
genSize,
shrunkIterations
);
}
@Override
public ShrinkingDistance distance() {
List>> shrinkablesForDistance = new ArrayList<>();
for (int i = 0; i < maxTransformations; i++) {
if (i < iterations.size()) {
shrinkablesForDistance.add(iterations.get(i).shrinkable);
} else {
shrinkablesForDistance.add(Shrinkable.unshrinkable(t -> t));
}
}
return ShrinkingDistance.forCollection(shrinkablesForDistance);
}
@Override
public String toString() {
return String.format("ShrinkableChain[maxSize=%s, iterations=%s]", maxTransformations, iterations);
}
private class ChainInstance implements Chain {
@Override
public Iterator start() {
return new ChainIterator(initialSupplier.get());
}
@Override
public int maxTransformations() {
return maxTransformations;
}
@Override
public List transformations() {
return iterations.stream().map(i -> i.transformation()).collect(Collectors.toList());
}
@Override
public List> transformers() {
return iterations.stream().map(i -> i.transformer()).collect(Collectors.toList());
}
@Override
public String toString() {
String actionsString = JqwikStringSupport.displayString(transformations());
return String.format("Chain: %s", actionsString);
}
}
private class ChainIterator implements Iterator {
private final Random random = SourceOfRandomness.newRandom(randomSeed);
private int steps = 0;
private @Nullable T current;
private boolean initialSupplied = false;
private @Nullable Transformer nextTransformer = null;
private ChainIterator(T initial) {
this.current = initial;
}
@Override
public boolean hasNext() {
if (!initialSupplied) {
return true;
}
synchronized (ShrinkableChain.this) {
if (isInfinite()) {
nextTransformer = nextTransformer();
return !nextTransformer.isEndOfChain();
} else {
if (steps < maxTransformations) {
nextTransformer = nextTransformer();
return !nextTransformer.isEndOfChain();
} else {
return false;
}
}
}
}
@Override
public T next() {
if (!initialSupplied) {
initialSupplied = true;
return current;
}
synchronized (ShrinkableChain.this) {
Transformer transformer = nextTransformer;
current = transformState(transformer, current);
return current;
}
}
private Transformer nextTransformer() {
// Fix random seed for same random sequence in re-runs
long nextSeed = random.nextLong();
if (steps < iterations.size()) {
return rerunStep();
} else {
return runNewStep(nextSeed);
}
}
private T transformState(Transformer transformer, T before) {
ChangeDetector super T> changeDetector = changeDetectorSupplier.get();
changeDetector.before(before);
try {
T after = transformer.apply(before);
boolean stateHasChanged = changeDetector.hasChanged(after);
ShrinkableChainIteration currentIteration = iterations.get(steps);
iterations.set(steps, currentIteration.withStateChange(stateHasChanged));
return after;
} finally {
steps++;
}
}
private Transformer rerunStep() {
ShrinkableChainIteration iteration = iterations.get(steps);
iteration.precondition().ifPresent(predicate -> {
if (!predicate.test(current)) {
throw new TestAbortedException("Precondition no longer valid");
}
});
// TODO: Could that be optimized to iteration.transformer()?
return iteration.shrinkable.value();
}
private Transformer runNewStep(long nextSeed) {
Random random = SourceOfRandomness.newRandom(nextSeed);
AtomicInteger attemptsCounter = new AtomicInteger(0);
while (attemptsCounter.get() < MAX_TRANSFORMER_TRIES) {
Tuple3>, Predicate, Boolean> arbitraryAccessTuple = nextTransformerArbitrary(random, attemptsCounter);
Arbitrary> arbitrary = arbitraryAccessTuple.get1();
Predicate precondition = arbitraryAccessTuple.get2();
boolean accessState = arbitraryAccessTuple.get3();
RandomGenerator> generator = arbitrary.generator(genSize);
Shrinkable> nextShrinkable = generator.next(random);
Transformer next = nextShrinkable.value();
if (next == Transformer.noop()) {
continue;
}
iterations.add(new ShrinkableChainIteration<>(precondition, accessState, nextShrinkable));
return next;
}
return failWithTooManyAttempts(attemptsCounter);
}
private Tuple3>, Predicate, Boolean> nextTransformerArbitrary(
Random random,
AtomicInteger attemptsCounter
) {
AtomicBoolean accessState = new AtomicBoolean(false);
Supplier supplier = () -> {
accessState.set(true);
return current;
};
while (attemptsCounter.getAndIncrement() < MAX_TRANSFORMER_TRIES) {
Transformation chainGenerator = transformationGenerator.apply(random);
Predicate precondition = chainGenerator.precondition();
boolean hasPrecondition = precondition != Transformation.NO_PRECONDITION;
if (hasPrecondition && !precondition.test(current)) {
continue;
}
accessState.set(false);
Arbitrary> arbitrary = chainGenerator.apply(supplier);
return Tuple.of(arbitrary, hasPrecondition ? precondition : null, accessState.get());
}
return failWithTooManyAttempts(attemptsCounter);
}
private R failWithTooManyAttempts(AtomicInteger attemptsCounter) {
String message = String.format("Could not generate a transformer after %s attempts.", attemptsCounter.get());
throw new JqwikException(message);
}
}
private boolean isInfinite() {
return maxTransformations < 0;
}
}