Skip to content

Commit 17d04f2

Browse files
authored
Fix event sequencing issues
1 parent aed26fd commit 17d04f2

File tree

12 files changed

+193
-231
lines changed

12 files changed

+193
-231
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class AtomicTest {
3535
private static final Pattern PARAM = Pattern.compile("[(\\[]");
3636

3737
public AtomicTest(Description description) {
38-
this.runner = RunChildren.getThreadRunner();
38+
this.runner = Run.getThreadRunner();
3939
this.description = description;
4040
this.particles = getParticles(runner, description);
4141
this.identity = this.particles.get(0);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static Object intercept(@This final Object runner, @Argument(0) final Fra
6666
Object target = LifecycleHooks.callProxy(proxy);
6767

6868
if (0 == depthGauge.decreaseDepth()) {
69-
METHOD_DEPTH.remove();
69+
METHOD_DEPTH.get().remove(hashCode);
7070
LOGGER.debug("testObjectCreated: {}", target);
7171
TARGET_TO_METHOD.put(toMapKey(target), method);
7272

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,
3737

3838
if (description.isTest()) {
3939
newAtomicTestFor(description);
40-
Object runner = RunChildren.getThreadRunner();
40+
Object runner = Run.getThreadRunner();
4141
FrameworkMethod method = null;
4242
List<Object> children = LifecycleHooks.invoke(runner, "getChildren");
4343
for (Object child : children) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public class Finished {
2020
*/
2121
public static void intercept(@This final Object scheduler, @SuperCall final Callable<?> proxy) throws Exception {
2222
LifecycleHooks.callProxy(proxy);
23-
RunChildren.finished();
23+
RunChild.finished();
2424
}
2525
}

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
166166
final TypeDescription fireTestFinished = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.FireTestFinished").resolve();
167167
final TypeDescription runReflectiveCall = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunReflectiveCall").resolve();
168168
final TypeDescription finished = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Finished").resolve();
169-
final TypeDescription schedule = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Schedule").resolve();
170-
final TypeDescription runChildren = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChildren").resolve();
171169
final TypeDescription runChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChild").resolve();
172170
final TypeDescription run = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Run").resolve();
173171
final TypeDescription describeChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.DescribeChild").resolve();
@@ -178,7 +176,6 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
178176

179177
final TypeDescription runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
180178
final TypeDescription description = TypePool.Default.ofSystemLoader().describe("org.junit.runner.Description").resolve();
181-
final SignatureToken runChildrenToken = new SignatureToken("runChildren", TypeDescription.VOID, Arrays.asList(runNotifier));
182179
final SignatureToken runToken = new SignatureToken("run", TypeDescription.VOID, Arrays.asList(runNotifier));
183180

184181
final TypeDescription frameworkMethod = TypePool.Default.ofSystemLoader().describe("org.junit.runners.model.FrameworkMethod").resolve();
@@ -210,8 +207,7 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
210207
@Override
211208
public Builder<?> transform(Builder<?> builder, TypeDescription type,
212209
ClassLoader classloader, JavaModule module) {
213-
return builder.method(named("schedule")).intercept(MethodDelegation.to(schedule))
214-
.method(named("finished")).intercept(MethodDelegation.to(finished))
210+
return builder.method(named("finished")).intercept(MethodDelegation.to(finished))
215211
.implement(Hooked.class);
216212
}
217213
})
@@ -221,7 +217,6 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
221217
public Builder<?> transform(Builder<?> builder, TypeDescription type,
222218
ClassLoader classloader, JavaModule module) {
223219
return builder.method(named("runChild")).intercept(MethodDelegation.to(runChild))
224-
.method(hasSignature(runChildrenToken)).intercept(MethodDelegation.to(runChildren))
225220
.method(hasSignature(runToken)).intercept(MethodDelegation.to(run))
226221
.method(named("describeChild")).intercept(MethodDelegation.to(describeChild))
227222
// NOTE: The 'methodBlock', 'createTest', and 'getTestRules' methods
@@ -300,7 +295,7 @@ public static AtomicTest getAtomicTestOf(Object target) {
300295
* objects)
301296
*/
302297
public static Object getParentOf(Object child) {
303-
return RunChildren.getParentOf(child);
298+
return Run.getParentOf(child);
304299
}
305300

306301
/**
@@ -320,7 +315,7 @@ public static Object getNotifierOf(final Object runner) {
320315
* @return active {@code ParentRunner} object (may be ({@code null})
321316
*/
322317
public static Object getThreadRunner() {
323-
return RunChildren.getThreadRunner();
318+
return Run.getThreadRunner();
324319
}
325320

326321
/**
@@ -354,8 +349,6 @@ public static Description describeChild(Object runner, Object child) {
354349
return invoke(runner, "describeChild", child);
355350
}
356351

357-
358-
359352
/**
360353
* Get the {@link ReflectiveCallable} object for the specified description.
361354
*
@@ -652,6 +645,5 @@ public T get(int index) {
652645
public int size() {
653646
return indexes.length;
654647
}
655-
656648
}
657649
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,20 @@
2020
* methodBlock} method.
2121
*/
2222
public class MethodBlock {
23-
private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> METHOD_DEPTH;
24-
private static final Function<Integer, DepthGauge> NEW_INSTANCE;
23+
private static final ThreadLocal<ConcurrentMap<String, DepthGauge>> METHOD_DEPTH;
24+
private static final Function<String, DepthGauge> NEW_INSTANCE;
2525
private static final Map<String, Statement> RUNNER_TO_STATEMENT = new ConcurrentHashMap<>();
2626

2727
static {
28-
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() {
28+
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<String, DepthGauge>>() {
2929
@Override
30-
protected ConcurrentMap<Integer, DepthGauge> initialValue() {
30+
protected ConcurrentMap<String, DepthGauge> initialValue() {
3131
return new ConcurrentHashMap<>();
3232
}
3333
};
34-
NEW_INSTANCE = new Function<Integer, DepthGauge>() {
34+
NEW_INSTANCE = new Function<String, DepthGauge>() {
3535
@Override
36-
public DepthGauge apply(Integer input) {
36+
public DepthGauge apply(String input) {
3737
return new DepthGauge();
3838
}
3939
};
@@ -56,14 +56,14 @@ public DepthGauge apply(Integer input) {
5656
public static Statement intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
5757
@Argument(0) final FrameworkMethod method) throws Exception {
5858

59-
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), runner.hashCode(), NEW_INSTANCE);
59+
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), toMapKey(runner), NEW_INSTANCE);
6060
depthGauge.increaseDepth();
6161

6262
Statement statement = (Statement) LifecycleHooks.callProxy(proxy);
6363

6464
// if at ground level
6565
if (0 == depthGauge.decreaseDepth()) {
66-
METHOD_DEPTH.remove();
66+
METHOD_DEPTH.get().remove(toMapKey(runner));
6767
try {
6868
// get parent of test runner
6969
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
@@ -78,7 +78,7 @@ public static Statement intercept(@This final Object runner, @SuperCall final Ca
7878
@Override
7979
public void evaluate() throws Throwable {
8080
// attach class runner to thread
81-
RunChildren.pushThreadRunner(threadRunner);
81+
Run.pushThreadRunner(threadRunner);
8282
}
8383
};
8484
}

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

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@
22

33
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
44

5+
import java.util.ArrayDeque;
6+
import java.util.Deque;
7+
import java.util.EmptyStackException;
8+
import java.util.List;
59
import java.util.Map;
10+
import java.util.Set;
611
import java.util.concurrent.Callable;
712
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.concurrent.ConcurrentMap;
14+
import java.util.concurrent.CopyOnWriteArraySet;
15+
16+
import org.junit.runner.Description;
17+
import org.junit.runner.notification.RunListener;
818
import org.junit.runner.notification.RunNotifier;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import com.google.common.base.Function;
23+
924
import net.bytebuddy.implementation.bind.annotation.Argument;
1025
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1126
import net.bytebuddy.implementation.bind.annotation.This;
@@ -14,7 +29,35 @@
1429
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
1530
*/
1631
public class Run {
32+
private static final ThreadLocal<Deque<Object>> RUNNER_STACK;
33+
private static final ThreadLocal<ConcurrentMap<String, DepthGauge>> METHOD_DEPTH;
34+
private static final Function<String, DepthGauge> NEW_INSTANCE;
35+
private static final Set<String> START_NOTIFIED = new CopyOnWriteArraySet<>();
36+
private static final Map<String, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
1737
private static final Map<String, RunNotifier> RUNNER_TO_NOTIFIER = new ConcurrentHashMap<>();
38+
private static final Set<String> NOTIFIERS = new CopyOnWriteArraySet<>();
39+
private static final Logger LOGGER = LoggerFactory.getLogger(Run.class);
40+
41+
static {
42+
RUNNER_STACK = new ThreadLocal<Deque<Object>>() {
43+
@Override
44+
protected Deque<Object> initialValue() {
45+
return new ArrayDeque<>();
46+
}
47+
};
48+
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<String, DepthGauge>>() {
49+
@Override
50+
protected ConcurrentMap<String, DepthGauge> initialValue() {
51+
return new ConcurrentHashMap<>();
52+
}
53+
};
54+
NEW_INSTANCE = new Function<String, DepthGauge>() {
55+
@Override
56+
public DepthGauge apply(String input) {
57+
return new DepthGauge();
58+
}
59+
};
60+
}
1861

1962
/**
2063
* Interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
@@ -27,13 +70,24 @@ public class Run {
2770
public static void intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
2871
@Argument(0) final RunNotifier notifier) throws Exception {
2972

73+
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), toMapKey(runner), NEW_INSTANCE);
74+
3075
try {
31-
RUNNER_TO_NOTIFIER.put(toMapKey(runner), notifier);
32-
RunChildren.pushThreadRunner(runner);
76+
if (0 == depthGauge.increaseDepth()) {
77+
RUNNER_TO_NOTIFIER.put(toMapKey(runner), notifier);
78+
pushThreadRunner(runner);
79+
attachRunListeners(runner, notifier);
80+
fireRunStarted(runner);
81+
}
82+
3383
LifecycleHooks.callProxy(proxy);
3484
} finally {
35-
RunChildren.popThreadRunner();
36-
RUNNER_TO_NOTIFIER.remove(toMapKey(runner));
85+
if (0 == depthGauge.decreaseDepth()) {
86+
METHOD_DEPTH.get().remove(toMapKey(runner));
87+
fireRunFinished(runner);
88+
popThreadRunner();
89+
RUNNER_TO_NOTIFIER.remove(toMapKey(runner));
90+
}
3791
}
3892
}
3993

@@ -46,4 +100,102 @@ public static void intercept(@This final Object runner, @SuperCall final Callabl
46100
static RunNotifier getNotifierOf(final Object runner) {
47101
return RUNNER_TO_NOTIFIER.get(toMapKey(runner));
48102
}
103+
104+
/**
105+
* Get the parent runner that owns specified child runner or framework method.
106+
*
107+
* @param child {@code ParentRunner} or {@code FrameworkMethod} object
108+
* @return {@code ParentRunner} object that owns the specified child ({@code null} for root objects)
109+
*/
110+
static Object getParentOf(final Object child) {
111+
return CHILD_TO_PARENT.get(toMapKey(child));
112+
}
113+
114+
/**
115+
* Push the specified JUnit test runner onto the stack for the current thread.
116+
*
117+
* @param runner JUnit test runner
118+
*/
119+
static void pushThreadRunner(final Object runner) {
120+
RUNNER_STACK.get().push(runner);
121+
}
122+
123+
/**
124+
* Pop the top JUnit test runner from the stack for the current thread.
125+
*
126+
* @return {@code ParentRunner} object
127+
* @throws EmptyStackException if called outside the scope of an active runner
128+
*/
129+
static Object popThreadRunner() {
130+
return RUNNER_STACK.get().pop();
131+
}
132+
133+
/**
134+
* Get the runner that owns the active thread context.
135+
*
136+
* @return active {@code ParentRunner} object
137+
*/
138+
static Object getThreadRunner() {
139+
return RUNNER_STACK.get().peek();
140+
}
141+
142+
/**
143+
* Fire the {@link RunnerWatcher#runStarted(Object)} event for the specified runner.
144+
* <p>
145+
* <b>NOTE</b>: If {@code runStarted} for the specified runner has already been fired, do nothing.
146+
* @param runner JUnit test runner
147+
* @return {@code true} if the {@code runStarted} event was fired; otherwise {@code false}
148+
*/
149+
static boolean fireRunStarted(Object runner) {
150+
if (START_NOTIFIED.add(toMapKey(runner))) {
151+
for (Object child : (List<?>) LifecycleHooks.invoke(runner, "getChildren")) {
152+
CHILD_TO_PARENT.put(toMapKey(child), runner);
153+
}
154+
155+
LOGGER.debug("runStarted: {}", runner);
156+
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
157+
watcher.runStarted(runner);
158+
}
159+
return true;
160+
}
161+
return false;
162+
}
163+
164+
/**
165+
* Fire the {@link RunnerWatcher#runFinished(Object)} event for the specified runner.
166+
*
167+
* @param runner JUnit test runner
168+
*/
169+
static void fireRunFinished(Object runner) {
170+
LOGGER.debug("runFinished: {}", runner);
171+
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
172+
watcher.runFinished(runner);
173+
}
174+
175+
START_NOTIFIED.remove(toMapKey(runner));
176+
for (Object child : (List<?>) LifecycleHooks.invoke(runner, "getChildren")) {
177+
CHILD_TO_PARENT.remove(toMapKey(child));
178+
}
179+
}
180+
181+
/**
182+
* Attach registered run listeners to the specified run notifier.
183+
* <p>
184+
* <b>NOTE</b>: If the specified run notifier has already been seen, do nothing.
185+
*
186+
* @param runner JUnit test runner
187+
* @param notifier JUnit {@link RunNotifier} object
188+
* @throws Exception if {@code run-started} notification
189+
*/
190+
static void attachRunListeners(Object runner, final RunNotifier notifier) throws Exception {
191+
if (NOTIFIERS.add(toMapKey(notifier))) {
192+
Description description = LifecycleHooks.invoke(runner, "getDescription");
193+
for (RunListener listener : LifecycleHooks.getRunListeners()) {
194+
// prevent potential duplicates
195+
notifier.removeListener(listener);
196+
notifier.addListener(listener);
197+
listener.testRunStarted(description);
198+
}
199+
}
200+
}
49201
}

0 commit comments

Comments
 (0)