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

net.jqwik.engine.properties.arbitraries.LazyOfArbitrary Maven / Gradle / Ivy

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

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

import net.jqwik.api.*;
import net.jqwik.api.Tuple.*;
import net.jqwik.api.lifecycle.*;
import net.jqwik.engine.*;
import net.jqwik.engine.execution.lifecycle.*;
import net.jqwik.engine.properties.shrinking.*;
import net.jqwik.engine.support.*;

import org.jspecify.annotations.*;

public class LazyOfArbitrary implements Arbitrary {

	// Cached arbitraries only have to survive one property
	private static Store>> arbitrariesStore() {
		try {
			return Store.getOrCreate(Tuple.of(LazyOfShrinkable.class, "arbitraries"), Lifespan.PROPERTY, LinkedHashMap::new);
		} catch (OutsideJqwikException outsideJqwikException) {
			return Store.free(LinkedHashMap::new);
		}
	}

	@SuppressWarnings("unchecked")
	public static  Arbitrary of(int hashIdentifier, List>> suppliers) {
		// It's important for good shrinking to work that the same arbitrary usage is handled by the same arbitrary instance
		LazyOfArbitrary arbitrary = arbitrariesStore().get().computeIfAbsent(hashIdentifier, ignore -> new LazyOfArbitrary<>(suppliers));
		return (Arbitrary) arbitrary;
	}

	private final List>> suppliers;

	private final Deque>> generatedParts = new ArrayDeque<>();

	// Remember generators during the same try. That way generators with state (e.g. unique()) work as expected
	private final Store>> generators = createGeneratorsStore();

	private Store>> createGeneratorsStore() {
		try {
			return Store.getOrCreate(Tuple.of(this, "generators"), Lifespan.TRY, LinkedHashMap::new);
		} catch (OutsideJqwikException outsideJqwikException) {
			return Store.free(LinkedHashMap::new);
		}
	}

	public LazyOfArbitrary(List>> suppliers) {
		this.suppliers = suppliers;
	}

	@Override
	public RandomGenerator generator(int genSize) {
		return random -> {
			int index = random.nextInt(suppliers.size());
			long seed = random.nextLong();

			Tuple2, Set>> shrinkableAndParts = generateCurrent(genSize, index, seed);
			return createShrinkable(shrinkableAndParts, genSize, seed, Collections.singleton(index));
		};
	}

	private LazyOfShrinkable createShrinkable(
			Tuple2, Set>> shrinkableAndParts,
			int genSize,
			long seed,
			Set usedIndices
	) {
		Shrinkable shrinkable = shrinkableAndParts.get1();
		Set> parts = shrinkableAndParts.get2();
		LazyOfShrinkable lazyOfShrinkable = new LazyOfShrinkable<>(
				shrinkable,
				depth(parts),
				parts,
				(LazyOfShrinkable lazyOf) -> shrink(lazyOf, genSize, seed, usedIndices)
		);
		addGenerated(lazyOfShrinkable);
		return lazyOfShrinkable;
	}

	private void addGenerated(LazyOfShrinkable lazyOfShrinkable) {
		if (peekGenerated() != null) {
			peekGenerated().add(lazyOfShrinkable);
		}
	}

	private Set> peekGenerated() {
		return generatedParts.peekFirst();
	}

	private void pushGeneratedLevel() {
		generatedParts.addFirst(new LinkedHashSet<>());
	}

	private void popGeneratedLevel() {
		generatedParts.removeFirst();
	}

	private int depth(Set> parts) {
		return parts.stream().mapToInt(p -> p.depth).map(depth -> depth + 1).max().orElse(0);
	}

	private Tuple2, Set>> generateCurrent(int genSize, int index, long seed) {
		try {
			pushGeneratedLevel();
			return Tuple.of(
					getGenerator(index, genSize).next(SourceOfRandomness.newRandom(seed)),
					peekGenerated()
			);
		} finally {
			// To clean up even if there's an exception during value generation
			popGeneratedLevel();
		}
	}

	private RandomGenerator getGenerator(int index, int genSize) {
		if (generators.get().get(index) == null) {
			RandomGenerator generator = suppliers.get(index).get().generator(genSize);
			generators.get().put(index, generator);
		}
		return generators.get().get(index);
	}

	private Stream> shrink(
			LazyOfShrinkable lazyOf,
			int genSize,
			long seed,
			Set usedIndexes
	) {
		return JqwikStreamSupport.concat(
				shrinkToParts(lazyOf),
				shrinkCurrent(lazyOf, genSize, seed, usedIndexes),
				shrinkToAlternatives(lazyOf.current, genSize, seed, usedIndexes)
				// I don't have an example to show that this adds shrinking quality:
				//shrinkToAlternativesAndGrow(lazyOf.current, genSize, seed, usedIndexes)
		);
	}

	private Stream> shrinkCurrent(
			LazyOfShrinkable lazyOf,
			int genSize,
			long seed,
			Set usedIndexes
	) {
		return lazyOf.current.shrink().map(shrinkable -> new LazyOfShrinkable<>(
				shrinkable,
				lazyOf.depth,
				Collections.emptySet(),
				(LazyOfShrinkable lazy) -> shrink(lazy, genSize, seed, usedIndexes)
		));
	}

	private Stream> shrinkToParts(LazyOfShrinkable lazyOf) {
		return JqwikStreamSupport.concat(
				lazyOf.parts.stream().flatMap(this::shrinkToParts),
				lazyOf.parts.stream().map(s -> s)
		);
	}

	private Stream> shrinkToAlternatives(Shrinkable current, int genSize, long seed, Set usedIndexes) {
		ShrinkingDistance distance = current.distance();
		Set newUsedIndexes = new LinkedHashSet<>(usedIndexes);
		return IntStream
					   .range(0, suppliers.size())
					   .filter(index -> !usedIndexes.contains(index))
					   .peek(newUsedIndexes::add)
					   .mapToObj(index -> generateCurrent(genSize, index, seed))
					   .filter(shrinkableAndParts -> shrinkableAndParts.get1().distance().compareTo(distance) < 0)
					   .map(shrinkableAndParts -> createShrinkable(shrinkableAndParts, genSize, seed, newUsedIndexes));
	}

	// Currently disabled since I'm not sure if it provides additional value
	@SuppressWarnings("unused")
	private Stream> shrinkToAlternativesAndGrow(Shrinkable current, int genSize, long seed, Set usedIndexes) {
		ShrinkingDistance distance = current.distance();
		Set newUsedIndexes = new LinkedHashSet<>(usedIndexes);
		return IntStream
					   .range(0, suppliers.size())
					   .filter(index -> !usedIndexes.contains(index))
					   .peek(newUsedIndexes::add)
					   .mapToObj(index -> generateCurrent(genSize, index, seed))
					   .map(Tuple1::get1)
					   .filter(tShrinkable -> tShrinkable.distance().compareTo(distance) < 0)
					   .flatMap(Shrinkable::grow)
					   .filter(shrinkable -> shrinkable.distance().compareTo(distance) < 0)
					   .map(grownShrinkable -> createShrinkable(
							   Tuple.of(grownShrinkable, Collections.emptySet()),
							   genSize,
							   seed,
							   newUsedIndexes
					   ));
	}

	@Override
	public EdgeCases edgeCases(int maxEdgeCases) {
		return EdgeCases.none();
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy