diff options
Diffstat (limited to 'src/main/java/org/junit/runner/notification')
5 files changed, 475 insertions, 268 deletions
diff --git a/src/main/java/org/junit/runner/notification/Failure.java b/src/main/java/org/junit/runner/notification/Failure.java index 501caa5..c03b4c1 100644 --- a/src/main/java/org/junit/runner/notification/Failure.java +++ b/src/main/java/org/junit/runner/notification/Failure.java @@ -12,68 +12,76 @@ import org.junit.runner.Description; * will be of a single test. However, if problems are encountered while constructing the * test (for example, if a {@link org.junit.BeforeClass} method is not static), it may describe * something other than a single test. + * + * @since 4.0 */ public class Failure implements Serializable { - private static final long serialVersionUID = 1L; - private final Description fDescription; - private final Throwable fThrownException; + private static final long serialVersionUID = 1L; - /** - * Constructs a <code>Failure</code> with the given description and exception. - * @param description a {@link org.junit.runner.Description} of the test that failed - * @param thrownException the exception that was thrown while running the test - */ - public Failure(Description description, Throwable thrownException) { - fThrownException = thrownException; - fDescription= description; - } + /* + * 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 + */ + private final Description fDescription; + private final Throwable fThrownException; - /** - * @return a user-understandable label for the test - */ - public String getTestHeader() { - return fDescription.getDisplayName(); - } + /** + * Constructs a <code>Failure</code> with the given description and exception. + * + * @param description a {@link org.junit.runner.Description} of the test that failed + * @param thrownException the exception that was thrown while running the test + */ + public Failure(Description description, Throwable thrownException) { + this.fThrownException = thrownException; + this.fDescription = description; + } - /** - * @return the raw description of the context of the failure. - */ - public Description getDescription() { - return fDescription; - } + /** + * @return a user-understandable label for the test + */ + public String getTestHeader() { + return fDescription.getDisplayName(); + } - /** - * @return the exception thrown - */ + /** + * @return the raw description of the context of the failure. + */ + public Description getDescription() { + return fDescription; + } - public Throwable getException() { - return fThrownException; - } + /** + * @return the exception thrown + */ - @Override - public String toString() { - StringBuffer buffer= new StringBuffer(); - buffer.append(getTestHeader() + ": "+fThrownException.getMessage()); - return buffer.toString(); - } + public Throwable getException() { + return fThrownException; + } - /** - * Convenience method - * @return the printed form of the exception - */ - public String getTrace() { - StringWriter stringWriter= new StringWriter(); - PrintWriter writer= new PrintWriter(stringWriter); - getException().printStackTrace(writer); - StringBuffer buffer= stringWriter.getBuffer(); - return buffer.toString(); - } + @Override + public String toString() { + return getTestHeader() + ": " + fThrownException.getMessage(); + } - /** - * Convenience method - * @return the message of the thrown exception - */ - public String getMessage() { - return getException().getMessage(); - } + /** + * Convenience method + * + * @return the printed form of the exception + */ + public String getTrace() { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + getException().printStackTrace(writer); + return stringWriter.toString(); + } + + /** + * Convenience method + * + * @return the message of the thrown exception + */ + public String getMessage() { + return getException().getMessage(); + } } diff --git a/src/main/java/org/junit/runner/notification/RunListener.java b/src/main/java/org/junit/runner/notification/RunListener.java index ffe8134..db9d8c1 100644 --- a/src/main/java/org/junit/runner/notification/RunListener.java +++ b/src/main/java/org/junit/runner/notification/RunListener.java @@ -1,15 +1,21 @@ package org.junit.runner.notification; -import org.junit.internal.AssumptionViolatedException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.runner.Description; import org.junit.runner.Result; /** - * <p>If you need to respond to the events during a test run, extend <code>RunListener</code> - * and override the appropriate methods. If a listener throws an exception while processing a - * test event, it will be removed for the remainder of the test run.</p> - * - * <p>For example, suppose you have a <code>Cowbell</code> + * Register an instance of this class with {@link RunNotifier} to be notified + * of events that occur during a test run. All of the methods in this class + * are abstract and have no implementation; override one or more methods to + * receive events. + * <p> + * For example, suppose you have a <code>Cowbell</code> * class that you want to make a noise whenever a test fails. You could write: * <pre> * public class RingingListener extends RunListener { @@ -18,9 +24,8 @@ import org.junit.runner.Result; * } * } * </pre> - * </p> - * - * <p>To invoke your listener, you need to run your tests through <code>JUnitCore</code>. + * <p> + * To invoke your listener, you need to run your tests through <code>JUnitCore</code>. * <pre> * public void main(String... args) { * JUnitCore core= new JUnitCore(); @@ -28,66 +33,108 @@ import org.junit.runner.Result; * core.run(MyTestClass.class); * } * </pre> - * </p> + * <p> + * If a listener throws an exception for a test event, the other listeners will + * have their {@link RunListener#testFailure(Failure)} called with a {@code Description} + * of {@link Description#TEST_MECHANISM} to indicate the failure. + * <p> + * By default, JUnit will synchronize calls to your listener. If your listener + * is thread-safe and you want to allow JUnit to call your listener from + * multiple threads when tests are run in parallel, you can annotate your + * test class with {@link RunListener.ThreadSafe}. + * <p> + * Listener methods will be called from the same thread as is running + * the test, unless otherwise indicated by the method Javadoc + * * @see org.junit.runner.JUnitCore + * @since 4.0 */ public class RunListener { - /** - * Called before any tests have been run. - * @param description describes the tests to be run - */ - public void testRunStarted(Description description) throws Exception { - } - - /** - * Called when all tests have finished - * @param result the summary of the test run, including all the tests that failed - */ - public void testRunFinished(Result result) throws Exception { - } - - /** - * Called when an atomic test is about to be started. - * @param description the description of the test that is about to be run - * (generally a class and method name) - */ - public void testStarted(Description description) throws Exception { - } + /** + * Called before any tests have been run. This may be called on an + * arbitrary thread. + * + * @param description describes the tests to be run + */ + public void testRunStarted(Description description) throws Exception { + } - /** - * Called when an atomic test has finished, whether the test succeeds or fails. - * @param description the description of the test that just ran - */ - public void testFinished(Description description) throws Exception { - } + /** + * Called when all tests have finished. This may be called on an + * arbitrary thread. + * + * @param result the summary of the test run, including all the tests that failed + */ + public void testRunFinished(Result result) throws Exception { + } - /** - * Called when an atomic test fails. - * @param failure describes the test that failed and the exception that was thrown - */ - public void testFailure(Failure failure) throws Exception { - } + /** + * Called when an atomic test is about to be started. + * + * @param description the description of the test that is about to be run + * (generally a class and method name) + */ + public void testStarted(Description description) throws Exception { + } - /** - * Called when an atomic test flags that it assumes a condition that is - * false - * - * @param failure - * describes the test that failed and the - * {@link AssumptionViolatedException} that was thrown - */ - public void testAssumptionFailure(Failure failure) { - } + /** + * Called when an atomic test has finished, whether the test succeeds or fails. + * + * @param description the description of the test that just ran + */ + public void testFinished(Description description) throws Exception { + } - /** - * Called when a test will not be run, generally because a test method is annotated - * with {@link org.junit.Ignore}. - * - * @param description describes the test that will not be run - */ - public void testIgnored(Description description) throws Exception { - } -} + /** + * Called when an atomic test fails, or when a listener throws an exception. + * + * <p>In the case of a failure of an atomic test, this method will be called + * with the same {@code Description} passed to + * {@link #testStarted(Description)}, from the same thread that called + * {@link #testStarted(Description)}. + * + * <p>In the case of a listener throwing an exception, this will be called with + * a {@code Description} of {@link Description#TEST_MECHANISM}, and may be called + * on an arbitrary thread. + * + * @param failure describes the test that failed and the exception that was thrown + */ + public void testFailure(Failure failure) throws Exception { + } + + /** + * Called when an atomic test flags that it assumes a condition that is + * false + * + * @param failure describes the test that failed and the + * {@link org.junit.AssumptionViolatedException} that was thrown + */ + public void testAssumptionFailure(Failure failure) { + } + /** + * Called when a test will not be run, generally because a test method is annotated + * with {@link org.junit.Ignore}. + * + * @param description describes the test that will not be run + */ + public void testIgnored(Description description) throws Exception { + } + + /** + * Indicates a {@code RunListener} that can have its methods called + * concurrently. This implies that the class is thread-safe (i.e. no set of + * listener calls can put the listener into an invalid state, even if those + * listener calls are being made by multiple threads without + * synchronization). + * + * @since 4.12 + */ + @Documented + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface ThreadSafe { + } +} diff --git a/src/main/java/org/junit/runner/notification/RunNotifier.java b/src/main/java/org/junit/runner/notification/RunNotifier.java index d0f6c85..6875f76 100644 --- a/src/main/java/org/junit/runner/notification/RunNotifier.java +++ b/src/main/java/org/junit/runner/notification/RunNotifier.java @@ -1,166 +1,214 @@ package org.junit.runner.notification; +import static java.util.Arrays.asList; + import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; -import org.junit.internal.AssumptionViolatedException; import org.junit.runner.Description; import org.junit.runner.Result; /** * If you write custom runners, you may need to notify JUnit of your progress running tests. * Do this by invoking the <code>RunNotifier</code> passed to your implementation of - * {@link org.junit.runner.Runner#run(RunNotifier)}. Future evolution of this class is likely to + * {@link org.junit.runner.Runner#run(RunNotifier)}. Future evolution of this class is likely to * move {@link #fireTestRunStarted(Description)} and {@link #fireTestRunFinished(Result)} * to a separate class since they should only be called once per run. + * + * @since 4.0 */ public class RunNotifier { - private final List<RunListener> fListeners= - Collections.synchronizedList(new ArrayList<RunListener>()); - private boolean fPleaseStop= false; - - /** Internal use only - */ - public void addListener(RunListener listener) { - fListeners.add(listener); - } - - /** Internal use only - */ - public void removeListener(RunListener listener) { - fListeners.remove(listener); + private final List<RunListener> listeners = new CopyOnWriteArrayList<RunListener>(); + private volatile boolean pleaseStop = false; + + /** + * Internal use only + */ + public void addListener(RunListener listener) { + if (listener == null) { + throw new NullPointerException("Cannot add a null listener"); + } + listeners.add(wrapIfNotThreadSafe(listener)); + } + + /** + * Internal use only + */ + public void removeListener(RunListener listener) { + if (listener == null) { + throw new NullPointerException("Cannot remove a null listener"); + } + listeners.remove(wrapIfNotThreadSafe(listener)); + } + + /** + * Wraps the given listener with {@link SynchronizedRunListener} if + * it is not annotated with {@link RunListener.ThreadSafe}. + */ + RunListener wrapIfNotThreadSafe(RunListener listener) { + return listener.getClass().isAnnotationPresent(RunListener.ThreadSafe.class) ? + listener : new SynchronizedRunListener(listener, this); + } + + + private abstract class SafeNotifier { + private final List<RunListener> currentListeners; + + SafeNotifier() { + this(listeners); + } + + SafeNotifier(List<RunListener> currentListeners) { + this.currentListeners = currentListeners; + } + + void run() { + int capacity = currentListeners.size(); + ArrayList<RunListener> safeListeners = new ArrayList<RunListener>(capacity); + ArrayList<Failure> failures = new ArrayList<Failure>(capacity); + for (RunListener listener : currentListeners) { + try { + notifyListener(listener); + safeListeners.add(listener); + } catch (Exception e) { + failures.add(new Failure(Description.TEST_MECHANISM, e)); + } + } + fireTestFailures(safeListeners, failures); + } + + abstract protected void notifyListener(RunListener each) throws Exception; + } + + /** + * Do not invoke. + */ + public void fireTestRunStarted(final Description description) { + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testRunStarted(description); + } + }.run(); + } + + /** + * Do not invoke. + */ + public void fireTestRunFinished(final Result result) { + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testRunFinished(result); + } + }.run(); + } + + /** + * Invoke to tell listeners that an atomic test is about to start. + * + * @param description the description of the atomic test (generally a class and method name) + * @throws StoppedByUserException thrown if a user has requested that the test run stop + */ + public void fireTestStarted(final Description description) throws StoppedByUserException { + if (pleaseStop) { + throw new StoppedByUserException(); + } + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testStarted(description); + } + }.run(); + } + + /** + * Invoke to tell listeners that an atomic test failed. + * + * @param failure the description of the test that failed and the exception thrown + */ + public void fireTestFailure(Failure failure) { + fireTestFailures(listeners, asList(failure)); + } + + private void fireTestFailures(List<RunListener> listeners, + final List<Failure> failures) { + if (!failures.isEmpty()) { + new SafeNotifier(listeners) { + @Override + protected void notifyListener(RunListener listener) throws Exception { + for (Failure each : failures) { + listener.testFailure(each); + } + } + }.run(); + } + } + + /** + * Invoke to tell listeners that an atomic test flagged that it assumed + * something false. + * + * @param failure the description of the test that failed and the + * {@link org.junit.AssumptionViolatedException} thrown + */ + public void fireTestAssumptionFailed(final Failure failure) { + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testAssumptionFailure(failure); + } + }.run(); + } + + /** + * Invoke to tell listeners that an atomic test was ignored. + * + * @param description the description of the ignored test + */ + public void fireTestIgnored(final Description description) { + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testIgnored(description); + } + }.run(); + } + + /** + * Invoke to tell listeners that an atomic test finished. Always invoke + * this method if you invoke {@link #fireTestStarted(Description)} + * as listeners are likely to expect them to come in pairs. + * + * @param description the description of the test that finished + */ + public void fireTestFinished(final Description description) { + new SafeNotifier() { + @Override + protected void notifyListener(RunListener each) throws Exception { + each.testFinished(description); + } + }.run(); + } + + /** + * Ask that the tests run stop before starting the next test. Phrased politely because + * the test currently running will not be interrupted. It seems a little odd to put this + * functionality here, but the <code>RunNotifier</code> is the only object guaranteed + * to be shared amongst the many runners involved. + */ + public void pleaseStop() { + pleaseStop = true; } - private abstract class SafeNotifier { - void run() { - synchronized (fListeners) { - for (Iterator<RunListener> all= fListeners.iterator(); all.hasNext();) - try { - notifyListener(all.next()); - } catch (Exception e) { - all.remove(); // Remove the offending listener first to avoid an infinite loop - fireTestFailure(new Failure(Description.TEST_MECHANISM, e)); - } - } - } - - abstract protected void notifyListener(RunListener each) throws Exception; - } - - /** - * Do not invoke. - */ - public void fireTestRunStarted(final Description description) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testRunStarted(description); - }; - }.run(); - } - - /** - * Do not invoke. - */ - public void fireTestRunFinished(final Result result) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testRunFinished(result); - }; - }.run(); - } - - /** - * Invoke to tell listeners that an atomic test is about to start. - * @param description the description of the atomic test (generally a class and method name) - * @throws StoppedByUserException thrown if a user has requested that the test run stop - */ - public void fireTestStarted(final Description description) throws StoppedByUserException { - if (fPleaseStop) - throw new StoppedByUserException(); - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testStarted(description); - }; - }.run(); - } - - /** - * Invoke to tell listeners that an atomic test failed. - * @param failure the description of the test that failed and the exception thrown - */ - public void fireTestFailure(final Failure failure) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testFailure(failure); - }; - }.run(); - } - - /** - * Invoke to tell listeners that an atomic test flagged that it assumed - * something false. - * - * @param failure - * the description of the test that failed and the - * {@link AssumptionViolatedException} thrown - */ - public void fireTestAssumptionFailed(final Failure failure) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testAssumptionFailure(failure); - }; - }.run(); - } - - /** - * Invoke to tell listeners that an atomic test was ignored. - * @param description the description of the ignored test - */ - public void fireTestIgnored(final Description description) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testIgnored(description); - } - }.run(); - } - - /** - * Invoke to tell listeners that an atomic test finished. Always invoke - * {@link #fireTestFinished(Description)} if you invoke {@link #fireTestStarted(Description)} - * as listeners are likely to expect them to come in pairs. - * @param description the description of the test that finished - */ - public void fireTestFinished(final Description description) { - new SafeNotifier() { - @Override - protected void notifyListener(RunListener each) throws Exception { - each.testFinished(description); - }; - }.run(); - } - - /** - * Ask that the tests run stop before starting the next test. Phrased politely because - * the test currently running will not be interrupted. It seems a little odd to put this - * functionality here, but the <code>RunNotifier</code> is the only object guaranteed - * to be shared amongst the many runners involved. - */ - public void pleaseStop() { - fPleaseStop= true; - } - - /** - * Internal use only. The Result's listener must be first. - */ - public void addFirstListener(RunListener listener) { - fListeners.add(0, listener); - } -}
\ No newline at end of file + /** + * Internal use only. The Result's listener must be first. + */ + public void addFirstListener(RunListener listener) { + if (listener == null) { + throw new NullPointerException("Cannot add a null listener"); + } + listeners.add(0, wrapIfNotThreadSafe(listener)); + } +} diff --git a/src/main/java/org/junit/runner/notification/StoppedByUserException.java b/src/main/java/org/junit/runner/notification/StoppedByUserException.java index 89be3ba..f5490f7 100644 --- a/src/main/java/org/junit/runner/notification/StoppedByUserException.java +++ b/src/main/java/org/junit/runner/notification/StoppedByUserException.java @@ -1,11 +1,12 @@ package org.junit.runner.notification; /** - * Thrown when a user has requested that the test run stop. Writers of + * Thrown when a user has requested that the test run stop. Writers of * test running GUIs should be prepared to catch a <code>StoppedByUserException</code>. - * + * * @see org.junit.runner.notification.RunNotifier + * @since 4.0 */ public class StoppedByUserException extends RuntimeException { - private static final long serialVersionUID= 1L; + private static final long serialVersionUID = 1L; } diff --git a/src/main/java/org/junit/runner/notification/SynchronizedRunListener.java b/src/main/java/org/junit/runner/notification/SynchronizedRunListener.java new file mode 100644 index 0000000..c53c1ee --- /dev/null +++ b/src/main/java/org/junit/runner/notification/SynchronizedRunListener.java @@ -0,0 +1,103 @@ +package org.junit.runner.notification; + +import org.junit.runner.Description; +import org.junit.runner.Result; + +/** + * Thread-safe decorator for {@link RunListener} implementations that synchronizes + * calls to the delegate. + * + * <p>This class synchronizes all listener calls on a RunNotifier instance. This is done because + * prior to JUnit 4.12, all listeners were called in a synchronized block in RunNotifier, + * so no two listeners were ever called concurrently. If we instead made the methods here + * sychronized, clients that added multiple listeners that called common code might see + * issues due to the reduced synchronization. + * + * @author Tibor Digana (tibor17) + * @author Kevin Cooney (kcooney) + * @since 4.12 + * + * @see RunNotifier + */ +@RunListener.ThreadSafe +final class SynchronizedRunListener extends RunListener { + private final RunListener listener; + private final Object monitor; + + SynchronizedRunListener(RunListener listener, Object monitor) { + this.listener = listener; + this.monitor = monitor; + } + + @Override + public void testRunStarted(Description description) throws Exception { + synchronized (monitor) { + listener.testRunStarted(description); + } + } + + @Override + public void testRunFinished(Result result) throws Exception { + synchronized (monitor) { + listener.testRunFinished(result); + } + } + + @Override + public void testStarted(Description description) throws Exception { + synchronized (monitor) { + listener.testStarted(description); + } + } + + @Override + public void testFinished(Description description) throws Exception { + synchronized (monitor) { + listener.testFinished(description); + } + } + + @Override + public void testFailure(Failure failure) throws Exception { + synchronized (monitor) { + listener.testFailure(failure); + } + } + + @Override + public void testAssumptionFailure(Failure failure) { + synchronized (monitor) { + listener.testAssumptionFailure(failure); + } + } + + @Override + public void testIgnored(Description description) throws Exception { + synchronized (monitor) { + listener.testIgnored(description); + } + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SynchronizedRunListener)) { + return false; + } + SynchronizedRunListener that = (SynchronizedRunListener) other; + + return listener.equals(that.listener); + } + + @Override + public String toString() { + return listener.toString() + " (with synchronization wrapper)"; + } +} |