diff options
author | Pete Bentley <prb@google.com> | 2021-02-21 18:26:28 +0000 |
---|---|---|
committer | Pete Bentley <prb@google.com> | 2021-03-02 23:54:01 +0000 |
commit | 8e80a2a7b89329f95cb41e8b2981044362478c04 (patch) | |
tree | c863bdca20bca95fc2dcfa52c63c84b048d61751 /src/main/java/org/junit/internal | |
parent | 565f36d28118dce0c0a08fe71924dcd25e039022 (diff) | |
download | junit-8e80a2a7b89329f95cb41e8b2981044362478c04.tar.gz |
Upgrade external/junit to 4.13.2
Contains just the changes from 4.12 to 4.13.2 and undoes local
Android changes. Will re-patch those in in subsequent CLs.
This change re-lands https://r.android.com/1598413.
Bug: 129054170
Test: m
Change-Id: I6135799c8be5db2ec4c3f13951c18c072427e30d
Diffstat (limited to 'src/main/java/org/junit/internal')
36 files changed, 1165 insertions, 78 deletions
diff --git a/src/main/java/org/junit/internal/ArrayComparisonFailure.java b/src/main/java/org/junit/internal/ArrayComparisonFailure.java index 8627d6e..d300e7e 100644 --- a/src/main/java/org/junit/internal/ArrayComparisonFailure.java +++ b/src/main/java/org/junit/internal/ArrayComparisonFailure.java @@ -16,11 +16,12 @@ public class ArrayComparisonFailure extends AssertionError { /* * We have to use the f prefix until the next major release to ensure - * serialization compatibility. - * See https://github.com/junit-team/junit/issues/976 + * serialization compatibility. + * See https://github.com/junit-team/junit4/issues/976 */ private final List<Integer> fIndices = new ArrayList<Integer>(); private final String fMessage; + private final AssertionError fCause; /** * Construct a new <code>ArrayComparisonFailure</code> with an error text and the array's @@ -32,7 +33,8 @@ public class ArrayComparisonFailure extends AssertionError { */ public ArrayComparisonFailure(String message, AssertionError cause, int index) { this.fMessage = message; - initCause(cause); + this.fCause = cause; + initCause(fCause); addDimension(index); } @@ -41,6 +43,11 @@ public class ArrayComparisonFailure extends AssertionError { } @Override + public synchronized Throwable getCause() { + return super.getCause() == null ? fCause : super.getCause(); + } + + @Override public String getMessage() { StringBuilder sb = new StringBuilder(); if (fMessage != null) { diff --git a/src/main/java/org/junit/internal/AssumptionViolatedException.java b/src/main/java/org/junit/internal/AssumptionViolatedException.java index 880d73f..0e79b56 100644 --- a/src/main/java/org/junit/internal/AssumptionViolatedException.java +++ b/src/main/java/org/junit/internal/AssumptionViolatedException.java @@ -1,5 +1,8 @@ package org.junit.internal; +import java.io.IOException; +import java.io.ObjectOutputStream; + import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.SelfDescribing; @@ -18,7 +21,7 @@ public class AssumptionViolatedException extends RuntimeException implements Sel /* * We have to use the f prefix until the next major release to ensure * serialization compatibility. - * See https://github.com/junit-team/junit/issues/976 + * See https://github.com/junit-team/junit4/issues/976 */ private final String fAssumption; private final boolean fValueMatcher; @@ -108,4 +111,29 @@ public class AssumptionViolatedException extends RuntimeException implements Sel } } } + + /** + * Override default Java object serialization to correctly deal with potentially unserializable matchers or values. + * By not implementing readObject, we assure ourselves of backwards compatibility and compatibility with the + * standard way of Java serialization. + * + * @param objectOutputStream The outputStream to write our representation to + * @throws IOException When serialization fails + */ + private void writeObject(ObjectOutputStream objectOutputStream) throws IOException { + ObjectOutputStream.PutField putField = objectOutputStream.putFields(); + putField.put("fAssumption", fAssumption); + putField.put("fValueMatcher", fValueMatcher); + + // We have to wrap the matcher into a serializable form. + putField.put("fMatcher", SerializableMatcherDescription.asSerializableMatcher(fMatcher)); + + // We have to wrap the value inside a non-String class (instead of serializing the String value directly) as + // A Description will handle a String and non-String object differently (1st is surrounded by '"' while the + // latter will be surrounded by '<' '>'. Wrapping it makes sure that the description of a serialized and + // non-serialized instance produce the exact same description + putField.put("fValue", SerializableValueDescription.asSerializableValue(fValue)); + + objectOutputStream.writeFields(); + } } diff --git a/src/main/java/org/junit/internal/Checks.java b/src/main/java/org/junit/internal/Checks.java new file mode 100644 index 0000000..9724947 --- /dev/null +++ b/src/main/java/org/junit/internal/Checks.java @@ -0,0 +1,37 @@ +package org.junit.internal; + +/** @since 4.13 */ +public final class Checks { + + private Checks() {} + + /** + * Checks that the given value is not {@code null}. + * + * @param value object reference to check + * @return the passed-in value, if not {@code null} + * @throws NullPointerException if {@code value} is {@code null} + */ + public static <T> T notNull(T value) { + if (value == null) { + throw new NullPointerException(); + } + return value; + } + + /** + * Checks that the given value is not {@code null}, using the given message + * as the exception message if an exception is thrown. + * + * @param value object reference to check + * @param message message to use if {@code value} is {@code null} + * @return the passed-in value, if not {@code null} + * @throws NullPointerException if {@code value} is {@code null} + */ + public static <T> T notNull(T value, String message) { + if (value == null) { + throw new NullPointerException(message); + } + return value; + } +} diff --git a/src/main/java/org/junit/internal/Classes.java b/src/main/java/org/junit/internal/Classes.java index 154603d..e8404f6 100644 --- a/src/main/java/org/junit/internal/Classes.java +++ b/src/main/java/org/junit/internal/Classes.java @@ -6,13 +6,39 @@ import static java.lang.Thread.currentThread; * Miscellaneous functions dealing with classes.
*/
public class Classes {
+
+ /**
+ * Do not instantiate.
+ * @deprecated will be private soon.
+ */
+ @Deprecated
+ public Classes() {
+ }
+
/**
* Returns Class.forName for {@code className} using the current thread's class loader.
+ * If the current thread does not have a class loader, falls back to the class loader for
+ * {@link Classes}.
*
* @param className Name of the class.
* @throws ClassNotFoundException
*/
public static Class<?> getClass(String className) throws ClassNotFoundException {
- return Class.forName(className, true, currentThread().getContextClassLoader());
+ return getClass(className, Classes.class);
+ }
+
+ /**
+ * Returns Class.forName for {@code className} using the current thread's class loader.
+ * If the current thread does not have a class loader, falls back to the class loader for the
+ * passed-in class.
+ *
+ * @param className Name of the class.
+ * @param callingClass Class that is requesting a the class
+ * @throws ClassNotFoundException
+ * @since 4.13
+ */
+ public static Class<?> getClass(String className, Class<?> callingClass) throws ClassNotFoundException {
+ ClassLoader classLoader = currentThread().getContextClassLoader();
+ return Class.forName(className, true, classLoader == null ? callingClass.getClassLoader() : classLoader);
}
}
diff --git a/src/main/java/org/junit/internal/ComparisonCriteria.java b/src/main/java/org/junit/internal/ComparisonCriteria.java index e6d49a4..ed1c674 100644 --- a/src/main/java/org/junit/internal/ComparisonCriteria.java +++ b/src/main/java/org/junit/internal/ComparisonCriteria.java @@ -25,6 +25,11 @@ public abstract class ComparisonCriteria { */ public void arrayEquals(String message, Object expecteds, Object actuals) throws ArrayComparisonFailure { + arrayEquals(message, expecteds, actuals, true); + } + + private void arrayEquals(String message, Object expecteds, Object actuals, boolean outer) + throws ArrayComparisonFailure { if (expecteds == actuals || Arrays.deepEquals(new Object[] {expecteds}, new Object[] {actuals})) { // The reflection-based loop below is potentially very slow, especially for primitive @@ -34,19 +39,37 @@ public abstract class ComparisonCriteria { } String header = message == null ? "" : message + ": "; - int expectedsLength = assertArraysAreSameLength(expecteds, - actuals, header); + // Only include the user-provided message in the outer exception. + String exceptionMessage = outer ? header : ""; + + if (expecteds == null) { + Assert.fail(exceptionMessage + "expected array was null"); + } + if (actuals == null) { + Assert.fail(exceptionMessage + "actual array was null"); + } + + int actualsLength = Array.getLength(actuals); + int expectedsLength = Array.getLength(expecteds); + if (actualsLength != expectedsLength) { + header += "array lengths differed, expected.length=" + + expectedsLength + " actual.length=" + actualsLength + "; "; + } + int prefixLength = Math.min(actualsLength, expectedsLength); - for (int i = 0; i < expectedsLength; i++) { + for (int i = 0; i < prefixLength; i++) { Object expected = Array.get(expecteds, i); Object actual = Array.get(actuals, i); if (isArray(expected) && isArray(actual)) { try { - arrayEquals(message, expected, actual); + arrayEquals(message, expected, actual, false); } catch (ArrayComparisonFailure e) { e.addDimension(i); throw e; + } catch (AssertionError e) { + // Array lengths differed. + throw new ArrayComparisonFailure(header, e, i); } } else { try { @@ -56,27 +79,53 @@ public abstract class ComparisonCriteria { } } } - } - private boolean isArray(Object expected) { - return expected != null && expected.getClass().isArray(); + if (actualsLength != expectedsLength) { + Object expected = getToStringableArrayElement(expecteds, expectedsLength, prefixLength); + Object actual = getToStringableArrayElement(actuals, actualsLength, prefixLength); + try { + Assert.assertEquals(expected, actual); + } catch (AssertionError e) { + throw new ArrayComparisonFailure(header, e, prefixLength); + } + } } - private int assertArraysAreSameLength(Object expecteds, - Object actuals, String header) { - if (expecteds == null) { - Assert.fail(header + "expected array was null"); - } - if (actuals == null) { - Assert.fail(header + "actual array was null"); + private static final Object END_OF_ARRAY_SENTINEL = objectWithToString("end of array"); + + private Object getToStringableArrayElement(Object array, int length, int index) { + if (index < length) { + Object element = Array.get(array, index); + if (isArray(element)) { + return objectWithToString(componentTypeName(element.getClass()) + "[" + Array.getLength(element) + "]"); + } else { + return element; + } + } else { + return END_OF_ARRAY_SENTINEL; } - int actualsLength = Array.getLength(actuals); - int expectedsLength = Array.getLength(expecteds); - if (actualsLength != expectedsLength) { - Assert.fail(header + "array lengths differed, expected.length=" - + expectedsLength + " actual.length=" + actualsLength); + } + + private static Object objectWithToString(final String string) { + return new Object() { + @Override + public String toString() { + return string; + } + }; + } + + private String componentTypeName(Class<?> arrayClass) { + Class<?> componentType = arrayClass.getComponentType(); + if (componentType.isArray()) { + return componentTypeName(componentType) + "[]"; + } else { + return componentType.getName(); } - return expectedsLength; + } + + private boolean isArray(Object expected) { + return expected != null && expected.getClass().isArray(); } protected abstract void assertElementsEqual(Object expected, Object actual); diff --git a/src/main/java/org/junit/internal/SerializableMatcherDescription.java b/src/main/java/org/junit/internal/SerializableMatcherDescription.java new file mode 100644 index 0000000..e036557 --- /dev/null +++ b/src/main/java/org/junit/internal/SerializableMatcherDescription.java @@ -0,0 +1,47 @@ +package org.junit.internal; + +import java.io.Serializable; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; + +/** + * This class exists solely to provide a serializable description of a matcher to be serialized as a field in + * {@link AssumptionViolatedException}. Being a {@link Throwable}, it is required to be {@link Serializable}, but most + * implementations of {@link Matcher} are not. This class works around that limitation as + * {@link AssumptionViolatedException} only every uses the description of the {@link Matcher}, while still retaining + * backwards compatibility with classes compiled against its class signature before 4.14 and/or deserialization of + * previously serialized instances. + */ +class SerializableMatcherDescription<T> extends BaseMatcher<T> implements Serializable { + + private final String matcherDescription; + + private SerializableMatcherDescription(Matcher<T> matcher) { + matcherDescription = StringDescription.asString(matcher); + } + + public boolean matches(Object o) { + throw new UnsupportedOperationException("This Matcher implementation only captures the description"); + } + + public void describeTo(Description description) { + description.appendText(matcherDescription); + } + + /** + * Factory method that checks to see if the matcher is already serializable. + * @param matcher the matcher to make serializable + * @return The provided matcher if it is null or already serializable, + * the SerializableMatcherDescription representation of it if it is not. + */ + static <T> Matcher<T> asSerializableMatcher(Matcher<T> matcher) { + if (matcher == null || matcher instanceof Serializable) { + return matcher; + } else { + return new SerializableMatcherDescription<T>(matcher); + } + } +} diff --git a/src/main/java/org/junit/internal/SerializableValueDescription.java b/src/main/java/org/junit/internal/SerializableValueDescription.java new file mode 100644 index 0000000..4d055d7 --- /dev/null +++ b/src/main/java/org/junit/internal/SerializableValueDescription.java @@ -0,0 +1,38 @@ +package org.junit.internal; + +import java.io.Serializable; + +/** + * This class exists solely to provide a serializable description of a value to be serialized as a field in + * {@link AssumptionViolatedException}. Being a {@link Throwable}, it is required to be {@link Serializable}, but a + * value of type Object provides no guarantee to be serializable. This class works around that limitation as + * {@link AssumptionViolatedException} only every uses the string representation of the value, while still retaining + * backwards compatibility with classes compiled against its class signature before 4.14 and/or deserialization of + * previously serialized instances. + */ +class SerializableValueDescription implements Serializable { + private final String value; + + private SerializableValueDescription(Object value) { + this.value = String.valueOf(value); + } + + /** + * Factory method that checks to see if the value is already serializable. + * @param value the value to make serializable + * @return The provided value if it is null or already serializable, + * the SerializableValueDescription representation of it if it is not. + */ + static Object asSerializableValue(Object value) { + if (value == null || value instanceof Serializable) { + return value; + } else { + return new SerializableValueDescription(value); + } + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/org/junit/internal/TextListener.java b/src/main/java/org/junit/internal/TextListener.java index 9aa56c7..d548aeb 100644 --- a/src/main/java/org/junit/internal/TextListener.java +++ b/src/main/java/org/junit/internal/TextListener.java @@ -58,7 +58,7 @@ public class TextListener extends RunListener { protected void printFailures(Result result) { List<Failure> failures = result.getFailures(); - if (failures.size() == 0) { + if (failures.isEmpty()) { return; } if (failures.size() == 1) { @@ -74,7 +74,7 @@ public class TextListener extends RunListener { protected void printFailure(Failure each, String prefix) { getWriter().println(prefix + ") " + each.getTestHeader()); - getWriter().print(each.getTrace()); + getWriter().print(each.getTrimmedTrace()); } protected void printFooter(Result result) { diff --git a/src/main/java/org/junit/internal/Throwables.java b/src/main/java/org/junit/internal/Throwables.java index 86dceef..3f0f7a3 100644 --- a/src/main/java/org/junit/internal/Throwables.java +++ b/src/main/java/org/junit/internal/Throwables.java @@ -1,5 +1,17 @@ package org.junit.internal; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * Miscellaneous functions dealing with {@code Throwable}. * @@ -39,4 +51,223 @@ public final class Throwables { private static <T extends Throwable> void rethrow(Throwable e) throws T { throw (T) e; } + + /** + * Returns the stacktrace of the given Throwable as a String. + * + * @since 4.13 + */ + public static String getStacktrace(Throwable exception) { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + exception.printStackTrace(writer); + return stringWriter.toString(); + } + + /** + * Gets a trimmed version of the stack trace of the given exception. Stack trace + * elements that are below the test method are filtered out. + * + * @return a trimmed stack trace, or the original trace if trimming wasn't possible + */ + public static String getTrimmedStackTrace(Throwable exception) { + List<String> trimmedStackTraceLines = getTrimmedStackTraceLines(exception); + if (trimmedStackTraceLines.isEmpty()) { + return getFullStackTrace(exception); + } + + StringBuilder result = new StringBuilder(exception.toString()); + appendStackTraceLines(trimmedStackTraceLines, result); + appendStackTraceLines(getCauseStackTraceLines(exception), result); + return result.toString(); + } + + private static List<String> getTrimmedStackTraceLines(Throwable exception) { + List<StackTraceElement> stackTraceElements = Arrays.asList(exception.getStackTrace()); + int linesToInclude = stackTraceElements.size(); + + State state = State.PROCESSING_OTHER_CODE; + for (StackTraceElement stackTraceElement : asReversedList(stackTraceElements)) { + state = state.processStackTraceElement(stackTraceElement); + if (state == State.DONE) { + List<String> trimmedLines = new ArrayList<String>(linesToInclude + 2); + trimmedLines.add(""); + for (StackTraceElement each : stackTraceElements.subList(0, linesToInclude)) { + trimmedLines.add("\tat " + each); + } + if (exception.getCause() != null) { + trimmedLines.add("\t... " + (stackTraceElements.size() - trimmedLines.size()) + " trimmed"); + } + return trimmedLines; + } + linesToInclude--; + } + return Collections.emptyList(); + } + + private static final Method getSuppressed = initGetSuppressed(); + + private static Method initGetSuppressed() { + try { + return Throwable.class.getMethod("getSuppressed"); + } catch (Throwable e) { + return null; + } + } + + private static boolean hasSuppressed(Throwable exception) { + if (getSuppressed == null) { + return false; + } + try { + Throwable[] suppressed = (Throwable[]) getSuppressed.invoke(exception); + return suppressed.length != 0; + } catch (Throwable e) { + return false; + } + } + + private static List<String> getCauseStackTraceLines(Throwable exception) { + if (exception.getCause() != null || hasSuppressed(exception)) { + String fullTrace = getFullStackTrace(exception); + BufferedReader reader = new BufferedReader( + new StringReader(fullTrace.substring(exception.toString().length()))); + List<String> causedByLines = new ArrayList<String>(); + + try { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("Caused by: ") || line.trim().startsWith("Suppressed: ")) { + causedByLines.add(line); + while ((line = reader.readLine()) != null) { + causedByLines.add(line); + } + return causedByLines; + } + } + } catch (IOException e) { + // We should never get here, because we are reading from a StringReader + } + } + + return Collections.emptyList(); + } + + private static String getFullStackTrace(Throwable exception) { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + exception.printStackTrace(writer); + return stringWriter.toString(); + } + + private static void appendStackTraceLines( + List<String> stackTraceLines, StringBuilder destBuilder) { + for (String stackTraceLine : stackTraceLines) { + destBuilder.append(String.format("%s%n", stackTraceLine)); + } + } + + private static <T> List<T> asReversedList(final List<T> list) { + return new AbstractList<T>() { + + @Override + public T get(int index) { + return list.get(list.size() - index - 1); + } + + @Override + public int size() { + return list.size(); + } + }; + } + + private enum State { + PROCESSING_OTHER_CODE { + @Override public State processLine(String methodName) { + if (isTestFrameworkMethod(methodName)) { + return PROCESSING_TEST_FRAMEWORK_CODE; + } + return this; + } + }, + PROCESSING_TEST_FRAMEWORK_CODE { + @Override public State processLine(String methodName) { + if (isReflectionMethod(methodName)) { + return PROCESSING_REFLECTION_CODE; + } else if (isTestFrameworkMethod(methodName)) { + return this; + } + return PROCESSING_OTHER_CODE; + } + }, + PROCESSING_REFLECTION_CODE { + @Override public State processLine(String methodName) { + if (isReflectionMethod(methodName)) { + return this; + } else if (isTestFrameworkMethod(methodName)) { + // This is here to handle TestCase.runBare() calling TestCase.runTest(). + return PROCESSING_TEST_FRAMEWORK_CODE; + } + return DONE; + } + }, + DONE { + @Override public State processLine(String methodName) { + return this; + } + }; + + /** Processes a stack trace element method name, possibly moving to a new state. */ + protected abstract State processLine(String methodName); + + /** Processes a stack trace element, possibly moving to a new state. */ + public final State processStackTraceElement(StackTraceElement element) { + return processLine(element.getClassName() + "." + element.getMethodName() + "()"); + } + } + + private static final String[] TEST_FRAMEWORK_METHOD_NAME_PREFIXES = { + "org.junit.runner.", + "org.junit.runners.", + "org.junit.experimental.runners.", + "org.junit.internal.", + "junit.extensions", + "junit.framework", + "junit.runner", + "junit.textui", + }; + + private static final String[] TEST_FRAMEWORK_TEST_METHOD_NAME_PREFIXES = { + "org.junit.internal.StackTracesTest", + }; + + private static boolean isTestFrameworkMethod(String methodName) { + return isMatchingMethod(methodName, TEST_FRAMEWORK_METHOD_NAME_PREFIXES) && + !isMatchingMethod(methodName, TEST_FRAMEWORK_TEST_METHOD_NAME_PREFIXES); + } + + private static final String[] REFLECTION_METHOD_NAME_PREFIXES = { + "sun.reflect.", + "java.lang.reflect.", + "jdk.internal.reflect.", + "org.junit.rules.RunRules.<init>(", + "org.junit.rules.RunRules.applyAll(", // calls TestRules + "org.junit.runners.RuleContainer.apply(", // calls MethodRules & TestRules + "junit.framework.TestCase.runBare(", // runBare() directly calls setUp() and tearDown() + }; + + private static boolean isReflectionMethod(String methodName) { + return isMatchingMethod(methodName, REFLECTION_METHOD_NAME_PREFIXES); + } + + private static boolean isMatchingMethod(String methodName, String[] methodNamePrefixes) { + for (String methodNamePrefix : methodNamePrefixes) { + if (methodName.startsWith(methodNamePrefix)) { + return true; + } + } + + return false; + } } diff --git a/src/main/java/org/junit/internal/builders/AllDefaultPossibilitiesBuilder.java b/src/main/java/org/junit/internal/builders/AllDefaultPossibilitiesBuilder.java index d86ec95..8704a54 100644 --- a/src/main/java/org/junit/internal/builders/AllDefaultPossibilitiesBuilder.java +++ b/src/main/java/org/junit/internal/builders/AllDefaultPossibilitiesBuilder.java @@ -9,6 +9,17 @@ import org.junit.runners.model.RunnerBuilder; public class AllDefaultPossibilitiesBuilder extends RunnerBuilder { private final boolean canUseSuiteMethod; + /** + * @since 4.13 + */ + public AllDefaultPossibilitiesBuilder() { + canUseSuiteMethod = true; + } + + /** + * @deprecated used {@link #AllDefaultPossibilitiesBuilder()}. + */ + @Deprecated public AllDefaultPossibilitiesBuilder(boolean canUseSuiteMethod) { this.canUseSuiteMethod = canUseSuiteMethod; } diff --git a/src/main/java/org/junit/internal/builders/JUnit4Builder.java b/src/main/java/org/junit/internal/builders/JUnit4Builder.java index 6a00678..7959e75 100644 --- a/src/main/java/org/junit/internal/builders/JUnit4Builder.java +++ b/src/main/java/org/junit/internal/builders/JUnit4Builder.java @@ -1,12 +1,12 @@ package org.junit.internal.builders; import org.junit.runner.Runner; -import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.JUnit4; import org.junit.runners.model.RunnerBuilder; public class JUnit4Builder extends RunnerBuilder { @Override public Runner runnerForClass(Class<?> testClass) throws Throwable { - return new BlockJUnit4ClassRunner(testClass); + return new JUnit4(testClass); } -}
\ No newline at end of file +} diff --git a/src/main/java/org/junit/internal/management/FakeRuntimeMXBean.java b/src/main/java/org/junit/internal/management/FakeRuntimeMXBean.java new file mode 100644 index 0000000..477b150 --- /dev/null +++ b/src/main/java/org/junit/internal/management/FakeRuntimeMXBean.java @@ -0,0 +1,21 @@ +package org.junit.internal.management; + +import java.util.Collections; +import java.util.List; + +/** + * No-op implementation of RuntimeMXBean when the platform doesn't provide it. + */ +class FakeRuntimeMXBean implements RuntimeMXBean { + + /** + * {@inheritDoc} + * + * <p>Always returns an empty list. + */ + public List<String> getInputArguments() { + return Collections.emptyList(); + } + +} + diff --git a/src/main/java/org/junit/internal/management/FakeThreadMXBean.java b/src/main/java/org/junit/internal/management/FakeThreadMXBean.java new file mode 100644 index 0000000..893f2e3 --- /dev/null +++ b/src/main/java/org/junit/internal/management/FakeThreadMXBean.java @@ -0,0 +1,27 @@ +package org.junit.internal.management; + +/** + * No-op implementation of ThreadMXBean when the platform doesn't provide it. + */ +final class FakeThreadMXBean implements ThreadMXBean { + + /** + * {@inheritDoc} + * + * <p>Always throws an {@link UnsupportedOperationException} + */ + public long getThreadCpuTime(long id) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * <p>Always returns false. + */ + public boolean isThreadCpuTimeSupported() { + return false; + } + +} + diff --git a/src/main/java/org/junit/internal/management/ManagementFactory.java b/src/main/java/org/junit/internal/management/ManagementFactory.java new file mode 100644 index 0000000..5be1447 --- /dev/null +++ b/src/main/java/org/junit/internal/management/ManagementFactory.java @@ -0,0 +1,77 @@ +package org.junit.internal.management; + +import org.junit.internal.Classes; + +import java.lang.reflect.InvocationTargetException; + +/** + * Reflective wrapper around {@link java.lang.management.ManagementFactory} + */ +public class ManagementFactory { + private static final class FactoryHolder { + private static final Class<?> MANAGEMENT_FACTORY_CLASS; + + static { + Class<?> managementFactoryClass = null; + try { + managementFactoryClass = Classes.getClass("java.lang.management.ManagementFactory"); + } catch (ClassNotFoundException e) { + // do nothing, managementFactoryClass will be none on failure + } + MANAGEMENT_FACTORY_CLASS = managementFactoryClass; + } + + static Object getBeanObject(String methodName) { + if (MANAGEMENT_FACTORY_CLASS != null) { + try { + return MANAGEMENT_FACTORY_CLASS.getMethod(methodName).invoke(null); + } catch (IllegalAccessException e) { + // fallthrough + } catch (IllegalArgumentException e) { + // fallthrough + } catch (InvocationTargetException e) { + // fallthrough + } catch (NoSuchMethodException e) { + // fallthrough + } catch (SecurityException e) { + // fallthrough + } + } + return null; + } + } + + private static final class RuntimeHolder { + private static final RuntimeMXBean RUNTIME_MX_BEAN = + getBean(FactoryHolder.getBeanObject("getRuntimeMXBean")); + + private static final RuntimeMXBean getBean(Object runtimeMxBean) { + return runtimeMxBean != null + ? new ReflectiveRuntimeMXBean(runtimeMxBean) : new FakeRuntimeMXBean(); + } + } + + private static final class ThreadHolder { + private static final ThreadMXBean THREAD_MX_BEAN = + getBean(FactoryHolder.getBeanObject("getThreadMXBean")); + + private static final ThreadMXBean getBean(Object threadMxBean) { + return threadMxBean != null + ? new ReflectiveThreadMXBean(threadMxBean) : new FakeThreadMXBean(); + } + } + + /** + * @see java.lang.management.ManagementFactory#getRuntimeMXBean() + */ + public static RuntimeMXBean getRuntimeMXBean() { + return RuntimeHolder.RUNTIME_MX_BEAN; + } + + /** + * @see java.lang.management.ManagementFactory#getThreadMXBean() + */ + public static ThreadMXBean getThreadMXBean() { + return ThreadHolder.THREAD_MX_BEAN; + } +} diff --git a/src/main/java/org/junit/internal/management/ReflectiveRuntimeMXBean.java b/src/main/java/org/junit/internal/management/ReflectiveRuntimeMXBean.java new file mode 100644 index 0000000..289587a --- /dev/null +++ b/src/main/java/org/junit/internal/management/ReflectiveRuntimeMXBean.java @@ -0,0 +1,61 @@ +package org.junit.internal.management; + +import org.junit.internal.Classes; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** + * Implementation of {@link RuntimeMXBean} using the JVM reflectively. + */ +final class ReflectiveRuntimeMXBean implements RuntimeMXBean { + private final Object runtimeMxBean; + + private static final class Holder { + private static final Method getInputArgumentsMethod; + static { + Method inputArguments = null; + try { + Class<?> threadMXBeanClass = Classes.getClass("java.lang.management.RuntimeMXBean"); + inputArguments = threadMXBeanClass.getMethod("getInputArguments"); + } catch (ClassNotFoundException e) { + // do nothing, input arguments will be null on failure + } catch (NoSuchMethodException e) { + // do nothing, input arguments will be null on failure + } catch (SecurityException e) { + // do nothing, input arguments will be null on failure + } + getInputArgumentsMethod = inputArguments; + } + } + + ReflectiveRuntimeMXBean(Object runtimeMxBean) { + super(); + this.runtimeMxBean = runtimeMxBean; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + public List<String> getInputArguments() { + if (Holder.getInputArgumentsMethod != null) { + try { + return (List<String>) Holder.getInputArgumentsMethod.invoke(runtimeMxBean); + } catch (ClassCastException e) { // no multi-catch with source level 6 + // fallthrough + } catch (IllegalAccessException e) { + // fallthrough + } catch (IllegalArgumentException e) { + // fallthrough + } catch (InvocationTargetException e) { + // fallthrough + } + } + return Collections.emptyList(); + } + +} + diff --git a/src/main/java/org/junit/internal/management/ReflectiveThreadMXBean.java b/src/main/java/org/junit/internal/management/ReflectiveThreadMXBean.java new file mode 100644 index 0000000..bc741be --- /dev/null +++ b/src/main/java/org/junit/internal/management/ReflectiveThreadMXBean.java @@ -0,0 +1,92 @@ +package org.junit.internal.management; + +import org.junit.internal.Classes; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Implementation of {@link ThreadMXBean} using the JVM reflectively. + */ +final class ReflectiveThreadMXBean implements ThreadMXBean { + private final Object threadMxBean; + + + private static final class Holder { + static final Method getThreadCpuTimeMethod; + static final Method isThreadCpuTimeSupportedMethod; + + private static final String FAILURE_MESSAGE = "Unable to access ThreadMXBean"; + + static { + Method threadCpuTime = null; + Method threadCpuTimeSupported = null; + try { + Class<?> threadMXBeanClass = Classes.getClass("java.lang.management.ThreadMXBean"); + threadCpuTime = threadMXBeanClass.getMethod("getThreadCpuTime", long.class); + threadCpuTimeSupported = threadMXBeanClass.getMethod("isThreadCpuTimeSupported"); + } catch (ClassNotFoundException e) { + // do nothing, the methods will be null on failure + } catch (NoSuchMethodException e) { + // do nothing, the methods will be null on failure + } catch (SecurityException e) { + // do nothing, the methods will be null on failure + } + getThreadCpuTimeMethod = threadCpuTime; + isThreadCpuTimeSupportedMethod = threadCpuTimeSupported; + } + } + + ReflectiveThreadMXBean(Object threadMxBean) { + super(); + this.threadMxBean = threadMxBean; + } + + /** + * {@inheritDoc} + */ + public long getThreadCpuTime(long id) { + if (Holder.getThreadCpuTimeMethod != null) { + Exception error = null; + try { + return (Long) Holder.getThreadCpuTimeMethod.invoke(threadMxBean, id); + } catch (ClassCastException e) { + error = e; + // fallthrough + } catch (IllegalAccessException e) { + error = e; + // fallthrough + } catch (IllegalArgumentException e) { + error = e; + // fallthrough + } catch (InvocationTargetException e) { + error = e; + // fallthrough + } + throw new UnsupportedOperationException(Holder.FAILURE_MESSAGE, error); + } + throw new UnsupportedOperationException(Holder.FAILURE_MESSAGE); + } + + /** + * {@inheritDoc} + */ + public boolean isThreadCpuTimeSupported() { + if (Holder.isThreadCpuTimeSupportedMethod != null) { + try { + return (Boolean) Holder.isThreadCpuTimeSupportedMethod.invoke(threadMxBean); + } catch (ClassCastException e) { + // fallthrough + } catch (IllegalAccessException e) { + // fallthrough + } catch (IllegalArgumentException e) { + // fallthrough + } catch (InvocationTargetException e) { + // fallthrough + } + } + return false; + } + +} + diff --git a/src/main/java/org/junit/internal/management/RuntimeMXBean.java b/src/main/java/org/junit/internal/management/RuntimeMXBean.java new file mode 100644 index 0000000..84f8861 --- /dev/null +++ b/src/main/java/org/junit/internal/management/RuntimeMXBean.java @@ -0,0 +1,14 @@ +package org.junit.internal.management; + +import java.util.List; + +/** + * Wrapper for {@link java.lang.management.RuntimeMXBean}. + */ +public interface RuntimeMXBean { + + /** + * @see java.lang.management.RuntimeMXBean#getInputArguments() + */ + List<String> getInputArguments(); +} diff --git a/src/main/java/org/junit/internal/management/ThreadMXBean.java b/src/main/java/org/junit/internal/management/ThreadMXBean.java new file mode 100644 index 0000000..f9225c9 --- /dev/null +++ b/src/main/java/org/junit/internal/management/ThreadMXBean.java @@ -0,0 +1,17 @@ +package org.junit.internal.management; + +/** + * Wrapper for {@link java.lang.management.ThreadMXBean}. + */ +public interface ThreadMXBean { + /** + * @see java.lang.management.ThreadMXBean#getThreadCpuTime(long) + */ + long getThreadCpuTime(long id); + + /** + * @see java.lang.management.ThreadMXBean#isThreadCpuTimeSupported() + */ + boolean isThreadCpuTimeSupported(); +} + diff --git a/src/main/java/org/junit/internal/matchers/StacktracePrintingMatcher.java b/src/main/java/org/junit/internal/matchers/StacktracePrintingMatcher.java index 5d45ba3..93a6827 100644 --- a/src/main/java/org/junit/internal/matchers/StacktracePrintingMatcher.java +++ b/src/main/java/org/junit/internal/matchers/StacktracePrintingMatcher.java @@ -1,12 +1,11 @@ package org.junit.internal.matchers; -import java.io.PrintWriter; -import java.io.StringWriter; - import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.Matcher; +import org.junit.internal.Throwables; + /** * A matcher that delegates to throwableMatcher and in addition appends the * stacktrace of the actual Throwable in case of a mismatch. @@ -37,9 +36,7 @@ public class StacktracePrintingMatcher<T extends Throwable> extends } private String readStacktrace(Throwable throwable) { - StringWriter stringWriter = new StringWriter(); - throwable.printStackTrace(new PrintWriter(stringWriter)); - return stringWriter.toString(); + return Throwables.getStacktrace(throwable); } @Factory diff --git a/src/main/java/org/junit/internal/matchers/ThrowableCauseMatcher.java b/src/main/java/org/junit/internal/matchers/ThrowableCauseMatcher.java index 22ce8bd..6e2ff5e 100644 --- a/src/main/java/org/junit/internal/matchers/ThrowableCauseMatcher.java +++ b/src/main/java/org/junit/internal/matchers/ThrowableCauseMatcher.java @@ -14,9 +14,9 @@ import org.hamcrest.TypeSafeMatcher; public class ThrowableCauseMatcher<T extends Throwable> extends TypeSafeMatcher<T> { - private final Matcher<? extends Throwable> causeMatcher; + private final Matcher<?> causeMatcher; - public ThrowableCauseMatcher(Matcher<? extends Throwable> causeMatcher) { + public ThrowableCauseMatcher(Matcher<?> causeMatcher) { this.causeMatcher = causeMatcher; } @@ -44,7 +44,7 @@ public class ThrowableCauseMatcher<T extends Throwable> extends * @param <T> type of the outer exception */ @Factory - public static <T extends Throwable> Matcher<T> hasCause(final Matcher<? extends Throwable> matcher) { + public static <T extends Throwable> Matcher<T> hasCause(final Matcher<?> matcher) { return new ThrowableCauseMatcher<T>(matcher); } }
\ No newline at end of file diff --git a/src/main/java/org/junit/internal/matchers/TypeSafeMatcher.java b/src/main/java/org/junit/internal/matchers/TypeSafeMatcher.java index 4e2cc12..fb25982 100644 --- a/src/main/java/org/junit/internal/matchers/TypeSafeMatcher.java +++ b/src/main/java/org/junit/internal/matchers/TypeSafeMatcher.java @@ -40,7 +40,7 @@ public abstract class TypeSafeMatcher<T> extends BaseMatcher<T> { } private static boolean isMatchesSafelyMethod(Method method) { - return method.getName().equals("matchesSafely") + return "matchesSafely".equals(method.getName()) && method.getParameterTypes().length == 1 && !method.isSynthetic(); } diff --git a/src/main/java/org/junit/internal/requests/ClassRequest.java b/src/main/java/org/junit/internal/requests/ClassRequest.java index 3d6b100..d60e360 100644 --- a/src/main/java/org/junit/internal/requests/ClassRequest.java +++ b/src/main/java/org/junit/internal/requests/ClassRequest.java @@ -1,20 +1,18 @@ package org.junit.internal.requests; import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; -import org.junit.runner.Request; +import org.junit.internal.builders.SuiteMethodBuilder; import org.junit.runner.Runner; +import org.junit.runners.model.RunnerBuilder; -public class ClassRequest extends Request { - private final Object runnerLock = new Object(); - +public class ClassRequest extends MemoizingRequest { /* * We have to use the f prefix, because IntelliJ's JUnit4IdeaTestRunner uses * reflection to access this field. See - * https://github.com/junit-team/junit/issues/960 + * https://github.com/junit-team/junit4/issues/960 */ private final Class<?> fTestClass; private final boolean canUseSuiteMethod; - private volatile Runner runner; public ClassRequest(Class<?> testClass, boolean canUseSuiteMethod) { this.fTestClass = testClass; @@ -26,14 +24,31 @@ public class ClassRequest extends Request { } @Override - public Runner getRunner() { - if (runner == null) { - synchronized (runnerLock) { - if (runner == null) { - runner = new AllDefaultPossibilitiesBuilder(canUseSuiteMethod).safeRunnerForClass(fTestClass); - } + protected Runner createRunner() { + return new CustomAllDefaultPossibilitiesBuilder().safeRunnerForClass(fTestClass); + } + + private class CustomAllDefaultPossibilitiesBuilder extends AllDefaultPossibilitiesBuilder { + + @Override + protected RunnerBuilder suiteMethodBuilder() { + return new CustomSuiteMethodBuilder(); + } + } + + /* + * Customization of {@link SuiteMethodBuilder} that prevents use of the + * suite method when creating a runner for fTestClass when canUseSuiteMethod + * is false. + */ + private class CustomSuiteMethodBuilder extends SuiteMethodBuilder { + + @Override + public Runner runnerForClass(Class<?> testClass) throws Throwable { + if (testClass == fTestClass && !canUseSuiteMethod) { + return null; } + return super.runnerForClass(testClass); } - return runner; } }
\ No newline at end of file diff --git a/src/main/java/org/junit/internal/requests/FilterRequest.java b/src/main/java/org/junit/internal/requests/FilterRequest.java index 066cba3..5f00399 100644 --- a/src/main/java/org/junit/internal/requests/FilterRequest.java +++ b/src/main/java/org/junit/internal/requests/FilterRequest.java @@ -14,7 +14,7 @@ public final class FilterRequest extends Request { /* * We have to use the f prefix, because IntelliJ's JUnit4IdeaTestRunner uses * reflection to access this field. See - * https://github.com/junit-team/junit/issues/960 + * https://github.com/junit-team/junit4/issues/960 */ private final Filter fFilter; diff --git a/src/main/java/org/junit/internal/requests/MemoizingRequest.java b/src/main/java/org/junit/internal/requests/MemoizingRequest.java new file mode 100644 index 0000000..191c230 --- /dev/null +++ b/src/main/java/org/junit/internal/requests/MemoizingRequest.java @@ -0,0 +1,30 @@ +package org.junit.internal.requests; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.runner.Request; +import org.junit.runner.Runner; + +abstract class MemoizingRequest extends Request { + private final Lock runnerLock = new ReentrantLock(); + private volatile Runner runner; + + @Override + public final Runner getRunner() { + if (runner == null) { + runnerLock.lock(); + try { + if (runner == null) { + runner = createRunner(); + } + } finally { + runnerLock.unlock(); + } + } + return runner; + } + + /** Creates the {@link Runner} to return from {@link #getRunner()}. Called at most once. */ + protected abstract Runner createRunner(); +} diff --git a/src/main/java/org/junit/internal/requests/OrderingRequest.java b/src/main/java/org/junit/internal/requests/OrderingRequest.java new file mode 100644 index 0000000..441e595 --- /dev/null +++ b/src/main/java/org/junit/internal/requests/OrderingRequest.java @@ -0,0 +1,29 @@ +package org.junit.internal.requests; + +import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.runner.Request; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.Ordering; + +/** @since 4.13 */ +public class OrderingRequest extends MemoizingRequest { + private final Request request; + private final Ordering ordering; + + public OrderingRequest(Request request, Ordering ordering) { + this.request = request; + this.ordering = ordering; + } + + @Override + protected Runner createRunner() { + Runner runner = request.getRunner(); + try { + ordering.apply(runner); + } catch (InvalidOrderingException e) { + return new ErrorReportingRunner(ordering.getClass(), e); + } + return runner; + } +} diff --git a/src/main/java/org/junit/internal/runners/ErrorReportingRunner.java b/src/main/java/org/junit/internal/runners/ErrorReportingRunner.java index 1d32beb..f52abab 100644 --- a/src/main/java/org/junit/internal/runners/ErrorReportingRunner.java +++ b/src/main/java/org/junit/internal/runners/ErrorReportingRunner.java @@ -1,33 +1,44 @@ package org.junit.internal.runners; import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; import java.util.List; import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.InvalidTestClassError; import org.junit.runners.model.InitializationError; +import static java.util.Collections.singletonList; + public class ErrorReportingRunner extends Runner { private final List<Throwable> causes; - private final Class<?> testClass; + private final String classNames; public ErrorReportingRunner(Class<?> testClass, Throwable cause) { - if (testClass == null) { - throw new NullPointerException("Test class cannot be null"); + this(cause, testClass); + } + + public ErrorReportingRunner(Throwable cause, Class<?>... testClasses) { + if (testClasses == null || testClasses.length == 0) { + throw new NullPointerException("Test classes cannot be null or empty"); } - this.testClass = testClass; + for (Class<?> testClass : testClasses) { + if (testClass == null) { + throw new NullPointerException("Test class cannot be null"); + } + } + classNames = getClassNames(testClasses); causes = getCauses(cause); } - + @Override public Description getDescription() { - Description description = Description.createSuiteDescription(testClass); + Description description = Description.createSuiteDescription(classNames); for (Throwable each : causes) { - description.addChild(describeCause(each)); + description.addChild(describeCause()); } return description; } @@ -39,11 +50,25 @@ public class ErrorReportingRunner extends Runner { } } + private String getClassNames(Class<?>... testClasses) { + final StringBuilder builder = new StringBuilder(); + for (Class<?> testClass : testClasses) { + if (builder.length() != 0) { + builder.append(", "); + } + builder.append(testClass.getName()); + } + return builder.toString(); + } + @SuppressWarnings("deprecation") private List<Throwable> getCauses(Throwable cause) { if (cause instanceof InvocationTargetException) { return getCauses(cause.getCause()); } + if (cause instanceof InvalidTestClassError) { + return singletonList(cause); + } if (cause instanceof InitializationError) { return ((InitializationError) cause).getCauses(); } @@ -51,16 +76,15 @@ public class ErrorReportingRunner extends Runner { return ((org.junit.internal.runners.InitializationError) cause) .getCauses(); } - return Arrays.asList(cause); + return singletonList(cause); } - private Description describeCause(Throwable child) { - return Description.createTestDescription(testClass, - "initializationError"); + private Description describeCause() { + return Description.createTestDescription(classNames, "initializationError"); } private void runCause(Throwable child, RunNotifier notifier) { - Description description = describeCause(child); + Description description = describeCause(); notifier.fireTestStarted(description); notifier.fireTestFailure(new Failure(description, child)); notifier.fireTestFinished(description); diff --git a/src/main/java/org/junit/internal/runners/InitializationError.java b/src/main/java/org/junit/internal/runners/InitializationError.java index 52065ec..484f58d 100644 --- a/src/main/java/org/junit/internal/runners/InitializationError.java +++ b/src/main/java/org/junit/internal/runners/InitializationError.java @@ -15,7 +15,7 @@ public class InitializationError extends Exception { /* * We have to use the f prefix until the next major release to ensure * serialization compatibility. - * See https://github.com/junit-team/junit/issues/976 + * See https://github.com/junit-team/junit4/issues/976 */ private final List<Throwable> fErrors; diff --git a/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java b/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java index 631fcf2..0d51541 100644 --- a/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java +++ b/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java @@ -1,5 +1,8 @@ package org.junit.internal.runners; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + import junit.extensions.TestDecorator; import junit.framework.AssertionFailedError; import junit.framework.Test; @@ -12,15 +15,16 @@ import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runner.manipulation.Orderable; import org.junit.runner.manipulation.Sortable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -public class JUnit38ClassRunner extends Runner implements Filterable, Sortable { +public class JUnit38ClassRunner extends Runner implements Filterable, Orderable { private static final class OldTestClassAdaptingListener implements TestListener { private final RunNotifier notifier; @@ -170,6 +174,18 @@ public class JUnit38ClassRunner extends Runner implements Filterable, Sortable { } } + /** + * {@inheritDoc} + * + * @since 4.13 + */ + public void order(Orderer orderer) throws InvalidOrderingException { + if (getTest() instanceof Orderable) { + Orderable adapter = (Orderable) getTest(); + adapter.order(orderer); + } + } + private void setTest(Test test) { this.test = test; } diff --git a/src/main/java/org/junit/internal/runners/MethodValidator.java b/src/main/java/org/junit/internal/runners/MethodValidator.java index ba9c9d1..e656ee5 100644 --- a/src/main/java/org/junit/internal/runners/MethodValidator.java +++ b/src/main/java/org/junit/internal/runners/MethodValidator.java @@ -86,7 +86,7 @@ public class MethodValidator { } if (each.getReturnType() != Void.TYPE) { errors.add(new Exception("Method " + each.getName() - + " should be void")); + + "should have a return type of void")); } if (each.getParameterTypes().length != 0) { errors.add(new Exception("Method " + each.getName() diff --git a/src/main/java/org/junit/internal/runners/TestClass.java b/src/main/java/org/junit/internal/runners/TestClass.java index 1abaeea..6d24f4f 100644 --- a/src/main/java/org/junit/internal/runners/TestClass.java +++ b/src/main/java/org/junit/internal/runners/TestClass.java @@ -85,7 +85,7 @@ public class TestClass { } private List<Class<?>> getSuperClasses(Class<?> testClass) { - ArrayList<Class<?>> results = new ArrayList<Class<?>>(); + List<Class<?>> results = new ArrayList<Class<?>>(); Class<?> current = testClass; while (current != null) { results.add(current); diff --git a/src/main/java/org/junit/internal/runners/model/EachTestNotifier.java b/src/main/java/org/junit/internal/runners/model/EachTestNotifier.java index e094809..c5a0764 100644 --- a/src/main/java/org/junit/internal/runners/model/EachTestNotifier.java +++ b/src/main/java/org/junit/internal/runners/model/EachTestNotifier.java @@ -45,4 +45,27 @@ public class EachTestNotifier { public void fireTestIgnored() { notifier.fireTestIgnored(description); } + + /** + * Calls {@link RunNotifier#fireTestSuiteStarted(Description)}, passing the + * {@link Description} that was passed to the {@code EachTestNotifier} constructor. + * This should be called when a test suite is about to be started. + * @see RunNotifier#fireTestSuiteStarted(Description) + * @since 4.13 + */ + public void fireTestSuiteStarted() { + notifier.fireTestSuiteStarted(description); + } + + /** + * Calls {@link RunNotifier#fireTestSuiteFinished(Description)}, passing the + * {@link Description} that was passed to the {@code EachTestNotifier} constructor. + * This should be called when a test suite has finished, whether the test suite succeeds + * or fails. + * @see RunNotifier#fireTestSuiteFinished(Description) + * @since 4.13 + */ + public void fireTestSuiteFinished() { + notifier.fireTestSuiteFinished(description); + } }
\ No newline at end of file diff --git a/src/main/java/org/junit/internal/runners/rules/ValidationError.java b/src/main/java/org/junit/internal/runners/rules/ValidationError.java index d1af8ae..31bd660 100644 --- a/src/main/java/org/junit/internal/runners/rules/ValidationError.java +++ b/src/main/java/org/junit/internal/runners/rules/ValidationError.java @@ -5,6 +5,9 @@ import org.junit.runners.model.FrameworkMember; import java.lang.annotation.Annotation; class ValidationError extends Exception { + + private static final long serialVersionUID = 3176511008672645574L; + public ValidationError(FrameworkMember<?> member, Class<? extends Annotation> annotation, String suffix) { super(String.format("The @%s '%s' %s", annotation.getSimpleName(), member.getName(), suffix)); } diff --git a/src/main/java/org/junit/internal/runners/statements/ExpectException.java b/src/main/java/org/junit/internal/runners/statements/ExpectException.java index d0636bd..9a2a952 100644 --- a/src/main/java/org/junit/internal/runners/statements/ExpectException.java +++ b/src/main/java/org/junit/internal/runners/statements/ExpectException.java @@ -19,7 +19,9 @@ public class ExpectException extends Statement { next.evaluate(); complete = true; } catch (AssumptionViolatedException e) { - throw e; + if (!expected.isAssignableFrom(e.getClass())) { + throw e; + } } catch (Throwable e) { if (!expected.isAssignableFrom(e.getClass())) { String message = "Unexpected exception, expected<" diff --git a/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java b/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java index 9fad35b..9362cc1 100644 --- a/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java +++ b/src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java @@ -1,5 +1,8 @@ package org.junit.internal.runners.statements; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -7,6 +10,9 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.junit.internal.management.ManagementFactory; +import org.junit.internal.management.ThreadMXBean; +import org.junit.runners.model.MultipleFailureException; import org.junit.runners.model.Statement; import org.junit.runners.model.TestTimedOutException; @@ -14,6 +20,7 @@ public class FailOnTimeout extends Statement { private final Statement originalStatement; private final TimeUnit timeUnit; private final long timeout; + private final boolean lookForStuckThread; /** * Returns a new builder for building an instance. @@ -40,6 +47,7 @@ public class FailOnTimeout extends Statement { originalStatement = statement; timeout = builder.timeout; timeUnit = builder.unit; + lookForStuckThread = builder.lookForStuckThread; } /** @@ -48,6 +56,7 @@ public class FailOnTimeout extends Statement { * @since 4.12 */ public static class Builder { + private boolean lookForStuckThread = false; private long timeout = 0; private TimeUnit unit = TimeUnit.SECONDS; @@ -80,6 +89,20 @@ public class FailOnTimeout extends Statement { } /** + * Specifies whether to look for a stuck thread. If a timeout occurs and this + * feature is enabled, the test will look for a thread that appears to be stuck + * and dump its backtrace. This feature is experimental. Behavior may change + * after the 4.12 release in response to feedback. + * + * @param enable {@code true} to enable the feature + * @return {@code this} for method chaining. + */ + public Builder withLookingForStuckThread(boolean enable) { + this.lookForStuckThread = enable; + return this; + } + + /** * Builds a {@link FailOnTimeout} instance using the values in this builder, * wrapping the given statement. * @@ -97,7 +120,8 @@ public class FailOnTimeout extends Statement { public void evaluate() throws Throwable { CallableStatement callable = new CallableStatement(); FutureTask<Throwable> task = new FutureTask<Throwable>(callable); - Thread thread = new Thread(task, "Time-limited test"); + ThreadGroup threadGroup = threadGroupForNewThread(); + Thread thread = new Thread(threadGroup, task, "Time-limited test"); thread.setDaemon(true); thread.start(); callable.awaitStarted(); @@ -107,6 +131,31 @@ public class FailOnTimeout extends Statement { } } + private ThreadGroup threadGroupForNewThread() { + if (!lookForStuckThread) { + // Use the default ThreadGroup (usually the one from the current + // thread). + return null; + } + + // Create the thread in a new ThreadGroup, so if the time-limited thread + // becomes stuck, getStuckThread() can find the thread likely to be the + // culprit. + ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup"); + if (!threadGroup.isDaemon()) { + // Mark the new ThreadGroup as a daemon thread group, so it will be + // destroyed after the time-limited thread completes. By ensuring the + // ThreadGroup is destroyed, any data associated with the ThreadGroup + // (ex: via java.beans.ThreadGroupContext) is destroyed. + try { + threadGroup.setDaemon(true); + } catch (SecurityException e) { + // Swallow the exception to keep the same behavior as in JUnit 4.12. + } + } + return threadGroup; + } + /** * Wait for the test task, returning the exception thrown by the test if the * test failed, an exception indicating a timeout if the test timed out, or @@ -131,12 +180,114 @@ public class FailOnTimeout extends Statement { private Exception createTimeoutException(Thread thread) { StackTraceElement[] stackTrace = thread.getStackTrace(); + final Thread stuckThread = lookForStuckThread ? getStuckThread(thread) : null; Exception currThreadException = new TestTimedOutException(timeout, timeUnit); if (stackTrace != null) { currThreadException.setStackTrace(stackTrace); thread.interrupt(); } - return currThreadException; + if (stuckThread != null) { + Exception stuckThreadException = + new Exception("Appears to be stuck in thread " + + stuckThread.getName()); + stuckThreadException.setStackTrace(getStackTrace(stuckThread)); + return new MultipleFailureException( + Arrays.<Throwable>asList(currThreadException, stuckThreadException)); + } else { + return currThreadException; + } + } + + /** + * Retrieves the stack trace for a given thread. + * @param thread The thread whose stack is to be retrieved. + * @return The stack trace; returns a zero-length array if the thread has + * terminated or the stack cannot be retrieved for some other reason. + */ + private StackTraceElement[] getStackTrace(Thread thread) { + try { + return thread.getStackTrace(); + } catch (SecurityException e) { + return new StackTraceElement[0]; + } + } + + /** + * Determines whether the test appears to be stuck in some thread other than + * the "main thread" (the one created to run the test). This feature is experimental. + * Behavior may change after the 4.12 release in response to feedback. + * @param mainThread The main thread created by {@code evaluate()} + * @return The thread which appears to be causing the problem, if different from + * {@code mainThread}, or {@code null} if the main thread appears to be the + * problem or if the thread cannot be determined. The return value is never equal + * to {@code mainThread}. + */ + private Thread getStuckThread(Thread mainThread) { + List<Thread> threadsInGroup = getThreadsInGroup(mainThread.getThreadGroup()); + if (threadsInGroup.isEmpty()) { + return null; + } + + // Now that we have all the threads in the test's thread group: Assume that + // any thread we're "stuck" in is RUNNABLE. Look for all RUNNABLE threads. + // If just one, we return that (unless it equals threadMain). If there's more + // than one, pick the one that's using the most CPU time, if this feature is + // supported. + Thread stuckThread = null; + long maxCpuTime = 0; + for (Thread thread : threadsInGroup) { + if (thread.getState() == Thread.State.RUNNABLE) { + long threadCpuTime = cpuTime(thread); + if (stuckThread == null || threadCpuTime > maxCpuTime) { + stuckThread = thread; + maxCpuTime = threadCpuTime; + } + } + } + return (stuckThread == mainThread) ? null : stuckThread; + } + + /** + * Returns all active threads belonging to a thread group. + * @param group The thread group. + * @return The active threads in the thread group. The result should be a + * complete list of the active threads at some point in time. Returns an empty list + * if this cannot be determined, e.g. because new threads are being created at an + * extremely fast rate. + */ + private List<Thread> getThreadsInGroup(ThreadGroup group) { + final int activeThreadCount = group.activeCount(); // this is just an estimate + int threadArraySize = Math.max(activeThreadCount * 2, 100); + for (int loopCount = 0; loopCount < 5; loopCount++) { + Thread[] threads = new Thread[threadArraySize]; + int enumCount = group.enumerate(threads); + if (enumCount < threadArraySize) { + return Arrays.asList(threads).subList(0, enumCount); + } + // if there are too many threads to fit into the array, enumerate's result + // is >= the array's length; therefore we can't trust that it returned all + // the threads. Try again. + threadArraySize += 100; + } + // threads are proliferating too fast for us. Bail before we get into + // trouble. + return Collections.emptyList(); + } + + /** + * Returns the CPU time used by a thread, if possible. + * @param thr The thread to query. + * @return The CPU time used by {@code thr}, or 0 if it cannot be determined. + */ + private long cpuTime(Thread thr) { + ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); + if (mxBean.isThreadCpuTimeSupported()) { + try { + return mxBean.getThreadCpuTime(thr.getId()); + } catch (UnsupportedOperationException e) { + } + } + return 0; } private class CallableStatement implements Callable<Throwable> { diff --git a/src/main/java/org/junit/internal/runners/statements/RunAfters.java b/src/main/java/org/junit/internal/runners/statements/RunAfters.java index 7512a7d..5e56c33 100644 --- a/src/main/java/org/junit/internal/runners/statements/RunAfters.java +++ b/src/main/java/org/junit/internal/runners/statements/RunAfters.java @@ -30,7 +30,7 @@ public class RunAfters extends Statement { } finally { for (FrameworkMethod each : afters) { try { - each.invokeExplosively(target); + invokeMethod(each); } catch (Throwable e) { errors.add(e); } @@ -38,4 +38,11 @@ public class RunAfters extends Statement { } MultipleFailureException.assertEmpty(errors); } + + /** + * @since 4.13 + */ + protected void invokeMethod(FrameworkMethod method) throws Throwable { + method.invokeExplosively(target); + } }
\ No newline at end of file diff --git a/src/main/java/org/junit/internal/runners/statements/RunBefores.java b/src/main/java/org/junit/internal/runners/statements/RunBefores.java index 238fbe7..bd835c7 100644 --- a/src/main/java/org/junit/internal/runners/statements/RunBefores.java +++ b/src/main/java/org/junit/internal/runners/statements/RunBefores.java @@ -21,8 +21,15 @@ public class RunBefores extends Statement { @Override public void evaluate() throws Throwable { for (FrameworkMethod before : befores) { - before.invokeExplosively(target); + invokeMethod(before); } next.evaluate(); } + + /** + * @since 4.13 + */ + protected void invokeMethod(FrameworkMethod method) throws Throwable { + method.invokeExplosively(target); + } }
\ No newline at end of file |