net.jqwik.engine.properties.shrinking.ShrinkableContainer Maven / Gradle / Ivy
package net.jqwik.engine.properties.shrinking;
import java.util.*;
import java.util.stream.*;
import org.jspecify.annotations.*;
import net.jqwik.api.*;
import net.jqwik.api.support.*;
import net.jqwik.engine.properties.*;
import net.jqwik.engine.support.*;
import static net.jqwik.engine.properties.UniquenessChecker.*;
abstract class ShrinkableContainer implements Shrinkable {
protected final List extends Shrinkable> elements;
protected final int minSize;
protected final int maxSize;
protected final Collection extends FeatureExtractor> uniquenessExtractors;
protected final @Nullable Arbitrary elementArbitrary;
ShrinkableContainer(
List extends Shrinkable> elements,
int minSize, int maxSize,
Collection extends FeatureExtractor> uniquenessExtractors,
@Nullable Arbitrary elementArbitrary
) {
this.elements = elements;
this.minSize = minSize;
this.maxSize = maxSize;
this.uniquenessExtractors = uniquenessExtractors;
this.elementArbitrary = elementArbitrary;
}
abstract C createValue(List extends Shrinkable> shrinkables);
@Override
public C value() {
return createValue(elements);
}
@Override
public Stream> shrink() {
return JqwikStreamSupport.concat(
shrinkSizeOfList(),
shrinkElementsOneAfterTheOther(0),
shrinkPairsOfElements()
);
}
@Override
public Optional> grow(Shrinkable> before, Shrinkable> after) {
if (before instanceof ShrinkableContainer && after instanceof ShrinkableContainer) {
ShrinkableContainer, ?> beforeContainer = (ShrinkableContainer, ?>) before;
ShrinkableContainer, ?> afterContainer = (ShrinkableContainer, ?>) after;
// Moving shrinkable from one container to another is only allowed if both contain elements
// created by the same arbitrary
if (Objects.equals(beforeContainer.elementArbitrary, afterContainer.elementArbitrary)) {
List> removedShrinkables = new ArrayList<>((beforeContainer).elements);
removedShrinkables.removeAll((afterContainer).elements);
return growBy(removedShrinkables);
}
}
return Optional.empty();
}
@Override
public Stream> grow() {
return growOneElementAfterTheOther();
}
private Stream> growOneElementAfterTheOther() {
List>> growPerElementStreams = new ArrayList<>();
for (int i = 0; i < elements.size(); i++) {
int index = i;
Shrinkable element = elements.get(i);
Stream> shrinkElement = element.grow().flatMap(shrunkElement -> {
List> elementsCopy = new ArrayList<>(elements);
elementsCopy.set(index, shrunkElement);
return Stream.of(createShrinkable(elementsCopy));
});
growPerElementStreams.add(shrinkElement);
}
return JqwikStreamSupport.concat(growPerElementStreams);
}
private Optional> growBy(List> shrinkables) {
if (elements.size() + shrinkables.size() <= maxSize) {
List> grownElements = new ArrayList<>(elements);
for (Shrinkable> shrinkable : shrinkables) {
try {
@SuppressWarnings("unchecked")
Shrinkable castShrinkable = (Shrinkable) shrinkable;
grownElements.add(0, castShrinkable);
} catch (Throwable classCastException) {
return Optional.empty();
}
}
Shrinkable grownShrinkable = createShrinkable(grownElements);
if (hasReallyGrown(grownShrinkable)) {
return Optional.of(grownShrinkable);
}
}
return Optional.empty();
}
protected boolean hasReallyGrown(Shrinkable grownShrinkable) {
return true;
}
protected Stream> shrinkSizeAggressively() {
return new AggressiveSizeOfListShrinker>(minSize)
.shrink(elements)
.map(this::createShrinkable)
.sorted(Comparator.comparing(Shrinkable::distance));
}
protected Stream> shrinkSizeOfList() {
return new SizeOfListShrinker>(minSize)
.shrink(elements)
.map(this::createShrinkable)
.sorted(Comparator.comparing(Shrinkable::distance));
}
protected Stream> shrinkElementsOneAfterTheOther(int maxToShrink) {
List>> shrinkPerElementStreams = new ArrayList<>();
for (int i = 0; i < elements.size(); i++) {
if (maxToShrink > 0 && i >= maxToShrink) {
break;
}
int index = i;
Shrinkable element = elements.get(i);
Stream> shrinkElement = element.shrink().flatMap(shrunkElement -> {
List> elementsCopy = new ArrayList<>(elements);
elementsCopy.remove(index);
if (!checkShrinkableUniqueIn(uniquenessExtractors, shrunkElement, elementsCopy)) {
return Stream.empty();
}
elementsCopy.add(index, shrunkElement);
return Stream.of(createShrinkable(elementsCopy));
});
shrinkPerElementStreams.add(shrinkElement);
}
return JqwikStreamSupport.concat(shrinkPerElementStreams);
}
protected Stream> shrinkPairsOfElements() {
ShrinkingCommons.ContainerCreator createContainer = newElements -> {
if (checkUniquenessOfShrinkables(uniquenessExtractors, newElements)) {
return createShrinkable(newElements);
} else {
// null value will skip the entry in zipped stream
return null;
}
};
return ShrinkingCommons.shrinkPairsOfElements(elements, createContainer);
}
protected Stream> sortElements() {
return ShrinkingCommons.sortElements(elements, (ShrinkingCommons.ContainerCreator) this::createShrinkable);
}
@Override
public ShrinkingDistance distance() {
return ShrinkingDistance.forCollection(elements);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShrinkableContainer, ?> that = (ShrinkableContainer, ?>) o;
return value().equals(that.value());
}
@Override
public int hashCode() {
return HashCodeSupport.hash(elements);
}
@Override
public String toString() {
return String.format(
"%s<%s>(%s:%s)",
getClass().getSimpleName(),
value().getClass().getSimpleName(),
value(), distance()
);
}
abstract Shrinkable createShrinkable(List extends Shrinkable> shrunkElements);
}