22
33import 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 ;
59import java .util .Map ;
10+ import java .util .Set ;
611import java .util .concurrent .Callable ;
712import 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 ;
818import org .junit .runner .notification .RunNotifier ;
19+ import org .slf4j .Logger ;
20+ import org .slf4j .LoggerFactory ;
21+
22+ import com .google .common .base .Function ;
23+
924import net .bytebuddy .implementation .bind .annotation .Argument ;
1025import net .bytebuddy .implementation .bind .annotation .SuperCall ;
1126import net .bytebuddy .implementation .bind .annotation .This ;
1429 * This class declares the interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
1530 */
1631public 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