Skip to content

Commit f036d1f

Browse files
committed
OMG! It finally does something useful!
1 parent 9ba1299 commit f036d1f

File tree

8 files changed

+248
-189
lines changed

8 files changed

+248
-189
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>com.nordstrom.tools</groupId>
44
<artifactId>junit-foundation</artifactId>
5-
<version>4.0.1-SNAPSHOT</version>
5+
<version>5.0.0-SNAPSHOT</version>
66
<packaging>jar</packaging>
77

88
<name>JUnit Foundation</name>

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

Lines changed: 53 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,19 @@
77
import java.lang.reflect.InvocationTargetException;
88
import java.lang.reflect.Method;
99
import java.util.HashMap;
10+
import java.util.HashSet;
1011
import java.util.Map;
1112
import java.util.ServiceLoader;
13+
import java.util.Set;
1214
import java.util.concurrent.Callable;
1315
import java.util.concurrent.ConcurrentHashMap;
14-
import java.util.concurrent.atomic.AtomicInteger;
15-
1616
import org.junit.After;
1717
import org.junit.Before;
1818
import org.junit.Test;
19-
import org.junit.internal.AssumptionViolatedException;
20-
import org.junit.internal.runners.model.EachTestNotifier;
2119
import org.junit.runner.Description;
20+
import org.junit.runner.notification.RunListener;
2221
import org.junit.runner.notification.RunNotifier;
23-
import org.junit.runners.model.FrameworkMethod;
24-
import org.junit.runners.model.Statement;
2522
import org.junit.runners.model.TestClass;
26-
import org.slf4j.Logger;
27-
import org.slf4j.LoggerFactory;
28-
2923
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
3024
import com.nordstrom.common.base.UncheckedThrow;
3125
import com.nordstrom.common.file.PathUtils.ReportsDirectory;
@@ -34,6 +28,7 @@
3428
import net.bytebuddy.agent.builder.AgentBuilder;
3529
import net.bytebuddy.description.type.TypeDescription;
3630
import net.bytebuddy.implementation.MethodDelegation;
31+
import net.bytebuddy.implementation.attribute.AnnotationRetention;
3732
import net.bytebuddy.implementation.bind.annotation.Argument;
3833
import net.bytebuddy.implementation.bind.annotation.SuperCall;
3934
import 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

Comments
 (0)