All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jqwik.engine.properties.state.ShrinkableChain Maven / Gradle / Ivy

There is a newer version: 1.9.1
Show 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 initialSupplier;
	private final Function> transformationGenerator;
	private final int maxTransformations;
	private final int genSize;
	private final List> iterations;
	private final Supplier> changeDetectorSupplier;

	public ShrinkableChain(
		long randomSeed,
		Supplier initialSupplier,
		Function> transformationGenerator,
		Supplier> changeDetectorSupplier,
		int maxTransformations,
		int genSize
	) {
		this(randomSeed, initialSupplier, transformationGenerator, changeDetectorSupplier, maxTransformations, genSize, new ArrayList<>());
	}

	private ShrinkableChain(
		long randomSeed, Supplier initialSupplier,
		Function> transformationGenerator,
		Supplier> 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 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;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy