Skip to content

Commit 72af03c

Browse files
committed
Make RunNotifier code concurrent.
Squashed commit of the following commits from #625: commit a400a3a Author: Kevin Cooney <[email protected]> Date: Sat Mar 2 08:49:39 2013 -0800 Move wrapIfNotThreadSafe() to RunNotifier commit 38ac72e Author: Kevin Cooney <[email protected]> Date: Thu Feb 28 22:37:29 2013 -0800 Revert changes to build.xml commit e1172af Author: Kevin Cooney <[email protected]> Date: Thu Feb 28 22:13:52 2013 -0800 minor cleanup commit b11b5aa Author: Kevin Cooney <[email protected]> Date: Mon Feb 25 17:09:10 2013 -0800 Undo changes to notice commit a85566f Author: Kevin Cooney <[email protected]> Date: Mon Feb 25 17:08:11 2013 -0800 Make code style consistent commit 4b8efa1 Author: Kevin Cooney <[email protected]> Date: Sun Feb 24 17:45:23 2013 -0800 Update in response to code review commends by dsaff and Tibor17 commit a6fb342 Author: Kevin Cooney <[email protected]> Date: Sun Feb 24 17:44:29 2013 -0800 Make RunNotifierTest a JUnit4-style test again commit 6b39220 Author: Kevin Cooney <[email protected]> Date: Fri Feb 15 08:38:43 2013 -0800 Remove copyright commit 6a65cd9 Author: Kevin Cooney <[email protected]> Date: Thu Feb 14 17:50:25 2013 -0800 Remove unrelated changes commit 3997816 Merge: 10c5130 dbe8a97 Author: Kevin Cooney <[email protected]> Date: Thu Feb 14 17:40:40 2013 -0800 Merge branch 'master' into concurrent-run-listeners commit 10c5130 Author: Kevin Cooney <[email protected]> Date: Thu Feb 14 11:03:39 2013 -0800 Remove mention of jcip-annotations from NOTICE.txt Reduce diffs commit 2873269 Author: Kevin Cooney <[email protected]> Date: Thu Feb 14 10:46:09 2013 -0800 Replace Concurrent annotation with RunNotifier.ThreadSafe Update Javadoc Delete AddRemoveListenerTest (functionality covered by RunNotifierTest) commit 58a5a92 Merge: 02f1486 cc7d45b Author: Kevin Cooney <[email protected]> Date: Thu Feb 7 08:56:08 2013 -0800 Remove dependency on jcip-annotations commit cc7d45b Merge: 789c302 02f1486 Author: Kevin Cooney <[email protected]> Date: Thu Feb 7 08:55:24 2013 -0800 Merge branch 'concurrent-run-listeners' into concurrent-run-listeners_remove-jcip-deps Conflicts: src/test/java/org/junit/runner/notification/SynchronizedRunListenerTest.java commit 789c302 Author: Kevin Cooney <[email protected]> Date: Thu Feb 7 08:53:11 2013 -0800 Remove tabs commit 02f1486 Merge: a04aae1 708d972 Author: Kevin Cooney <[email protected]> Date: Thu Feb 7 08:47:40 2013 -0800 Merge branch 'Tibor17-junit.issues' into concurrent-run-listeners Conflicts: src/test/java/org/junit/runner/notification/SynchronizedRunListenerTest.java src/test/java/org/junit/tests/AllTests.java commit 708d972 Author: Tibor Digana <[email protected]> Date: Thu Feb 7 01:07:10 2013 +0100 threadsafe annotation commit 796801e Author: Kevin Cooney <[email protected]> Date: Mon Feb 4 17:17:16 2013 -0800 Remove jcip-annotations dependency; add @Concurrent annotation commit e30fe38 Author: Tibor Digana <[email protected]> Date: Tue Feb 5 01:18:13 2013 +0100 AllTests run SynchronizedRunListenerTest commit 0c35851 Author: Tibor Digana <[email protected]> Date: Tue Feb 5 01:11:43 2013 +0100 clarified the purpose of SynchronizedRunListener commit f8016a4 Author: Tibor Digana <[email protected]> Date: Tue Feb 5 01:09:25 2013 +0100 merged from https://github.com/kcooney/junit/blob/c099a99b76ccd985639882369652dfcadb3722b4/.classpath commit a5e8a71 Author: Tibor Digana <[email protected]> Date: Tue Feb 5 01:03:44 2013 +0100 merged from https://github.com/kcooney/junit/blob/41626fd3022c0d73d6f02807be0344a9cf6acafa/src/test/java/org/junit/runner/notification/SynchronizedRunListenerTest.java commit a04aae1 Author: Kevin Cooney <[email protected]> Date: Mon Feb 4 13:31:29 2013 -0800 Add tests in AddRemoveTestListenerTest to RunNotifierTest commit 5112c1a Merge: 41626fd c220ad7 Author: Kevin Cooney <[email protected]> Date: Mon Feb 4 13:07:37 2013 -0800 Merge branch 'Tibor17-junit.issues' into concurrent-run-listeners Conflicts: build.xml src/main/java/org/junit/runner/notification/RunNotifier.java src/main/java/org/junit/runner/notification/SynchronizedRunListener.java src/test/java/org/junit/tests/AllTests.java commit c220ad7 Author: Tibor Digana <[email protected]> Date: Mon Feb 4 00:42:22 2013 +0100 using COWAL + backward compatible + SynchronizedRunListener commit 41626fd Author: Kevin Cooney <[email protected]> Date: Sun Feb 3 09:54:24 2013 -0800 Cleanup implementation and tests commit c099a99 Author: Kevin Cooney <[email protected]> Date: Sat Feb 2 17:26:39 2013 -0800 Fix equals method for SynchronizedRunListener commit 1d6914b Author: Tibor Digana <[email protected]> Date: Fri Feb 1 00:42:38 2013 +0100 backward compatible, SynchronizedRunListener, removed EqualRunListener commit 21a926e Author: Tibor Digana <[email protected]> Date: Wed Jan 30 20:01:55 2013 +0100 problem to remove synchronized listener commit 20c6b53 Merge: 573828c 27ba66f Author: Tibor Digana <[email protected]> Date: Tue Jan 29 19:37:25 2013 +0100 Merge branch 'master' of git://github.com/KentBeck/junit into junit.issues commit 573828c Author: Tibor Digana <[email protected]> Date: Sun Jan 20 22:38:12 2013 +0100 AllTests commit 497e6ca Merge: a691962 3aca014 Author: Tibor Digana <[email protected]> Date: Sun Jan 20 22:29:44 2013 +0100 Merge branch 'master' of git://github.com/KentBeck/junit into junit.issues commit a691962 Author: Tibor Digana <[email protected]> Date: Sun Jan 20 22:28:12 2013 +0100 #realUsage test, simplified commit d2f1710 Author: Tibor Digana <[email protected]> Date: Thu Dec 20 16:06:19 2012 +0100 1E3 -> 1000, 1E2 -> 100 commit 4df3e81 Author: Tibor Digana <[email protected]> Date: Wed Dec 19 19:41:23 2012 +0100 synchronized substituted by thread safe collection
1 parent 6fd44da commit 72af03c

File tree

8 files changed

+626
-54
lines changed

8 files changed

+626
-54
lines changed

src/main/java/org/junit/runner/Result.java

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public boolean wasSuccessful() {
6565
return getFailureCount() == 0;
6666
}
6767

68+
@RunListener.ThreadSafe
6869
private class Listener extends RunListener {
6970
@Override
7071
public void testRunStarted(Description description) throws Exception {

src/main/java/org/junit/runner/notification/RunListener.java

+51-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package org.junit.runner.notification;
22

3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
39
import org.junit.internal.AssumptionViolatedException;
410
import org.junit.runner.Description;
511
import org.junit.runner.Result;
612

713
/**
8-
* <p>If you need to respond to the events during a test run, extend <code>RunListener</code>
9-
* and override the appropriate methods. If a listener throws an exception while processing a
10-
* test event, it will be removed for the remainder of the test run.</p>
14+
* Register an instance of this class with {@link RunNotifier} to be notified
15+
* of events that occur during a test run. All of the methods in this class
16+
* are abstract and have no implementation; override one or more methods to
17+
* receive events.
1118
*
1219
* <p>For example, suppose you have a <code>Cowbell</code>
1320
* class that you want to make a noise whenever a test fails. You could write:
@@ -18,7 +25,6 @@
1825
* }
1926
* }
2027
* </pre>
21-
* </p>
2228
*
2329
* <p>To invoke your listener, you need to run your tests through <code>JUnitCore</code>.
2430
* <pre>
@@ -28,23 +34,36 @@
2834
* core.run(MyTestClass.class);
2935
* }
3036
* </pre>
31-
* </p>
37+
*
38+
* <p>If a listener throws an exception for a test event, the other listeners will
39+
* have their {@link RunListener#testFailure(Failure)} called with a {@code Description}
40+
* of {@link Description#TEST_MECHANISM} to indicate the failure.
41+
*
42+
* <p>By default, JUnit will synchronize calls to your listener. If your listener
43+
* is thread-safe and you want to allow JUnit to call your listener from
44+
* multiple threads when tests are run in parallel, you can annotate your
45+
* test class with {@link RunListener.ThreadSafe}.
46+
*
47+
* <p>Listener methods will be called from the same thread as is running
48+
* the test, unless otherwise indicated by the method Javadoc
3249
*
3350
* @see org.junit.runner.JUnitCore
3451
* @since 4.0
3552
*/
3653
public class RunListener {
3754

3855
/**
39-
* Called before any tests have been run.
56+
* Called before any tests have been run. This may be called on an
57+
* arbitrary thread.
4058
*
4159
* @param description describes the tests to be run
4260
*/
4361
public void testRunStarted(Description description) throws Exception {
4462
}
4563

4664
/**
47-
* Called when all tests have finished
65+
* Called when all tests have finished. This may be called on an
66+
* arbitrary thread.
4867
*
4968
* @param result the summary of the test run, including all the tests that failed
5069
*/
@@ -69,7 +88,16 @@ public void testFinished(Description description) throws Exception {
6988
}
7089

7190
/**
72-
* Called when an atomic test fails.
91+
* Called when an atomic test fails, or when a listener throws an exception.
92+
*
93+
* <p>In the case of a failure of an atomic test, this method will be called
94+
* with the same {@code Description} passed to
95+
* {@link #testStarted(Description)}, from the same thread that called
96+
* {@link #testStarted(Description)}.
97+
*
98+
* <p>In the case of a listener throwing an exception, this will be called with
99+
* a {@code Description} of {@link Description#TEST_MECHANISM}, and may be called
100+
* on an arbitrary thread.
73101
*
74102
* @param failure describes the test that failed and the exception that was thrown
75103
*/
@@ -94,6 +122,20 @@ public void testAssumptionFailure(Failure failure) {
94122
*/
95123
public void testIgnored(Description description) throws Exception {
96124
}
97-
}
98125

99126

127+
/**
128+
* Indicates a {@code RunListener} that can have its methods called
129+
* concurrently. This implies that the class is thread-safe (i.e. no set of
130+
* listener calls can put the listener into an invalid state, even if those
131+
* listener calls are being made by multiple threads without
132+
* synchronization).
133+
*
134+
* @since 4.12
135+
*/
136+
@Documented
137+
@Target(ElementType.TYPE)
138+
@Retention(RetentionPolicy.RUNTIME)
139+
public @interface ThreadSafe {
140+
}
141+
}

src/main/java/org/junit/runner/notification/RunNotifier.java

+37-35
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import static java.util.Arrays.asList;
44

55
import java.util.ArrayList;
6-
import java.util.Collections;
7-
import java.util.Iterator;
6+
import java.util.Collection;
87
import java.util.List;
8+
import java.util.concurrent.CopyOnWriteArrayList;
99

1010
import org.junit.internal.AssumptionViolatedException;
1111
import org.junit.runner.Description;
@@ -21,24 +21,39 @@
2121
* @since 4.0
2222
*/
2323
public class RunNotifier {
24-
private final List<RunListener> fListeners =
25-
Collections.synchronizedList(new ArrayList<RunListener>());
24+
private final List<RunListener> fListeners = new CopyOnWriteArrayList<RunListener>();
2625
private volatile boolean fPleaseStop = false;
2726

2827
/**
2928
* Internal use only
3029
*/
3130
public void addListener(RunListener listener) {
32-
fListeners.add(listener);
31+
if (listener == null) {
32+
throw new NullPointerException("Cannot add a null listener");
33+
}
34+
fListeners.add(wrapIfNotThreadSafe(listener));
3335
}
3436

3537
/**
3638
* Internal use only
3739
*/
3840
public void removeListener(RunListener listener) {
39-
fListeners.remove(listener);
41+
if (listener == null) {
42+
throw new NullPointerException("Cannot remove a null listener");
43+
}
44+
fListeners.remove(wrapIfNotThreadSafe(listener));
45+
}
46+
47+
/**
48+
* Wraps the given listener with {@link SynchronizedRunListener} if
49+
* it is not annotated with {@link RunListener.ThreadSafe}.
50+
*/
51+
RunListener wrapIfNotThreadSafe(RunListener listener) {
52+
return listener.getClass().isAnnotationPresent(RunListener.ThreadSafe.class) ?
53+
listener : new SynchronizedRunListener(listener, this);
4054
}
4155

56+
4257
private abstract class SafeNotifier {
4358
private final List<RunListener> fCurrentListeners;
4459

@@ -51,21 +66,18 @@ private abstract class SafeNotifier {
5166
}
5267

5368
void run() {
54-
synchronized (fListeners) {
55-
List<RunListener> safeListeners = new ArrayList<RunListener>();
56-
List<Failure> failures = new ArrayList<Failure>();
57-
for (Iterator<RunListener> all = fCurrentListeners.iterator(); all
58-
.hasNext(); ) {
59-
try {
60-
RunListener listener = all.next();
61-
notifyListener(listener);
62-
safeListeners.add(listener);
63-
} catch (Exception e) {
64-
failures.add(new Failure(Description.TEST_MECHANISM, e));
65-
}
69+
int capacity = fCurrentListeners.size();
70+
ArrayList<RunListener> safeListeners = new ArrayList<RunListener>(capacity);
71+
ArrayList<Failure> failures = new ArrayList<Failure>(capacity);
72+
for (RunListener listener : fCurrentListeners) {
73+
try {
74+
notifyListener(listener);
75+
safeListeners.add(listener);
76+
} catch (Exception e) {
77+
failures.add(new Failure(Description.TEST_MECHANISM, e));
6678
}
67-
fireTestFailures(safeListeners, failures);
6879
}
80+
fireTestFailures(safeListeners, failures);
6981
}
7082

7183
abstract protected void notifyListener(RunListener each) throws Exception;
@@ -80,8 +92,6 @@ public void fireTestRunStarted(final Description description) {
8092
protected void notifyListener(RunListener each) throws Exception {
8193
each.testRunStarted(description);
8294
}
83-
84-
;
8595
}.run();
8696
}
8797

@@ -94,8 +104,6 @@ public void fireTestRunFinished(final Result result) {
94104
protected void notifyListener(RunListener each) throws Exception {
95105
each.testRunFinished(result);
96106
}
97-
98-
;
99107
}.run();
100108
}
101109

@@ -114,8 +122,6 @@ public void fireTestStarted(final Description description) throws StoppedByUserE
114122
protected void notifyListener(RunListener each) throws Exception {
115123
each.testStarted(description);
116124
}
117-
118-
;
119125
}.run();
120126
}
121127

@@ -133,14 +139,11 @@ private void fireTestFailures(List<RunListener> listeners,
133139
if (!failures.isEmpty()) {
134140
new SafeNotifier(listeners) {
135141
@Override
136-
protected void notifyListener(RunListener listener)
137-
throws Exception {
142+
protected void notifyListener(RunListener listener) throws Exception {
138143
for (Failure each : failures) {
139144
listener.testFailure(each);
140145
}
141146
}
142-
143-
;
144147
}.run();
145148
}
146149
}
@@ -158,8 +161,6 @@ public void fireTestAssumptionFailed(final Failure failure) {
158161
protected void notifyListener(RunListener each) throws Exception {
159162
each.testAssumptionFailure(failure);
160163
}
161-
162-
;
163164
}.run();
164165
}
165166

@@ -190,8 +191,6 @@ public void fireTestFinished(final Description description) {
190191
protected void notifyListener(RunListener each) throws Exception {
191192
each.testFinished(description);
192193
}
193-
194-
;
195194
}.run();
196195
}
197196

@@ -209,6 +208,9 @@ public void pleaseStop() {
209208
* Internal use only. The Result's listener must be first.
210209
*/
211210
public void addFirstListener(RunListener listener) {
212-
fListeners.add(0, listener);
211+
if (listener == null) {
212+
throw new NullPointerException("Cannot add a null listener");
213+
}
214+
fListeners.add(0, wrapIfNotThreadSafe(listener));
213215
}
214-
}
216+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.junit.runner.notification;
2+
3+
import org.junit.runner.Description;
4+
import org.junit.runner.Result;
5+
6+
/**
7+
* Thread-safe decorator for {@link RunListener} implementations that synchronizes
8+
* calls to the delegate.
9+
*
10+
* <p>This class synchronizes all listener calls on a RunNotifier instance. This is done because
11+
* prior to JUnit 4.12, all listeners were called in a synchronized block in RunNotifier,
12+
* so no two listeners were ever called concurrently. If we instead made the methods here
13+
* sychronized, clients that added multiple listeners that called common code might see
14+
* issues due to the reduced synchronization.
15+
*
16+
* @author Tibor Digana (tibor17)
17+
* @author Kevin Cooney (kcooney)
18+
* @since 4.12
19+
*
20+
* @see RunNotifier
21+
*/
22+
@RunListener.ThreadSafe
23+
final class SynchronizedRunListener extends RunListener {
24+
private final RunListener fListener;
25+
private final Object fMonitor;
26+
27+
SynchronizedRunListener(RunListener listener, Object monitor) {
28+
fListener = listener;
29+
fMonitor = monitor;
30+
}
31+
32+
@Override
33+
public void testRunStarted(Description description) throws Exception {
34+
synchronized (fMonitor) {
35+
fListener.testRunStarted(description);
36+
}
37+
}
38+
39+
@Override
40+
public void testRunFinished(Result result) throws Exception {
41+
synchronized (fMonitor) {
42+
fListener.testRunFinished(result);
43+
}
44+
}
45+
46+
@Override
47+
public void testStarted(Description description) throws Exception {
48+
synchronized (fMonitor) {
49+
fListener.testStarted(description);
50+
}
51+
}
52+
53+
@Override
54+
public void testFinished(Description description) throws Exception {
55+
synchronized (fMonitor) {
56+
fListener.testFinished(description);
57+
}
58+
}
59+
60+
@Override
61+
public void testFailure(Failure failure) throws Exception {
62+
synchronized (fMonitor) {
63+
fListener.testFailure(failure);
64+
}
65+
}
66+
67+
@Override
68+
public void testAssumptionFailure(Failure failure) {
69+
synchronized (fMonitor) {
70+
fListener.testAssumptionFailure(failure);
71+
}
72+
}
73+
74+
@Override
75+
public void testIgnored(Description description) throws Exception {
76+
synchronized (fMonitor) {
77+
fListener.testIgnored(description);
78+
}
79+
}
80+
81+
@Override
82+
public int hashCode() {
83+
return fListener.hashCode();
84+
}
85+
86+
@Override
87+
public boolean equals(Object other) {
88+
if (this == other) {
89+
return true;
90+
}
91+
if (!(other instanceof SynchronizedRunListener)) {
92+
return false;
93+
}
94+
SynchronizedRunListener that = (SynchronizedRunListener) other;
95+
96+
return fListener.equals(that.fListener);
97+
}
98+
99+
@Override
100+
public String toString() {
101+
return fListener.toString() + " (with synchronization wrapper)";
102+
}
103+
}

0 commit comments

Comments
 (0)