From ef6122aa0c610b83b9dc08f1ff2d8fd98d2c8140 Mon Sep 17 00:00:00 2001 From: Johannes Link Date: Thu, 16 Mar 2023 15:05:18 +0100 Subject: [PATCH] Added Arbitrary.ignoreExceptions(maxThrows, exceptionTypes) --- TODO.md | 3 -- .../main/java/net/jqwik/api/Arbitrary.java | 44 ++++++++++++++++++- .../net/jqwik/api/ExhaustiveGenerator.java | 11 +++-- .../java/net/jqwik/api/RandomGenerator.java | 22 ++++++---- .../engine/facades/ArbitraryFacadeImpl.java | 8 ++-- .../ExhaustiveGeneratorFacadeImpl.java | 8 +++- .../facades/RandomGeneratorFacadeImpl.java | 14 +++--- .../IgnoreExceptionExhaustiveGenerator.java | 8 ++-- .../randomized/IgnoreExceptionGenerator.java | 8 ++-- .../api/ArbitraryIgnoreExceptionsTests.java | 22 +++++++++- 10 files changed, 110 insertions(+), 38 deletions(-) diff --git a/TODO.md b/TODO.md index e2fdbbd6a..f925207f0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,5 @@ # 1.7.3 - - Add Arbitrary.ignoreException(maxMisses, ...) - See https://github.com/jqwik-team/jqwik/issues/462. - - Allow annotation @BeforeTry on member variables of tests to reinitialize them before each try. - Alternative: New annotation @InitBeforeTry diff --git a/api/src/main/java/net/jqwik/api/Arbitrary.java b/api/src/main/java/net/jqwik/api/Arbitrary.java index a96b4bc16..707c5bc81 100644 --- a/api/src/main/java/net/jqwik/api/Arbitrary.java +++ b/api/src/main/java/net/jqwik/api/Arbitrary.java @@ -49,7 +49,7 @@ abstract class ArbitraryFacade { public abstract Arbitrary flatMap(Arbitrary self, Function> mapper); - public abstract Arbitrary ignoreExceptions(Arbitrary self, Class[] exceptionTypes); + public abstract Arbitrary ignoreExceptions(Arbitrary self, int maxThrows, Class[] exceptionTypes); public abstract Arbitrary dontShrink(Arbitrary self); @@ -534,6 +534,7 @@ default Arbitrary> tuple5() { * * @param exceptionType The exception type to ignore * @return a new arbitrary instance + * @throws TooManyFilterMissesException if more than 10000 exceptions are thrown in a row */ @SuppressWarnings("unchecked") @API(status = MAINTAINED, since = "1.3.1") @@ -541,6 +542,23 @@ default Arbitrary ignoreException(Class exceptionType) { return ignoreExceptions(exceptionType); } + /** + * Create a new arbitrary of type {@code T} that will use the underlying + * arbitrary to create the tuple values but will ignore any raised exception of + * type {@code exceptionType} during generation. + * + * @param maxThrows The maximum number of subsequent exception throws before generation + * is stopped. + * @param exceptionType The exception type to ignore + * @return a new arbitrary instance + * @throws TooManyFilterMissesException if more than {@code maxThrows} exceptions are thrown in a row + */ + @SuppressWarnings("unchecked") + @API(status = EXPERIMENTAL, since = "1.7.3") + default Arbitrary ignoreException(int maxThrows, Class exceptionType) { + return ignoreExceptions(maxThrows, exceptionType); + } + /** * Create a new arbitrary of type {@code T} that will use the underlying * arbitrary to create the tuple values but will ignore any raised exception in @@ -552,11 +570,33 @@ default Arbitrary ignoreException(Class exceptionType) { * * @param exceptionTypes The exception types to ignore * @return a new arbitrary instance + * @throws TooManyFilterMissesException if more than 10000 exceptions are thrown in a row */ @SuppressWarnings("unchecked") @API(status = MAINTAINED, since = "1.7.2") default Arbitrary ignoreExceptions(Class... exceptionTypes) { - return ArbitraryFacade.implementation.ignoreExceptions(Arbitrary.this, exceptionTypes); + return this.ignoreExceptions(10000, exceptionTypes); + } + + /** + * Create a new arbitrary of type {@code T} that will use the underlying + * arbitrary to create the tuple values but will ignore any raised exception in + * {@code exceptionTypes} during generation. + * + *

+ * If {@code exceptionTypes} is empty, the original arbitrary is returned. + *

+ * + * @param maxThrows The maximum number of subsequent exception throws before generation + * is stopped. + * @param exceptionTypes The exception types to ignore + * @return a new arbitrary instance + * @throws TooManyFilterMissesException if more than {@code maxThrows} exceptions are thrown in a row + */ + @SuppressWarnings("unchecked") + @API(status = EXPERIMENTAL, since = "1.7.3") + default Arbitrary ignoreExceptions(int maxThrows, Class... exceptionTypes) { + return ArbitraryFacade.implementation.ignoreExceptions(Arbitrary.this, maxThrows, exceptionTypes); } /** diff --git a/api/src/main/java/net/jqwik/api/ExhaustiveGenerator.java b/api/src/main/java/net/jqwik/api/ExhaustiveGenerator.java index d0ad7f190..b542cd5ec 100644 --- a/api/src/main/java/net/jqwik/api/ExhaustiveGenerator.java +++ b/api/src/main/java/net/jqwik/api/ExhaustiveGenerator.java @@ -28,7 +28,11 @@ abstract class ExhaustiveGeneratorFacade { public abstract ExhaustiveGenerator injectNull(ExhaustiveGenerator self); - public abstract ExhaustiveGenerator ignoreExceptions(ExhaustiveGenerator self, Class[] exceptionTypes); + public abstract ExhaustiveGenerator ignoreExceptions( + ExhaustiveGenerator self, + Class[] exceptionTypes, + int maxThrows + ); } /** @@ -48,9 +52,8 @@ default ExhaustiveGenerator injectNull() { return ExhaustiveGeneratorFacade.implementation.injectNull(this); } - default ExhaustiveGenerator ignoreExceptions(Class[] exceptionTypes) { - return ExhaustiveGeneratorFacade.implementation.ignoreExceptions(this, exceptionTypes); + default ExhaustiveGenerator ignoreExceptions(int maxThrows, Class[] exceptionTypes) { + return ExhaustiveGeneratorFacade.implementation.ignoreExceptions(this, exceptionTypes, maxThrows); } - } diff --git a/api/src/main/java/net/jqwik/api/RandomGenerator.java b/api/src/main/java/net/jqwik/api/RandomGenerator.java index 0537dc6fd..694d66742 100644 --- a/api/src/main/java/net/jqwik/api/RandomGenerator.java +++ b/api/src/main/java/net/jqwik/api/RandomGenerator.java @@ -22,11 +22,11 @@ abstract class RandomGeneratorFacade { public abstract Shrinkable flatMap(Shrinkable self, Function> mapper, long nextLong); public abstract Shrinkable flatMap( - Shrinkable wrappedShrinkable, - Function> mapper, - int genSize, - long nextLong, - boolean withEmbeddedEdgeCases + Shrinkable wrappedShrinkable, + Function> mapper, + int genSize, + long nextLong, + boolean withEmbeddedEdgeCases ); public abstract RandomGenerator filter(RandomGenerator self, Predicate filterPredicate, int maxMisses); @@ -37,7 +37,11 @@ public abstract Shrinkable flatMap( public abstract RandomGenerator injectDuplicates(RandomGenerator self, double duplicateProbability); - public abstract RandomGenerator ignoreExceptions(RandomGenerator self, Class[] exceptionTypes); + public abstract RandomGenerator ignoreExceptions( + RandomGenerator self, + Class[] exceptionTypes, + int maxThrows + ); } /** @@ -72,7 +76,7 @@ default RandomGenerator flatMap(Function> mapper, int gen return random -> { Shrinkable wrappedShrinkable = RandomGenerator.this.next(random); return RandomGeneratorFacade.implementation - .flatMap(wrappedShrinkable, mapper, genSize, random.nextLong(), withEmbeddedEdgeCases); + .flatMap(wrappedShrinkable, mapper, genSize, random.nextLong(), withEmbeddedEdgeCases); }; } @@ -102,8 +106,8 @@ default RandomGenerator injectDuplicates(double duplicateProbability) { } @API(status = INTERNAL) - default RandomGenerator ignoreExceptions(Class[] exceptionTypes) { - return RandomGeneratorFacade.implementation.ignoreExceptions(this, exceptionTypes); + default RandomGenerator ignoreExceptions(int maxThrows, Class[] exceptionTypes) { + return RandomGeneratorFacade.implementation.ignoreExceptions(this, exceptionTypes, maxThrows); } @API(status = INTERNAL) diff --git a/engine/src/main/java/net/jqwik/engine/facades/ArbitraryFacadeImpl.java b/engine/src/main/java/net/jqwik/engine/facades/ArbitraryFacadeImpl.java index 6b4b63f15..5bf580f7d 100644 --- a/engine/src/main/java/net/jqwik/engine/facades/ArbitraryFacadeImpl.java +++ b/engine/src/main/java/net/jqwik/engine/facades/ArbitraryFacadeImpl.java @@ -81,25 +81,25 @@ public Arbitrary injectNull(Arbitrary self, double nullProbability) { } @Override - public Arbitrary ignoreExceptions(Arbitrary self, Class[] exceptionTypes) { + public Arbitrary ignoreExceptions(Arbitrary self, int maxThrows, Class[] exceptionTypes) { if (exceptionTypes.length == 0) { return self; } return new ArbitraryDelegator(self) { @Override public RandomGenerator generator(int genSize) { - return super.generator(genSize).ignoreExceptions(exceptionTypes); + return super.generator(genSize).ignoreExceptions(maxThrows, exceptionTypes); } @Override public RandomGenerator generatorWithEmbeddedEdgeCases(int genSize) { - return super.generatorWithEmbeddedEdgeCases(genSize).ignoreExceptions(exceptionTypes); + return super.generatorWithEmbeddedEdgeCases(genSize).ignoreExceptions(maxThrows, exceptionTypes); } @Override public Optional> exhaustive(long maxNumberOfSamples) { return super.exhaustive(maxNumberOfSamples) - .map(generator -> generator.ignoreExceptions(exceptionTypes)); + .map(generator -> generator.ignoreExceptions(maxThrows, exceptionTypes)); } @Override diff --git a/engine/src/main/java/net/jqwik/engine/facades/ExhaustiveGeneratorFacadeImpl.java b/engine/src/main/java/net/jqwik/engine/facades/ExhaustiveGeneratorFacadeImpl.java index a759405f2..ef1ae38c4 100644 --- a/engine/src/main/java/net/jqwik/engine/facades/ExhaustiveGeneratorFacadeImpl.java +++ b/engine/src/main/java/net/jqwik/engine/facades/ExhaustiveGeneratorFacadeImpl.java @@ -25,7 +25,11 @@ public ExhaustiveGenerator injectNull(ExhaustiveGenerator self) { } @Override - public ExhaustiveGenerator ignoreExceptions(final ExhaustiveGenerator self, final Class[] exceptionTypes) { - return new IgnoreExceptionExhaustiveGenerator<>(self, exceptionTypes); + public ExhaustiveGenerator ignoreExceptions( + final ExhaustiveGenerator self, + final Class[] exceptionTypes, + int maxThrows + ) { + return new IgnoreExceptionExhaustiveGenerator<>(self, exceptionTypes, maxThrows); } } diff --git a/engine/src/main/java/net/jqwik/engine/facades/RandomGeneratorFacadeImpl.java b/engine/src/main/java/net/jqwik/engine/facades/RandomGeneratorFacadeImpl.java index 6268cd868..c91ff7589 100644 --- a/engine/src/main/java/net/jqwik/engine/facades/RandomGeneratorFacadeImpl.java +++ b/engine/src/main/java/net/jqwik/engine/facades/RandomGeneratorFacadeImpl.java @@ -18,11 +18,11 @@ public Shrinkable flatMap(Shrinkable self, Function Shrinkable flatMap( - Shrinkable self, - Function> mapper, - int genSize, - long nextLong, - boolean withEmbeddedEdgeCases + Shrinkable self, + Function> mapper, + int genSize, + long nextLong, + boolean withEmbeddedEdgeCases ) { return new FlatMappedShrinkable<>(self, mapper, genSize, nextLong, withEmbeddedEdgeCases); } @@ -48,7 +48,7 @@ public RandomGenerator injectDuplicates(RandomGenerator self, double d } @Override - public RandomGenerator ignoreExceptions(RandomGenerator self, Class[] exceptionTypes) { - return new IgnoreExceptionGenerator<>(self, exceptionTypes); + public RandomGenerator ignoreExceptions(RandomGenerator self, Class[] exceptionTypes, int maxThrows) { + return new IgnoreExceptionGenerator<>(self, exceptionTypes, maxThrows); } } diff --git a/engine/src/main/java/net/jqwik/engine/properties/arbitraries/exhaustive/IgnoreExceptionExhaustiveGenerator.java b/engine/src/main/java/net/jqwik/engine/properties/arbitraries/exhaustive/IgnoreExceptionExhaustiveGenerator.java index 71676a471..2f201ff51 100644 --- a/engine/src/main/java/net/jqwik/engine/properties/arbitraries/exhaustive/IgnoreExceptionExhaustiveGenerator.java +++ b/engine/src/main/java/net/jqwik/engine/properties/arbitraries/exhaustive/IgnoreExceptionExhaustiveGenerator.java @@ -9,10 +9,12 @@ public class IgnoreExceptionExhaustiveGenerator implements ExhaustiveGenerator { private final ExhaustiveGenerator toFilter; private final Class[] exceptionTypes; + private final int maxThrows; - public IgnoreExceptionExhaustiveGenerator(ExhaustiveGenerator toFilter, Class[] exceptionTypes) { + public IgnoreExceptionExhaustiveGenerator(ExhaustiveGenerator toFilter, Class[] exceptionTypes, int maxThrows) { this.toFilter = toFilter; this.exceptionTypes = exceptionTypes; + this.maxThrows = maxThrows; } @Override @@ -43,7 +45,7 @@ public T next() { } private T findNext() { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < maxThrows; i++) { if (!mappedIterator.hasNext()) { return null; } @@ -56,7 +58,7 @@ private T findNext() { throw throwable; } } - String message = String.format("Filter missed more than %s times.", 10000); + String message = String.format("Filter missed more than %s times.", maxThrows); throw new TooManyFilterMissesException(message); } diff --git a/engine/src/main/java/net/jqwik/engine/properties/arbitraries/randomized/IgnoreExceptionGenerator.java b/engine/src/main/java/net/jqwik/engine/properties/arbitraries/randomized/IgnoreExceptionGenerator.java index 277def788..ad4b917ec 100644 --- a/engine/src/main/java/net/jqwik/engine/properties/arbitraries/randomized/IgnoreExceptionGenerator.java +++ b/engine/src/main/java/net/jqwik/engine/properties/arbitraries/randomized/IgnoreExceptionGenerator.java @@ -12,10 +12,12 @@ public class IgnoreExceptionGenerator implements RandomGenerator { private final RandomGenerator base; private final Class[] exceptionTypes; + private final int maxThrows; - public IgnoreExceptionGenerator(RandomGenerator base, Class[] exceptionTypes) { + public IgnoreExceptionGenerator(RandomGenerator base, Class[] exceptionTypes, int maxThrows) { this.base = base; this.exceptionTypes = exceptionTypes; + this.maxThrows = maxThrows; } @Override @@ -24,7 +26,7 @@ public Shrinkable next(final Random random) { } private Shrinkable nextUntilAccepted(Random random, Function> fetchShrinkable) { - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < maxThrows; i++) { try { Shrinkable next = fetchShrinkable.apply(random); // Enforce value generation for possible exception raising @@ -37,7 +39,7 @@ private Shrinkable nextUntilAccepted(Random random, Function arbitrary = @@ -99,6 +99,26 @@ void failIfFilterWillDiscard10000ValuesInARow(@ForAll Random random) { assertThatThrownBy(() -> generator.next(random).value()).isInstanceOf(JqwikException.class); } + @Example + void maxThrowsIsConsideredForFailing(@ForAll Random random) { + AtomicInteger countMisses = new AtomicInteger(); + Arbitrary arbitrary = + Arbitraries.integers() + .map(anInt -> { + countMisses.incrementAndGet(); + throw new IllegalArgumentException("Always fail"); + }); + Arbitrary filtered = arbitrary.ignoreException( + 100, + IllegalArgumentException.class + ); + // Allowing edge cases will raise the number since edge case filtering cannot be cancelled + RandomGenerator generator = filtered.generator(10, false); + + assertThatThrownBy(() -> generator.next(random)).isInstanceOf(TooManyFilterMissesException.class); + assertThat(countMisses).hasValue(100); + } + @Example void exhaustiveGeneration() { Arbitrary arbitrary =