Skip to content

Commit 7140dee

Browse files
authored
Consolidate watcher interfaces via JUnitWatcher marker (#63)
Resolves #61 This set of revisions consolidates the declaration of JUnit Foundation service providers into a single configuration file. Support for the previous file-per-interface approach is retained to provide backward compatibility. One significant benefit to switching from ServiceLoaders to pre-initialized unmodifiable lists is that I no longer need to synchronize my iterators. The code is therefore smaller and less complicated. Another benefit is that we no longer end up with multiple instances of each service provider that implements multiple interfaces. Previously, a separate instance would be created for each implemented interface.
1 parent 7014709 commit 7140dee

File tree

8 files changed

+194
-169
lines changed

8 files changed

+194
-169
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,17 +229,21 @@ To enable notifications in the native test runner of IDEs like Eclipse or IDEA,
229229

230230
To provide reliable, consistent behavior regardless of execution environment, **JUnit Foundation** notification subscribers are registered through the standard Java **ServiceLoader** mechanism. To attach **JUnit Foundation** watchers and standard JUnit run listeners to your tests, declare them in **ServiceLoader** [provider configuration files](https://docs.oracle.com/javase/tutorial/ext/basics/spi.html#register-service-providers) in a **_META-INF/services/_** folder of your project resources:
231231

232-
###### com.nordstrom.automation.junit.MethodWatcher
232+
###### com.nordstrom.automation.junit.JUnitWatcher
233233
```
234-
com.mycompany.example.MyWatcher
234+
# implements com.nordstrom.automation.junit.MethodWatcher
235+
com.mycompany.example.MyMethodWatcher
236+
# implements com.nordstrom.automation.junit.ShutdownListener
237+
com.mycompany.example.MyShutdownListener
235238
```
236239

237240
###### org.junit.runner.notification.RunListener
238241
```
239-
com.mycompany.example.MyListener
242+
# implements org.junit.runner.notification.RunListener
243+
com.mycompany.example.MyRunListener
240244
```
241245

242-
The preceding **ServiceLoader** provider configuration files declare a **JUnit Foundation** [MethodWatcher](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/MethodWatcher.java) and a standard JUnit [RunListener](https://github.com/junit-team/junit4/blob/41d44734f41aba0cf6ba5a11ff5d32ffed155027/src/main/java/org/junit/runner/notification/RunListener.java).
246+
The preceding **ServiceLoader** provider configuration files declare a **JUnit Foundation** [MethodWatcher](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/MethodWatcher.java), a **JUnit Foundation** [ShutdownListener](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ShutdownListener.java), and a standard **JUnit** [RunListener](https://github.com/junit-team/junit4/blob/41d44734f41aba0cf6ba5a11ff5d32ffed155027/src/main/java/org/junit/runner/notification/RunListener.java). Note that all **JUnit Foundation** watcher/listener service providers are declared in a single common configuration file to eliminate the need to declare classes implementing multiple interfaces in several different configuration files.
243247

244248
### Defined Service Provider Interfaces
245249

@@ -443,7 +447,7 @@ Failed attempts of tests that are selected for retry are tallied as ignored test
443447
**JUnit** provides a run listener feature, but this operates most readily on a per-class basis. The method for attaching these run listeners also imposes structural and operational constraints on **JUnit** projects, and the configuration required to register for end-of-suite notifications necessitates hard-coding the composition of the suite. All of these factors make run listeners unattractive or ineffectual for final cleanup operations.
444448

445449
**JUnit Foundation** enables you to declare shutdown listeners in a service loader configuration file.
446-
**_META-INF/services/com.nordstrom.automation.junit.ShutdownListener_** is the service loader shutdown listener configuration file. By default, this file is absent. To add managed listeners, create this file and add the fully-qualified names of their classes, one line per item. When it loads, the **JUnit Foundation** Java agent uses the service loader to instantiate your shutdown listeners and attaches them to the active JVM.
450+
As shown previously under [ServiceLoader Configuration Files](#serviceloader-configuration-files), **_META-INF/services/com.nordstrom.automation.junit.JUnitWatcher_** is the configuration file where shutdown listeners are declared. By default, this file is absent. To add managed listeners, create this file and add the fully-qualified names of their classes, one line per item. When it loads, the **JUnit Foundation** Java agent uses the service loader to instantiate your shutdown listeners and attaches them to the active JVM.
447451

448452
## Artifact Capture
449453

src/main/java/com/nordstrom/automation/junit/CreateTest.java

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.nordstrom.automation.junit;
22

33
import java.util.Map;
4-
import java.util.ServiceLoader;
54
import java.util.concurrent.Callable;
65
import java.util.concurrent.ConcurrentHashMap;
76
import org.slf4j.Logger;
87
import org.slf4j.LoggerFactory;
98

10-
import com.google.common.base.Optional;
119
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
1210
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1311
import net.bytebuddy.implementation.bind.annotation.This;
@@ -19,14 +17,12 @@
1917
@SuppressWarnings("squid:S1118")
2018
public class CreateTest {
2119

22-
private static final ServiceLoader<TestObjectWatcher> objectWatcherLoader;
2320
private static final Map<Object, Object> TARGET_TO_RUNNER = new ConcurrentHashMap<>();
2421
private static final Map<Object, Object> RUNNER_TO_TARGET = new ConcurrentHashMap<>();
2522
private static final ThreadLocal<DepthGauge> DEPTH;
2623
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTest.class);
2724

2825
static {
29-
objectWatcherLoader = ServiceLoader.load(TestObjectWatcher.class);
3026
DEPTH = new ThreadLocal<DepthGauge>() {
3127
@Override
3228
protected DepthGauge initialValue() {
@@ -60,10 +56,8 @@ public static Object intercept(@This final Object runner,
6056

6157
if (DEPTH.get().atGroundLevel()) {
6258
LOGGER.debug("testObjectCreated: {}", target);
63-
synchronized(objectWatcherLoader) {
64-
for (TestObjectWatcher watcher : objectWatcherLoader) {
65-
watcher.testObjectCreated(target, runner);
66-
}
59+
for (TestObjectWatcher watcher : LifecycleHooks.getObjectWatchers()) {
60+
watcher.testObjectCreated(target, runner);
6761
}
6862
}
6963

@@ -91,26 +85,4 @@ static Object getRunnerForTarget(Object target) {
9185
static Object getTargetForRunner(Object runner) {
9286
return RUNNER_TO_TARGET.get(runner);
9387
}
94-
95-
/**
96-
* Get reference to an instance of the specified watcher type.
97-
*
98-
* @param <T> watcher type
99-
* @param watcherType watcher type
100-
* @return optional watcher instance
101-
*/
102-
@SuppressWarnings("unchecked")
103-
static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> watcherType) {
104-
if (TestObjectWatcher.class.isAssignableFrom(watcherType)) {
105-
synchronized(objectWatcherLoader) {
106-
for (TestObjectWatcher watcher : objectWatcherLoader) {
107-
if (watcher.getClass() == watcherType) {
108-
return Optional.of((T) watcher);
109-
}
110-
}
111-
}
112-
}
113-
return Optional.absent();
114-
}
115-
11688
}

src/main/java/com/nordstrom/automation/junit/LifecycleHooks.java

Lines changed: 157 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import java.lang.instrument.Instrumentation;
77
import java.lang.reflect.Field;
88
import java.lang.reflect.InvocationTargetException;
9+
import java.util.AbstractList;
10+
import java.util.ArrayList;
911
import java.util.Arrays;
12+
import java.util.List;
1013
import java.util.ServiceLoader;
1114
import java.util.concurrent.Callable;
1215
import java.util.concurrent.ConcurrentMap;
@@ -36,6 +39,11 @@
3639
public class LifecycleHooks {
3740

3841
private static JUnitConfig config;
42+
private static final List<JUnitWatcher> watchers;
43+
private static final List<RunWatcher<?>> runWatchers;
44+
private static final List<RunnerWatcher> runnerWatchers;
45+
private static final List<TestObjectWatcher> objectWatchers;
46+
private static final List<MethodWatcher<?>> methodWatchers;
3947

4048
private LifecycleHooks() {
4149
throw new AssertionError("LifecycleHooks is a static utility class that cannot be instantiated");
@@ -46,8 +54,68 @@ private LifecycleHooks() {
4654
* and BlockJUnit4ClassRunner classes to enable the core functionality of JUnit Foundation.
4755
*/
4856
static {
49-
for (ShutdownListener listener : ServiceLoader.load(ShutdownListener.class)) {
50-
Runtime.getRuntime().addShutdownHook(getShutdownHook(listener));
57+
WatcherClassifier classifier = new WatcherClassifier();
58+
59+
for (JUnitWatcher watcher : ServiceLoader.load(JUnitWatcher.class)) {
60+
classifier.add(watcher);
61+
}
62+
63+
for (ShutdownListener watcher : ServiceLoader.load(ShutdownListener.class)) {
64+
classifier.add(watcher);
65+
}
66+
67+
for (RunWatcher<?> watcher : ServiceLoader.load(RunWatcher.class)) {
68+
classifier.add(watcher);
69+
}
70+
71+
for (RunnerWatcher watcher : ServiceLoader.load(RunnerWatcher.class)) {
72+
classifier.add(watcher);
73+
}
74+
75+
for (TestObjectWatcher watcher : ServiceLoader.load(TestObjectWatcher.class)) {
76+
classifier.add(watcher);
77+
}
78+
79+
for (MethodWatcher<?> watcher : ServiceLoader.load(MethodWatcher.class)) {
80+
classifier.add(watcher);
81+
}
82+
83+
watchers = classifier.watchers;
84+
85+
runWatchers = new WatcherList<>(classifier.runWatcherIndexes);
86+
runnerWatchers = new WatcherList<>(classifier.runnerWatcherIndexes);
87+
objectWatchers = new WatcherList<>(classifier.objectWatcherIndexes);
88+
methodWatchers = new WatcherList<>(classifier.methodWatcherIndexes);
89+
}
90+
91+
private static class WatcherClassifier {
92+
int i = 0;
93+
94+
List<JUnitWatcher> watchers = new ArrayList<>();
95+
List<Class<? extends JUnitWatcher>> watcherClasses = new ArrayList<>();
96+
97+
List<Integer> runWatcherIndexes = new ArrayList<>();
98+
List<Integer> runnerWatcherIndexes = new ArrayList<>();
99+
List<Integer> objectWatcherIndexes = new ArrayList<>();
100+
List<Integer> methodWatcherIndexes = new ArrayList<>();
101+
102+
boolean add(JUnitWatcher watcher) {
103+
if ( ! watcherClasses.contains(watcher.getClass())) {
104+
watchers.add(watcher);
105+
watcherClasses.add(watcher.getClass());
106+
107+
if (watcher instanceof ShutdownListener) {
108+
Runtime.getRuntime().addShutdownHook(getShutdownHook((ShutdownListener) watcher));
109+
}
110+
111+
if (watcher instanceof RunWatcher) runWatcherIndexes.add(i);
112+
if (watcher instanceof RunnerWatcher) runnerWatcherIndexes.add(i);
113+
if (watcher instanceof TestObjectWatcher) objectWatcherIndexes.add(i);
114+
if (watcher instanceof MethodWatcher) methodWatcherIndexes.add(i);
115+
116+
i++;
117+
}
118+
return false;
51119
}
52120
}
53121

@@ -334,17 +402,14 @@ static Object callProxy(final Callable<?> proxy) throws Exception {
334402
* @param watcherType watcher type
335403
* @return optional watcher instance
336404
*/
405+
@SuppressWarnings("unchecked")
337406
public static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> watcherType) {
338-
Optional<T> watcher = CreateTest.getAttachedWatcher(watcherType);
339-
if (watcher.isPresent()) return watcher;
340-
341-
watcher = Run.getAttachedWatcher(watcherType);
342-
if (watcher.isPresent()) return watcher;
343-
344-
watcher = RunAnnouncer.getAttachedWatcher(watcherType);
345-
if (watcher.isPresent()) return watcher;
346-
347-
return RunReflectiveCall.getAttachedWatcher(watcherType);
407+
for (JUnitWatcher watcher : watchers) {
408+
if (watcher.getClass() == watcherType) {
409+
return Optional.of((T) watcher);
410+
}
411+
}
412+
return Optional.absent();
348413
}
349414

350415
/**
@@ -378,4 +443,84 @@ static <K, T> T computeIfAbsent(ConcurrentMap<K, T> map, K key, Function<K, T> f
378443
}
379444
return val;
380445
}
446+
447+
/**
448+
* Get the list of attached {@link RunWatcher} objects.
449+
*
450+
* @return run watcher list
451+
*/
452+
static List<RunWatcher<?>> getRunWatchers() {
453+
return runWatchers;
454+
}
455+
456+
/**
457+
* Get the list of attached {@link RunnerWatcher} objects.
458+
*
459+
* @return runner watcher list
460+
*/
461+
static List<RunnerWatcher> getRunnerWatchers() {
462+
return runnerWatchers;
463+
}
464+
465+
/**
466+
* Get the list of attached {@link TestObjectWatcher} objects.
467+
*
468+
* @return test object watcher list
469+
*/
470+
static List<TestObjectWatcher> getObjectWatchers() {
471+
return objectWatchers;
472+
}
473+
474+
/**
475+
* Get the list of attached {@link MethodWatcher} objects.
476+
*
477+
* @return method watcher list
478+
*/
479+
static List<MethodWatcher<?>> getMethodWatchers() {
480+
return methodWatchers;
481+
}
482+
483+
/**
484+
* This class encapsulates the process of retrieving watcher objects of the target type from the collection of all
485+
* attached watcher objects. This is a private nested class that directly accesses the main collection. It is also
486+
* unmodifiable. Any attempts to alter the collection will trigger an {@link UnsupportedOperationException}.
487+
*
488+
* @param <T> subclass of {@link JUnitWatcher} object supplied by this instance
489+
*/
490+
private static class WatcherList<T extends JUnitWatcher> extends AbstractList<T> {
491+
492+
private int[] indexes;
493+
494+
/**
495+
* Constructor for a list of watcher objects of the target type retrieved from the collection of all attached
496+
* {@link JUnitWatcher} objects.
497+
*
498+
* @param indexes indexes of watchers of the requisite type in the main collection
499+
*/
500+
private WatcherList(List<Integer> indexes) {
501+
int i = 0;
502+
this.indexes = new int[indexes.size()];
503+
for (int index : indexes) {
504+
this.indexes[i++] = index;
505+
}
506+
}
507+
508+
/**
509+
* {@inheritDoc}
510+
*/
511+
@Override
512+
@SuppressWarnings("unchecked")
513+
public T get(int index) {
514+
return (T) watchers.get(indexes[index]);
515+
}
516+
517+
/**
518+
* {@inheritDoc}
519+
*/
520+
@Override
521+
public int size() {
522+
return indexes.length;
523+
}
524+
525+
}
381526
}

src/main/java/com/nordstrom/automation/junit/Run.java

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public class Run {
3131
private static final Set<String> startNotified = new CopyOnWriteArraySet<>();
3232
private static final Set<String> finishNotified = new CopyOnWriteArraySet<>();
3333
private static final ServiceLoader<RunListener> runListenerLoader;
34-
private static final ServiceLoader<RunnerWatcher> runnerWatcherLoader;
3534
private static final Map<Object, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
3635
private static final Set<RunNotifier> NOTIFIERS = new CopyOnWriteArraySet<>();
3736
private static final Logger LOGGER = LoggerFactory.getLogger(Run.class);
@@ -44,7 +43,6 @@ protected Deque<Object> initialValue() {
4443
}
4544
};
4645
runListenerLoader = ServiceLoader.load(RunListener.class);
47-
runnerWatcherLoader = ServiceLoader.load(RunnerWatcher.class);
4846
}
4947

5048
/**
@@ -143,10 +141,8 @@ static boolean fireRunStarted(Object runner) {
143141
CHILD_TO_PARENT.put(grandchild, runner);
144142
}
145143
LOGGER.debug("runStarted: {}", runner);
146-
synchronized(runnerWatcherLoader) {
147-
for (RunnerWatcher watcher : runnerWatcherLoader) {
148-
watcher.runStarted(runner);
149-
}
144+
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
145+
watcher.runStarted(runner);
150146
}
151147
return true;
152148
}
@@ -163,37 +159,14 @@ static boolean fireRunStarted(Object runner) {
163159
static boolean fireRunFinished(Object runner) {
164160
if (finishNotified.add(runner.toString())) {
165161
LOGGER.debug("runFinished: {}", runner);
166-
synchronized(runnerWatcherLoader) {
167-
for (RunnerWatcher watcher : runnerWatcherLoader) {
168-
watcher.runFinished(runner);
169-
}
162+
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
163+
watcher.runFinished(runner);
170164
}
171165
return true;
172166
}
173167
return false;
174168
}
175169

176-
/**
177-
* Get reference to an instance of the specified watcher type.
178-
*
179-
* @param <T> watcher type
180-
* @param watcherType watcher type
181-
* @return optional watcher instance
182-
*/
183-
@SuppressWarnings("unchecked")
184-
static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> watcherType) {
185-
if (RunnerWatcher.class.isAssignableFrom(watcherType)) {
186-
synchronized(runnerWatcherLoader) {
187-
for (RunnerWatcher watcher : runnerWatcherLoader) {
188-
if (watcher.getClass() == watcherType) {
189-
return Optional.of((T) watcher);
190-
}
191-
}
192-
}
193-
}
194-
return Optional.absent();
195-
}
196-
197170
/**
198171
* Get reference to an instance of the specified listener type.
199172
*

0 commit comments

Comments
 (0)