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

net.jqwik.engine.properties.shrinking.PropertyShrinker Maven / Gradle / Ivy

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

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;

import org.junit.platform.engine.*;

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

public class PropertyShrinker {

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

	private final FalsifiedSample originalSample;
	private final ShrinkingMode shrinkingMode;
	private final int boundedShrinkingSeconds;
	private final Consumer falsifiedSampleReporter;
	private final Method targetMethod;

	private final AtomicInteger shrinkingStepsCounter = new AtomicInteger(0);
	private final List shrinkingSequence = new LinkedList<>();

	private Optional currentBest = Optional.empty();
	private volatile boolean shrinkingInterrupted = false;

	public PropertyShrinker(
		FalsifiedSample originalSample,
		ShrinkingMode shrinkingMode,
		int boundedShrinkingSeconds,
		Consumer falsifiedSampleReporter,
		Method targetMethod
	) {
		this.originalSample = originalSample;
		this.shrinkingMode = shrinkingMode;
		this.boundedShrinkingSeconds = boundedShrinkingSeconds;
		this.falsifiedSampleReporter = falsifiedSampleReporter;
		this.targetMethod = targetMethod;
	}

	public ShrunkFalsifiedSample shrink(Falsifier> forAllFalsifier) {
		if (shrinkingMode == ShrinkingMode.OFF) {
			return unshrunkOriginalSample();
		}

		Falsifier> allowOnlyEquivalentErrorsFalsifier = sample -> {
			TryExecutionResult result = forAllFalsifier.execute(sample);
			if (isFalsifiedButErrorIsNotEquivalent(result, originalSample.falsifyingError())) {
				return TryExecutionResult.invalid();
			}
			return result;
		};

		Consumer sampleShrunkConsumer = sample -> {
			shrinkingStepsCounter.incrementAndGet();
			falsifiedSampleReporter.accept(sample);
		};

		Consumer shrinkAttemptConsumer = currentBest -> {
			if (currentBest != null) {
				this.currentBest = Optional.of(currentBest);
			}
		};

		return shrink(allowOnlyEquivalentErrorsFalsifier, sampleShrunkConsumer, shrinkAttemptConsumer);
	}

	public List shrinkingSequence() {
		int indexOfLastFalsified = shrinkingSequence.lastIndexOf(TryExecutionResult.Status.FALSIFIED);
		return shrinkingSequence.subList(0, indexOfLastFalsified + 1);
	}

	private ShrunkFalsifiedSample shrink(
		Falsifier> falsifier,
		Consumer sampleShrunkConsumer,
		Consumer shrinkAttemptConsumer
	) {
		FalsifiedSample fullyShrunkSample;
		Supplier shrinkUntilDone = () -> shrinkAsLongAsSampleImproves(falsifier, sampleShrunkConsumer, shrinkAttemptConsumer);
		if (shrinkingMode == ShrinkingMode.FULL) {
			fullyShrunkSample = shrinkUntilDone.get();
		} else {
			fullyShrunkSample = withTimeout(shrinkUntilDone);
		}
		return new ShrunkFalsifiedSampleImpl(fullyShrunkSample, shrinkingStepsCounter.get());
	}

	private FalsifiedSample withTimeout(Supplier shrinkUntilDone) {
		try {
			TestDescriptor currentDescriptor = CurrentTestDescriptor.get();
			DomainContext currentContext = CurrentDomainContext.get();
			Supplier shrinkWithTestDescriptor =
				() -> CurrentDomainContext.runWithContext(
					currentContext,
					() -> CurrentTestDescriptor.runWithDescriptor(currentDescriptor, shrinkUntilDone)
				);
			CompletableFuture falsifiedSampleFuture = CompletableFuture.supplyAsync(shrinkWithTestDescriptor);
			return falsifiedSampleFuture.get(boundedShrinkingSeconds, TimeUnit.SECONDS);
		} catch (InterruptedException | ExecutionException e) {
			return JqwikExceptionSupport.throwAsUncheckedException(e);
		} catch (TimeoutException e) {
			shrinkingInterrupted = true;
			logShrinkingBoundReached();
			return currentBest.orElse(originalSample);
		}
	}

	private FalsifiedSample shrinkAsLongAsSampleImproves(
		final Falsifier> falsifier,
		final Consumer sampleShrunkConsumer,
		final Consumer shrinkAttemptConsumer
	) {
		Falsifier> recordingFalsifier = params -> {
			TryExecutionResult executionResult = falsifier.execute(params);
			if (!shrinkingInterrupted) {
				shrinkingSequence.add(executionResult.status());
			}
			return executionResult;
		};

		ShrinkingAlgorithm plainShrinker = new ShrinkingAlgorithm(
			originalSample,
			sampleShrunkConsumer,
			shrinkAttemptConsumer
		);

		return plainShrinker.shrink(recordingFalsifier);
	}

	private ShrunkFalsifiedSample unshrunkOriginalSample() {
		return new ShrunkFalsifiedSampleImpl(originalSample, 0);
	}

	private boolean isFalsifiedButErrorIsNotEquivalent(TryExecutionResult result, Optional originalError) {
		boolean areEquivalent = new ErrorEquivalenceChecker(targetMethod).areEquivalent(originalError, result.throwable());
		return result.isFalsified() && !areEquivalent;
	}

	private void logShrinkingBoundReached() {
		String value = String.format(
			"Shrinking timeout reached after %s seconds." +
				"%n  You can switch on full shrinking with '@Property(shrinking = ShrinkingMode.FULL)'",
			boundedShrinkingSeconds
		);
		LOG.warning(value);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy