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

net.jqwik.engine.properties.RandomizedShrinkablesGenerator Maven / Gradle / Ivy

There is a newer version: 1.9.1
Show newest version
package net.jqwik.engine.properties;

import java.util.*;
import java.util.logging.*;
import java.util.stream.*;

import net.jqwik.api.*;
import net.jqwik.engine.*;
import net.jqwik.engine.properties.arbitraries.*;
import net.jqwik.engine.support.*;
import net.jqwik.engine.support.types.*;

import static java.lang.Math.*;

public class RandomizedShrinkablesGenerator implements ForAllParametersGenerator {

	private static final Logger LOG = Logger.getLogger(RandomizedShrinkablesGenerator.class.getName());

	public static RandomizedShrinkablesGenerator forParameters(
		List parameters,
		ArbitraryResolver arbitraryResolver,
		Random random,
		int genSize,
		EdgeCasesMode edgeCasesMode
	) {

		List> listOfEdgeCases = listOfEdgeCases(parameters, arbitraryResolver, edgeCasesMode, genSize);
		int edgeCasesTotal = calculateEdgeCasesTotal(listOfEdgeCases);

		logEdgeCasesOutnumberTriesIfApplicable(genSize, edgeCasesTotal);

		return new RandomizedShrinkablesGenerator(
			randomShrinkablesGenerator(parameters, arbitraryResolver, genSize, edgeCasesMode.activated()),
			new EdgeCasesGenerator(listOfEdgeCases),
			edgeCasesMode,
			edgeCasesTotal,
			calculateBaseToEdgeCaseRatio(listOfEdgeCases, genSize),
			random.nextLong()
		);
	}

	private static void logEdgeCasesOutnumberTriesIfApplicable(int genSize, int edgeCasesTotal) {
		int logEdgeCasesExceedTriesLimit = max(genSize, 100);
		if (edgeCasesTotal >= logEdgeCasesExceedTriesLimit && genSize > 1) {
			String message = String.format(
				"Edge case generation exceeds number of tries. Stopped after %s generated cases.",
				edgeCasesTotal
			);
			LOG.log(Level.INFO, message);
		}
	}

	private static int calculateEdgeCasesTotal(final List> listOfEdgeCases) {
		if (listOfEdgeCases.isEmpty()) {
			return 0;
		}
		return listOfEdgeCases.stream().mapToInt(EdgeCases::size).reduce(1, (a, b) -> a * b);
	}

	private static PurelyRandomShrinkablesGenerator randomShrinkablesGenerator(
		List parameters,
		ArbitraryResolver arbitraryResolver,
		int genSize,
		boolean withEdgeCases
	) {
		List parameterGenerators = parameterGenerators(parameters, arbitraryResolver, genSize, withEdgeCases);
		return new PurelyRandomShrinkablesGenerator(parameterGenerators);
	}

	private static List parameterGenerators(
		List parameters,
		ArbitraryResolver arbitraryResolver,
		int genSize,
		boolean withEdgeCases
	) {
		return parameters.stream()
						 .map(parameter -> resolveParameter(arbitraryResolver, parameter, genSize, withEdgeCases))
						 .collect(Collectors.toList());
	}

	private static List> listOfEdgeCases(
		List parameters,
		ArbitraryResolver arbitraryResolver,
		EdgeCasesMode edgeCasesMode,
		int genSize
	) {
		List> listOfEdgeCases = new ArrayList<>();

		if (edgeCasesMode.activated() && !parameters.isEmpty()) {
			int maxEdgeCasesNextParameter = genSize;
			for (MethodParameter parameter : parameters) {
				EdgeCases edgeCases = resolveEdgeCases(arbitraryResolver, parameter, maxEdgeCasesNextParameter);
				// If a single parameter has no edge cases the combination of parameters have no edge cases
				if (edgeCases.isEmpty()) {
					return Collections.emptyList();
				}
				listOfEdgeCases.add(edgeCases);
				maxEdgeCasesNextParameter = calculateNextParamMaxEdgeCases(maxEdgeCasesNextParameter, edgeCases.size());
			}
		}
		return listOfEdgeCases;
	}

	private static  int calculateNextParamMaxEdgeCases(int maxEdgeCases, int baseCasesSize) {
		int maxDerivedEdgeCases = Math.max(1, maxEdgeCases / baseCasesSize);
		// When in doubt generate a few more edge cases
		if (maxEdgeCases % baseCasesSize > 0) {
			maxDerivedEdgeCases += 1;
		}
		return maxDerivedEdgeCases;
	}

	private static int calculateBaseToEdgeCaseRatio(List> edgeCases, int genSize) {
		int countEdgeCases = edgeCases.stream().mapToInt(EdgeCases::size).reduce(1, (a, b) -> max(a * b, 1));
		return EdgeCasesGenerator.calculateBaseToEdgeCaseRatio(genSize, countEdgeCases);
	}

	private static EdgeCases resolveEdgeCases(
		ArbitraryResolver arbitraryResolver,
		MethodParameter parameter,
		int maxEdgeCases
	) {
		List> edgeCases = resolveArbitraries(arbitraryResolver, parameter)
			.stream()
			.map(objectArbitrary -> objectArbitrary.edgeCases(maxEdgeCases))
			.collect(Collectors.toList());
		return EdgeCasesSupport.concat(edgeCases, maxEdgeCases);
	}

	private static RandomizedParameterGenerator resolveParameter(
		ArbitraryResolver arbitraryResolver,
		MethodParameter parameter,
		int genSize,
		boolean withEdgeCases
	) {
		Set> arbitraries = resolveArbitraries(arbitraryResolver, parameter);

		// This logging only makes sense if arbitraries get the capability to describe themselves
		// Supplier message = () -> String.format("Parameter %s generated by arbitraries %s", parameter.getRawParameter(), arbitraries);
		// LOG.log(Level.INFO, message);

		return new RandomizedParameterGenerator(parameter, arbitraries, genSize, withEdgeCases);
	}

	private static Set> resolveArbitraries(ArbitraryResolver arbitraryResolver, MethodParameter parameter) {
		Set> arbitraries =
			arbitraryResolver.forParameter(parameter).stream()
							 .map(Arbitrary::asGeneric)
							 .collect(Collectors.toSet());
		if (arbitraries.isEmpty()) {
			throw new CannotFindArbitraryException(TypeUsageImpl.forParameter(parameter), parameter.getAnnotation(ForAll.class));
		}
		return arbitraries;
	}

	private final PurelyRandomShrinkablesGenerator randomGenerator;
	private final EdgeCasesGenerator edgeCasesGenerator;
	private final EdgeCasesMode edgeCasesMode;
	private final int edgeCasesTotal;
	private final int baseToEdgeCaseRatio;
	private final long baseRandomSeed;
	private Random random;

	private boolean allEdgeCasesGenerated = false;
	private int edgeCasesTried = 0;

	private RandomizedShrinkablesGenerator(
		PurelyRandomShrinkablesGenerator randomGenerator,
		EdgeCasesGenerator edgeCasesGenerator,
		EdgeCasesMode edgeCasesMode,
		int edgeCasesTotal,
		int baseToEdgeCaseRatio,
		long baseRandomSeed
	) {
		this.randomGenerator = randomGenerator;
		this.edgeCasesGenerator = edgeCasesGenerator;
		this.edgeCasesMode = edgeCasesMode;
		this.edgeCasesTotal = edgeCasesTotal;
		this.baseToEdgeCaseRatio = baseToEdgeCaseRatio;
		this.baseRandomSeed = baseRandomSeed;
		this.random = SourceOfRandomness.newRandom(baseRandomSeed);
	}

	@Override
	public boolean hasNext() {
		// Randomized generation should always be able to generate a next set of values
		return true;
	}

	@Override
	public List> next() {
		if (!allEdgeCasesGenerated) {
			if (edgeCasesMode.generateFirst()) {
				if (edgeCasesGenerator.hasNext()) {
					edgeCasesTried++;
					return edgeCasesGenerator.next();
				} else {
					allEdgeCasesGenerated = true;
				}
			}
			if (edgeCasesMode.mixIn()) {
				if (shouldGenerateEdgeCase(random)) {
					if (edgeCasesGenerator.hasNext()) {
						edgeCasesTried++;
						return edgeCasesGenerator.next();
					} else {
						allEdgeCasesGenerated = true;
					}
				}
			}
		}
		return randomGenerator.generateNext(random);
	}

	@Override
	public int edgeCasesTotal() {
		return edgeCasesTotal;
	}

	@Override
	public int edgeCasesTried() {
		return edgeCasesTried;
	}

	@Override
	public void reset() {
		random = SourceOfRandomness.newRandom(baseRandomSeed);
	}

	private boolean shouldGenerateEdgeCase(Random localRandom) {
		return localRandom.nextInt(baseToEdgeCaseRatio + 1) == 0;
	}

}