LUCENE-8999: LuceneTestCase.expectThrows now propogates assert/assumption failures up to the test w/o wrapping in a new assertion failure unless the caller has explicitly expected them

This commit is contained in:
Chris Hostetter 2019-10-07 10:41:57 -07:00
parent ceb47042d1
commit 4d0afd4aff
3 changed files with 245 additions and 67 deletions

View File

@ -189,6 +189,9 @@ Other
* LUCENE-8998: Fix OverviewImplTest.testIsOptimized reproducible failure. (Tomoko Uchida)
* LUCENE-8999: LuceneTestCase.expectThrows now propogates assert/assumption failures up to the test
w/o wrapping in a new assertion failure unless the caller has explicitly expected them (hossman)
======================= Lucene 8.2.0 =======================
API Changes

View File

@ -115,6 +115,7 @@ import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.internal.AssumptionViolatedException;
import com.carrotsearch.randomizedtesting.JUnit4MethodProvider;
import com.carrotsearch.randomizedtesting.LifecycleScope;
@ -2720,17 +2721,16 @@ public abstract class LuceneTestCase extends Assert {
/** Checks a specific exception class is thrown by the given runnable, and returns it. */
public static <T extends Throwable> T expectThrows(Class<T> expectedType, String noExceptionMessage, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (Throwable e) {
if (expectedType.isInstance(e)) {
return expectedType.cast(e);
}
AssertionFailedError assertion = new AssertionFailedError("Unexpected exception type, expected " + expectedType.getSimpleName() + " but got " + e);
assertion.initCause(e);
throw assertion;
final Throwable thrown = _expectThrows(Collections.singletonList(expectedType), runnable);
if (expectedType.isInstance(thrown)) {
return expectedType.cast(thrown);
}
throw new AssertionFailedError(noExceptionMessage);
if (null == thrown) {
throw new AssertionFailedError(noExceptionMessage);
}
AssertionFailedError assertion = new AssertionFailedError("Unexpected exception type, expected " + expectedType.getSimpleName() + " but got " + thrown);
assertion.initCause(thrown);
throw assertion;
}
/** Checks a specific exception class is thrown by the given runnable, and returns it. */
@ -2739,16 +2739,13 @@ public abstract class LuceneTestCase extends Assert {
throw new AssertionError("At least one expected exception type is required?");
}
Throwable thrown = null;
try {
runnable.run();
} catch (Throwable e) {
final Throwable thrown = _expectThrows(expectedTypes, runnable);
if (null != thrown) {
for (Class<? extends T> expectedType : expectedTypes) {
if (expectedType.isInstance(e)) {
return expectedType.cast(e);
if (expectedType.isInstance(thrown)) {
return expectedType.cast(thrown);
}
}
thrown = e;
}
List<String> exceptionTypes = expectedTypes.stream().map(c -> c.getSimpleName()).collect(Collectors.toList());
@ -2771,29 +2768,28 @@ public abstract class LuceneTestCase extends Assert {
*/
public static <TO extends Throwable, TW extends Throwable> TW expectThrows
(Class<TO> expectedOuterType, Class<TW> expectedWrappedType, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (Throwable e) {
if (expectedOuterType.isInstance(e)) {
Throwable cause = e.getCause();
if (expectedWrappedType.isInstance(cause)) {
return expectedWrappedType.cast(cause);
} else {
AssertionFailedError assertion = new AssertionFailedError
("Unexpected wrapped exception type, expected " + expectedWrappedType.getSimpleName()
+ " but got: " + cause);
assertion.initCause(e);
throw assertion;
}
}
AssertionFailedError assertion = new AssertionFailedError
("Unexpected outer exception type, expected " + expectedOuterType.getSimpleName()
+ " but got: " + e);
assertion.initCause(e);
throw assertion;
final Throwable thrown = _expectThrows(Collections.singletonList(expectedOuterType), runnable);
if (null == thrown) {
throw new AssertionFailedError("Expected outer exception " + expectedOuterType.getSimpleName()
+ " but no exception was thrown.");
}
throw new AssertionFailedError("Expected outer exception " + expectedOuterType.getSimpleName()
+ " but no exception was thrown.");
if (expectedOuterType.isInstance(thrown)) {
Throwable cause = thrown.getCause();
if (expectedWrappedType.isInstance(cause)) {
return expectedWrappedType.cast(cause);
} else {
AssertionFailedError assertion = new AssertionFailedError
("Unexpected wrapped exception type, expected " + expectedWrappedType.getSimpleName()
+ " but got: " + cause);
assertion.initCause(thrown);
throw assertion;
}
}
AssertionFailedError assertion = new AssertionFailedError
("Unexpected outer exception type, expected " + expectedOuterType.getSimpleName()
+ " but got: " + thrown);
assertion.initCause(thrown);
throw assertion;
}
/**
@ -2805,41 +2801,65 @@ public abstract class LuceneTestCase extends Assert {
*/
public static <TO extends Throwable, TW extends Throwable> TO expectThrowsAnyOf
(LinkedHashMap<Class<? extends TO>,List<Class<? extends TW>>> expectedOuterToWrappedTypes, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (Throwable e) {
for (Map.Entry<Class<? extends TO>, List<Class<? extends TW>>> entry : expectedOuterToWrappedTypes.entrySet()) {
Class<? extends TO> expectedOuterType = entry.getKey();
List<Class<? extends TW>> expectedWrappedTypes = entry.getValue();
Throwable cause = e.getCause();
if (expectedOuterType.isInstance(e)) {
if (expectedWrappedTypes.isEmpty()) {
return null; // no wrapped exception
} else {
for (Class<? extends TW> expectedWrappedType : expectedWrappedTypes) {
if (expectedWrappedType.isInstance(cause)) {
return expectedOuterType.cast(e);
}
final List<Class<? extends TO>> outerClasses = expectedOuterToWrappedTypes.keySet().stream().collect(Collectors.toList());
final Throwable thrown = _expectThrows(outerClasses, runnable);
if (null == thrown) {
List<String> outerTypes = outerClasses.stream().map(Class::getSimpleName).collect(Collectors.toList());
throw new AssertionFailedError("Expected any of the following outer exception types: " + outerTypes
+ " but no exception was thrown.");
}
for (Map.Entry<Class<? extends TO>, List<Class<? extends TW>>> entry : expectedOuterToWrappedTypes.entrySet()) {
Class<? extends TO> expectedOuterType = entry.getKey();
List<Class<? extends TW>> expectedWrappedTypes = entry.getValue();
Throwable cause = thrown.getCause();
if (expectedOuterType.isInstance(thrown)) {
if (expectedWrappedTypes.isEmpty()) {
return null; // no wrapped exception
} else {
for (Class<? extends TW> expectedWrappedType : expectedWrappedTypes) {
if (expectedWrappedType.isInstance(cause)) {
return expectedOuterType.cast(thrown);
}
List<String> wrappedTypes = expectedWrappedTypes.stream().map(Class::getSimpleName).collect(Collectors.toList());
AssertionFailedError assertion = new AssertionFailedError
("Unexpected wrapped exception type, expected one of " + wrappedTypes + " but got: " + cause);
assertion.initCause(e);
throw assertion;
}
List<String> wrappedTypes = expectedWrappedTypes.stream().map(Class::getSimpleName).collect(Collectors.toList());
AssertionFailedError assertion = new AssertionFailedError
("Unexpected wrapped exception type, expected one of " + wrappedTypes + " but got: " + cause);
assertion.initCause(thrown);
throw assertion;
}
}
List<String> outerTypes = expectedOuterToWrappedTypes.keySet().stream().map(Class::getSimpleName).collect(Collectors.toList());
AssertionFailedError assertion = new AssertionFailedError
("Unexpected outer exception type, expected one of " + outerTypes + " but got: " + e);
assertion.initCause(e);
throw assertion;
}
List<String> outerTypes = expectedOuterToWrappedTypes.keySet().stream().map(Class::getSimpleName).collect(Collectors.toList());
throw new AssertionFailedError("Expected any of the following outer exception types: " + outerTypes
+ " but no exception was thrown.");
List<String> outerTypes = outerClasses.stream().map(Class::getSimpleName).collect(Collectors.toList());
AssertionFailedError assertion = new AssertionFailedError
("Unexpected outer exception type, expected one of " + outerTypes + " but got: " + thrown);
assertion.initCause(thrown);
throw assertion;
}
/**
* Helper method for {@link #expectThrows} and {@link #expectThrowsAnyOf} that takes care of propagating
* any {@link AssertionError} or {@link AssumptionViolatedException} instances thrown if and only if they
* are super classes of the <code>expectedTypes</code>. Otherwise simply returns any {@link Throwable}
* thrown, regardless of type, or null if the <code>runnable</code> completed w/o error.
*/
private static Throwable _expectThrows(List<? extends Class<?>> expectedTypes, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (AssertionError | AssumptionViolatedException ae) {
for (Class<?> expectedType : expectedTypes) {
if (expectedType.isInstance(ae)) { // user is expecting this type explicitly
return ae;
}
}
throw ae;
} catch (Throwable e) {
return e;
}
return null;
}
/** Returns true if the file exists (can be opened), false
* if it cannot be opened, and (unlike Java's
* File.exists) throws IOException if there's some

View File

@ -0,0 +1,155 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.util;
import java.util.concurrent.atomic.AtomicBoolean;
import java.io.IOException;
import org.junit.internal.AssumptionViolatedException;
public class TestExpectThrows extends LuceneTestCase {
private static class HuperDuperException extends IOException {
public HuperDuperException() {
/* No-Op */
}
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable throws (an
* instance of a subclass of) the expected Exception type: by returning that Exception.
*/
public void testPass() {
final AtomicBoolean ran = new AtomicBoolean(false);
final IOException returned = expectThrows(IOException.class, () -> {
ran.getAndSet(true);
throw new HuperDuperException();
});
assertTrue(ran.get());
assertNotNull(returned);
assertEquals(HuperDuperException.class, returned.getClass());
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable does not throw (an
* instance of a subclass of) the expected Exception type: by throwing an assertion to
* <code>FAIL</code> the test.
*/
public void testFail() {
final AtomicBoolean ran = new AtomicBoolean(false);
AssertionError caught = null;
try {
final IOException returned = expectThrows(IOException.class, () -> {
ran.getAndSet(true);
});
fail("must not complete"); // NOTE: we don't use expectThrows to test expectThrows
} catch (AssertionError ae) {
caught = ae;
}
assertTrue(ran.get());
assertNotNull(caught);
assertEquals("Expected exception IOException but no exception was thrown", caught.getMessage());
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable contains an
* assertion that does not pass: by allowing that assertion to propogate and
* <code>FAIL</code> the test.
*/
public void testNestedFail() {
final AtomicBoolean ran = new AtomicBoolean(false);
AssertionError caught = null;
try {
final IOException returned = expectThrows(IOException.class, () -> {
ran.getAndSet(true);
fail("this failure should propogate");
});
fail("must not complete"); // NOTE: we don't use expectThrows to test expectThrows
} catch (AssertionError ae) {
caught = ae;
}
assertTrue(ran.get());
assertNotNull(caught);
assertEquals("this failure should propogate", caught.getMessage());
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable contains an
* assumption that does not pass: by allowing that assumption to propogate and cause
* the test to <code>SKIP</code>.
*/
public void testNestedAssume() {
final AtomicBoolean ran = new AtomicBoolean(false);
AssumptionViolatedException caught = null;
try {
final IOException returned = expectThrows(IOException.class, () -> {
ran.getAndSet(true);
assumeTrue("this assumption should propogate", false);
});
fail("must not complete"); // NOTE: we don't use expectThrows to test expectThrows
} catch (AssumptionViolatedException ave) {
caught = ave;
}
assertTrue(ran.get());
assertNotNull(caught);
assertEquals("this assumption should propogate", caught.getMessage());
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable contains an
* assertion that does not pass but the caller has explicitly said they expect an Exception of that type:
* by returning that assertion failure Exception.
*/
public void testExpectingNestedFail() {
final AtomicBoolean ran = new AtomicBoolean(false);
AssertionError returned = null;
try {
returned = expectThrows(AssertionError.class, () -> {
ran.getAndSet(true);
fail("this failure should be returned, not propogated");
});
} catch (AssertionError caught) { // NOTE: we don't use expectThrows to test expectThrows
assertNull("An exception should not have been thrown", caught);
}
assertTrue(ran.get());
assertNotNull(returned);
assertEquals("this failure should be returned, not propogated", returned.getMessage());
}
/**
* Tests that {@link #expectThrows} behaves correctly when the Runnable contains an
* assumption that does not pass but the caller has explicitly said they expect an Exception of that type:
* by returning that assumption failure Exception.
*/
public void testExpectingNestedAssume() {
final AtomicBoolean ran = new AtomicBoolean(false);
AssumptionViolatedException returned = null;
try {
returned = expectThrows(AssumptionViolatedException.class, () -> {
ran.getAndSet(true);
assumeTrue("this assumption should be returned, not propogated", false);
});
} catch (AssumptionViolatedException caught) { // NOTE: we don't use expectThrows to test expectThrows
assertNull("An exception should not have been thrown", caught);
}
assertTrue(ran.get());
assertNotNull(returned);
assertEquals("this assumption should be returned, not propogated", returned.getMessage());
}
}