77import java .lang .reflect .InvocationTargetException ;
88import java .lang .reflect .Method ;
99import java .util .HashMap ;
10+ import java .util .HashSet ;
1011import java .util .Map ;
1112import java .util .ServiceLoader ;
13+ import java .util .Set ;
1214import java .util .concurrent .Callable ;
1315import java .util .concurrent .ConcurrentHashMap ;
14- import java .util .concurrent .atomic .AtomicInteger ;
15-
1616import org .junit .After ;
1717import org .junit .Before ;
1818import org .junit .Test ;
19- import org .junit .internal .AssumptionViolatedException ;
20- import org .junit .internal .runners .model .EachTestNotifier ;
2119import org .junit .runner .Description ;
20+ import org .junit .runner .notification .RunListener ;
2221import org .junit .runner .notification .RunNotifier ;
23- import org .junit .runners .model .FrameworkMethod ;
24- import org .junit .runners .model .Statement ;
2522import org .junit .runners .model .TestClass ;
26- import org .slf4j .Logger ;
27- import org .slf4j .LoggerFactory ;
28-
2923import com .nordstrom .automation .junit .JUnitConfig .JUnitSettings ;
3024import com .nordstrom .common .base .UncheckedThrow ;
3125import com .nordstrom .common .file .PathUtils .ReportsDirectory ;
3428import net .bytebuddy .agent .builder .AgentBuilder ;
3529import net .bytebuddy .description .type .TypeDescription ;
3630import net .bytebuddy .implementation .MethodDelegation ;
31+ import net .bytebuddy .implementation .attribute .AnnotationRetention ;
3732import net .bytebuddy .implementation .bind .annotation .Argument ;
3833import net .bytebuddy .implementation .bind .annotation .SuperCall ;
3934import net .bytebuddy .implementation .bind .annotation .This ;
@@ -47,11 +42,6 @@ public class LifecycleHooks {
4742 private static Map <Class <?>, Class <?>> proxyMap = new HashMap <>();
4843 private static JUnitConfig config ;
4944
50- private static final ServiceLoader <JUnitRetryAnalyzer > retryAnalyzerLoader ;
51- private static final ServiceLoader <TestClassWatcher > classWatcherLoader ;
52- private static final ServiceLoader <TestObjectWatcher > objectWatcherLoader ;
53- private static final Logger LOGGER = LoggerFactory .getLogger (LifecycleHooks .class );
54-
5545 private LifecycleHooks () {
5646 throw new AssertionError ("LifecycleHooks is a static utility class that cannot be instantiated" );
5747 }
@@ -61,10 +51,6 @@ private LifecycleHooks() {
6151 * and BlockJUnit4ClassRunner classes to enable the core functionality of JUnit Foundation.
6252 */
6353 static {
64- retryAnalyzerLoader = ServiceLoader .load (JUnitRetryAnalyzer .class );
65- classWatcherLoader = ServiceLoader .load (TestClassWatcher .class );
66- objectWatcherLoader = ServiceLoader .load (TestObjectWatcher .class );
67-
6854 for (ShutdownListener listener : ServiceLoader .load (ShutdownListener .class )) {
6955 Runtime .getRuntime ().addShutdownHook (getShutdownHook (listener ));
7056 }
@@ -79,11 +65,12 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
7965 TypeDescription type2 = TypePool .Default .ofClassPath ().describe ("org.junit.runners.BlockJUnit4ClassRunner" ).resolve ();
8066
8167 return new AgentBuilder .Default ()
82- .type (is (type1 ))
68+ .type (isSubTypeOf (type1 ))
8369 .transform ((builder , type , classLoader , module ) ->
8470 builder .method (named ("createTestClass" )).intercept (MethodDelegation .to (CreateTestClass .class ))
71+ .method (named ("run" )).intercept (MethodDelegation .to (Run .class ))
8572 .implement (Hooked .class ))
86- .type (is (type2 ))
73+ .type (isSubTypeOf (type2 ))
8774 .transform ((builder , type , classLoader , module ) ->
8875 builder .method (named ("createTest" )).intercept (MethodDelegation .to (CreateTest .class ))
8976 .method (named ("runChild" )).intercept (MethodDelegation .to (RunChild .class ))
@@ -111,7 +98,7 @@ public void run() {
11198 *
11299 * @return JUnit Foundation configuration object
113100 */
114- private static synchronized JUnitConfig getConfig () {
101+ static synchronized JUnitConfig getConfig () {
115102 if (config == null ) {
116103 config = JUnitConfig .getConfig ();
117104 }
@@ -120,7 +107,12 @@ private static synchronized JUnitConfig getConfig() {
120107
121108 @ SuppressWarnings ("squid:S1118" )
122109 public static class CreateTestClass {
123- private static final Map <TestClass , Object > CLASS_TO_RUNNER = new ConcurrentHashMap <>();
110+ static final ServiceLoader <TestClassWatcher > classWatcherLoader ;
111+ static final Map <TestClass , Object > CLASS_TO_RUNNER = new ConcurrentHashMap <>();
112+
113+ static {
114+ classWatcherLoader = ServiceLoader .load (TestClassWatcher .class );
115+ }
124116
125117 public static TestClass intercept (@ This Object runner , @ SuperCall Callable <?> proxy ) throws Exception {
126118 TestClass testClass = (TestClass ) proxy .call ();
@@ -134,14 +126,40 @@ public static TestClass intercept(@This Object runner, @SuperCall Callable<?> pr
134126 }
135127 }
136128
129+ @ SuppressWarnings ("squid:S1118" )
130+ public static class Run {
131+ static final ServiceLoader <RunListener > runListenerLoader ;
132+ private static final Set <RunNotifier > NOTIFIERS = new HashSet <>();
133+
134+ static {
135+ runListenerLoader = ServiceLoader .load (RunListener .class );
136+ }
137+
138+ public static void intercept (@ This Object runner , @ SuperCall Callable <?> proxy , @ Argument (0 ) RunNotifier notifier ) throws Exception {
139+ if (NOTIFIERS .add (notifier )) {
140+ Description description = invoke (runner , "getDescription" );
141+ for (RunListener listener : runListenerLoader ) {
142+ notifier .addListener (listener );
143+ listener .testRunStarted (description );
144+ }
145+ }
146+ proxy .call ();
147+ }
148+ }
149+
137150 /**
138151 * This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest createTest}
139152 * method.
140153 */
141154 @ SuppressWarnings ("squid:S1118" )
142155 public static class CreateTest {
143156
144- private static final Map <Object , TestClass > INSTANCE_TO_CLASS = new ConcurrentHashMap <>();
157+ static final ServiceLoader <TestObjectWatcher > objectWatcherLoader ;
158+ static final Map <Object , TestClass > INSTANCE_TO_CLASS = new ConcurrentHashMap <>();
159+
160+ static {
161+ objectWatcherLoader = ServiceLoader .load (TestObjectWatcher .class );
162+ }
145163
146164 /**
147165 * Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest createTest} method.
@@ -152,9 +170,9 @@ public static class CreateTest {
152170 * @throws Exception if something goes wrong
153171 */
154172 public static Object intercept (@ This Object runner , @ SuperCall Callable <?> proxy ) throws Exception {
155- Object testObj = installHooks (proxy .call ());
156- INSTANCE_TO_CLASS .put (testObj , invoke (runner , "getTestClass" ));
157- applyTimeout (testObj );
173+ Object testObj = LifecycleHooks . installHooks (proxy .call ());
174+ INSTANCE_TO_CLASS .put (testObj , LifecycleHooks . invoke (runner , "getTestClass" ));
175+ LifecycleHooks . applyTimeout (testObj );
158176
159177 for (TestObjectWatcher watcher : objectWatcherLoader ) {
160178 watcher .testObjectCreated (testObj , INSTANCE_TO_CLASS .get (testObj ));
@@ -164,33 +182,6 @@ public static Object intercept(@This Object runner, @SuperCall Callable<?> proxy
164182 }
165183 }
166184
167- /**
168- * This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#runChild runChild}
169- * method.
170- */
171- @ SuppressWarnings ("squid:S1118" )
172- public static class RunChild {
173-
174- /**
175- * Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#runChild runChild} method.
176- *
177- * @param runner underlying test runner
178- * @param proxy callable proxy for the intercepted method
179- * @param method test method to be run
180- * @param notifier run notifier through which events are published
181- * @throws Exception if something goes wrong
182- */
183- public static void intercept (@ This Object runner , @ SuperCall Callable <?> proxy , @ Argument (0 ) final FrameworkMethod method , @ Argument (1 ) RunNotifier notifier ) throws Exception {
184- int count = getMaxRetry (runner , method );
185-
186- if (count > 0 ) {
187- runChildWithRetry (runner , method , notifier , count );
188- } else {
189- proxy .call ();
190- }
191- }
192- }
193-
194185 /**
195186 * Get the test class object that wraps the specified instance.
196187 *
@@ -219,6 +210,14 @@ public static Object getRunnerFor(TestClass testClass) {
219210 throw new IllegalStateException ("No associated runner was for for specified test class" );
220211 }
221212
213+ public static Description getDescription (Object instance , Object target ) {
214+ TestClass testClass = getTestClassFor (instance );
215+ Object runner = getRunnerFor (testClass );
216+
217+
218+ return invoke (runner , "getDescription" , target );
219+ }
220+
222221 /**
223222 * If configured for default test timeout, apply this value to every test that doesn't already specify a longer
224223 * timeout interval.
@@ -243,108 +242,6 @@ static void applyTimeout(Object testObj) {
243242 }
244243 }
245244
246- /**
247- * Run the specified method, retrying on failure.
248- *
249- * @param runner underlying test runner
250- * @param method test method to be run
251- * @param notifier run notifier through which events are published
252- * @param maxRetry maximum number of retry attempts
253- */
254- static void runChildWithRetry (Object runner , final FrameworkMethod method , RunNotifier notifier , int maxRetry ) {
255- boolean doRetry = false ;
256- Statement statement = invoke (runner , "methodBlock" , method );
257- Description description = invoke (runner , "describeChild" , method );
258- AtomicInteger count = new AtomicInteger (maxRetry );
259-
260- do {
261- EachTestNotifier eachNotifier = new EachTestNotifier (notifier , description );
262-
263- eachNotifier .fireTestStarted ();
264- try {
265- statement .evaluate ();
266- doRetry = false ;
267- } catch (AssumptionViolatedException thrown ) {
268- doRetry = doRetry (runner , method , thrown , count );
269- if (doRetry ) {
270- description = RetriedTest .proxyFor (description , thrown );
271- eachNotifier .fireTestIgnored ();
272- } else {
273- eachNotifier .addFailedAssumption (thrown );
274- }
275- } catch (Throwable thrown ) {
276- doRetry = doRetry (runner , method , thrown , count );
277- if (doRetry ) {
278- description = RetriedTest .proxyFor (description , thrown );
279- eachNotifier .fireTestIgnored ();
280- } else {
281- eachNotifier .addFailure (thrown );
282- }
283- } finally {
284- eachNotifier .fireTestFinished ();
285- }
286- } while (doRetry );
287- }
288-
289- /**
290- * Determine if the indicated failure should be retried.
291- *
292- * @param method failed test method
293- * @param thrown exception for this failed test
294- * @param retryCounter retry counter (remaining attempts)
295- * @return {@code true} if failed test should be retried; otherwise {@code false}
296- */
297- static boolean doRetry (Object runner , FrameworkMethod method , Throwable thrown , AtomicInteger retryCounter ) {
298- boolean doRetry = false ;
299- if ((retryCounter .decrementAndGet () > -1 ) && isRetriable (method , thrown )) {
300- LOGGER .warn ("### RETRY ### {}" , method );
301- doRetry = true ;
302- }
303- return doRetry ;
304- }
305-
306- /**
307- * Get the configured maximum retry count for failed tests ({@link JUnitSetting#MAX_RETRY MAX_RETRY}).
308- * <p>
309- * <b>NOTE</b>: If the specified method or the class that declares it are marked with the {@code @NoRetry}
310- * annotation, this method returns zero (0).
311- *
312- * @param method test method for which retry is being considered
313- * @return maximum retry attempts that will be made if the specified method fails
314- */
315- static int getMaxRetry (Object runner , final FrameworkMethod method ) {
316- int maxRetry = 0 ;
317-
318- // determine if retry is disabled for this method
319- NoRetry noRetryOnMethod = method .getAnnotation (NoRetry .class );
320- // determine if retry is disabled for the class that declares this method
321- NoRetry noRetryOnClass = method .getDeclaringClass ().getAnnotation (NoRetry .class );
322-
323- // if method isn't ignored or excluded from retry attempts
324- if (Boolean .FALSE .equals (invoke (runner , "isIgnored" , method )) && (noRetryOnMethod == null ) && (noRetryOnClass == null )) {
325- // get configured maximum retry count
326- maxRetry = getConfig ().getInteger (JUnitSettings .MAX_RETRY .key (), Integer .valueOf (0 ));
327- }
328-
329- return maxRetry ;
330- }
331-
332- /**
333- * Determine if the specified failed test should be retried.
334- *
335- * @param method failed test method
336- * @param thrown exception for this failed test
337- * @return {@code true} if test should be retried; otherwise {@code false}
338- */
339- static boolean isRetriable (final FrameworkMethod method , final Throwable thrown ) {
340- for (JUnitRetryAnalyzer analyzer : retryAnalyzerLoader ) {
341- if (analyzer .retry (method , thrown )) {
342- return true ;
343- }
344- }
345- return false ;
346- }
347-
348245 /**
349246 * Create an enhanced instance of the specified test class object.
350247 *
@@ -364,6 +261,7 @@ static synchronized Object installHooks(Object testObj) {
364261 if (proxyType == null ) {
365262 try {
366263 proxyType = new ByteBuddy ()
264+ .with (AnnotationRetention .ENABLED )
367265 .subclass (testClass )
368266 .name (getSubclassName (testObj ))
369267 .method (isAnnotatedWith (anyOf (Test .class , Before .class , After .class )))
0 commit comments