Skip to content

Commit 887995e

Browse files
authored
Merge pull request #10 from Nordstrom/pr/export-lifecycle-hierarchy
Major revisions to the implementation of lifecycle hooks to provide more usable functionality
2 parents 9ba1299 + 69ec630 commit 887995e

File tree

8 files changed

+314
-197
lines changed

8 files changed

+314
-197
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: 92 additions & 153 deletions
Large diffs are not rendered by default.

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
import java.util.Set;
1111
import java.util.concurrent.Callable;
1212

13-
import net.bytebuddy.implementation.bind.annotation.AllArguments;
13+
import org.junit.runners.model.FrameworkMethod;
14+
15+
import com.nordstrom.common.base.UncheckedThrow;
16+
1417
import net.bytebuddy.implementation.bind.annotation.BindingPriority;
1518
import net.bytebuddy.implementation.bind.annotation.Origin;
1619
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
@@ -41,84 +44,87 @@ private MethodInterceptor() {
4144
*
4245
* @param clazz "enhanced" class upon which the method was invoked
4346
* @param method {@link Method} object for the invoked method
44-
* @param args method invocation arguments
4547
* @param proxy call-able proxy for the intercepted method
46-
* @return {@code anything} (the result of invoking the intercepted method)
4748
* @throws Exception {@code anything} (exception thrown by the intercepted method)
4849
*/
4950
@RuntimeType
5051
@BindingPriority(1)
51-
public static Object intercept(@Origin Class<?> clazz, @Origin Method method, @AllArguments Object[] args,
52-
@SuperCall Callable<?> proxy) throws Exception
53-
{
54-
Object result;
52+
public static void intercept(@Origin Class<?> clazz, @Origin Method method, @SuperCall Callable<?> proxy)
53+
throws Exception {
5554
attachWatchers(clazz);
5655

56+
Throwable thrown = null;
57+
FrameworkMethod member = new FrameworkMethod(method);
5758
synchronized(methodWatchers2) {
5859
for (MethodWatcher2 watcher : methodWatchers2) {
59-
watcher.beforeInvocation(method, args);
60+
watcher.beforeInvocation(member);
6061
}
6162
}
6263

6364
try {
64-
result = proxy.call();
65+
proxy.call();
66+
} catch (Throwable t) {
67+
thrown = t;
6568
} finally {
6669
synchronized(methodWatchers2) {
6770
for (MethodWatcher2 watcher : methodWatchers2) {
68-
watcher.afterInvocation(method, args);
71+
watcher.afterInvocation(member, thrown);
6972
}
7073
}
7174
}
7275

73-
return result;
76+
if (thrown != null) {
77+
throw UncheckedThrow.throwUnchecked(thrown);
78+
}
7479
}
7580

7681
/**
7782
* This is the method that intercepts annotated methods in "enhanced" JUnit test class objects.
7883
*
7984
* @param obj "enhanced" object upon which the method was invoked
8085
* @param method {@link Method} object for the invoked method
81-
* @param args method invocation arguments
8286
* @param proxy call-able proxy for the intercepted method
83-
* @return {@code anything} (the result of invoking the intercepted method)
8487
* @throws Exception {@code anything} (exception thrown by the intercepted method)
8588
*/
8689
@RuntimeType
8790
@BindingPriority(2)
88-
public static Object intercept(@This Object obj, @Origin Method method, @AllArguments Object[] args,
89-
@SuperCall Callable<?> proxy) throws Exception
90-
{
91-
Object result;
92-
91+
public static void intercept(@This Object obj, @Origin Method method, @SuperCall Callable<?> proxy)
92+
throws Exception {
93+
Throwable thrown = null;
94+
FrameworkMethod member = new FrameworkMethod(method);
9395
synchronized(methodWatchers) {
9496
for (MethodWatcher watcher : methodWatchers) {
95-
watcher.beforeInvocation(obj, method, args);
97+
watcher.beforeInvocation(obj, member);
9698
}
9799
}
98100
synchronized(methodWatchers2) {
99101
for (MethodWatcher2 watcher : methodWatchers2) {
100-
watcher.beforeInvocation(obj, method, args);
102+
watcher.beforeInvocation(obj, member);
101103
}
102104
}
103105

104106
try {
105-
result = proxy.call();
107+
proxy.call();
108+
} catch (Throwable t) {
109+
thrown = t;
106110
} finally {
107111
synchronized(watchers) {
108112
synchronized(methodWatchers) {
109113
for (MethodWatcher watcher : methodWatchers) {
110-
watcher.afterInvocation(obj, method, args);
114+
watcher.afterInvocation(obj, member, thrown);
111115
}
112116
}
113117
synchronized(methodWatchers2) {
114118
for (MethodWatcher2 watcher : methodWatchers2) {
115-
watcher.afterInvocation(obj, method, args);
119+
watcher.afterInvocation(obj, member, thrown);
116120
}
117121
}
118122
}
119123
}
120124

121-
return result;
125+
if (thrown != null) {
126+
throw UncheckedThrow.throwUnchecked(thrown);
127+
}
122128
}
123129

124130
/**
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.nordstrom.automation.junit;
22

3-
import java.lang.reflect.Method;
3+
import org.junit.runners.model.FrameworkMethod;
44

55
/**
66
* This interface defines the methods implemented by JUnit method watchers. These watchers are attached to test classes
@@ -12,17 +12,16 @@ public interface MethodWatcher {
1212
* Invoked before each test or configuration method is invoked
1313
*
1414
* @param obj "enhanced" object upon which the method was invoked
15-
* @param method {@link Method} object for the invoked method
16-
* @param args method invocation arguments
15+
* @param method {@link FrameworkMethod} object for the invoked method
1716
*/
18-
void beforeInvocation(Object obj, Method method, Object[] args);
17+
void beforeInvocation(Object obj, FrameworkMethod method);
1918

2019
/**
2120
* Invoked after each test or configuration method is invoked
2221
*
2322
* @param obj "enhanced" object upon which the method was invoked
24-
* @param method {@link Method} object for the invoked method
25-
* @param args method invocation arguments
23+
* @param method {@link FrameworkMethod} object for the invoked method
24+
* @param thrown exception thrown by method; {@code null} on normal completion
2625
*/
27-
void afterInvocation(Object obj, Method method, Object[] args);
26+
void afterInvocation(Object obj, FrameworkMethod method, Throwable thrown);
2827
}
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.nordstrom.automation.junit;
22

3-
import java.lang.reflect.Method;
3+
import org.junit.runners.model.FrameworkMethod;
44

55
/**
66
* This interface defines the methods implemented by JUnit method watchers. These watcher are attached to test classes
@@ -11,16 +11,15 @@ public interface MethodWatcher2 extends MethodWatcher {
1111
/**
1212
* Invoked before each class-level configuration method is invoked
1313
*
14-
* @param method {@link Method} object for the invoked method
15-
* @param args method invocation arguments
14+
* @param method {@link FrameworkMethod} object for the invoked method
1615
*/
17-
void beforeInvocation(Method method, Object[] args);
16+
void beforeInvocation(FrameworkMethod method);
1817

1918
/**
2019
* Invoked after each class-level configuration method is invoked
2120
*
22-
* @param method {@link Method} object for the invoked method
23-
* @param args method invocation arguments
21+
* @param method {@link FrameworkMethod} object for the invoked method
22+
* @param thrown exception thrown by method; {@code null} on normal completion
2423
*/
25-
void afterInvocation(Method method, Object[] args);
24+
void afterInvocation(FrameworkMethod method, Throwable thrown);
2625
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static com.nordstrom.automation.junit.LifecycleHooks.invoke;
4+
5+
import java.util.ServiceLoader;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
import org.junit.internal.AssumptionViolatedException;
9+
import org.junit.internal.runners.model.EachTestNotifier;
10+
import org.junit.runner.Description;
11+
import org.junit.runner.notification.RunNotifier;
12+
import org.junit.runners.model.FrameworkMethod;
13+
import org.junit.runners.model.Statement;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
17+
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
18+
19+
/**
20+
* This class provided the utility methods used by the <b>JUnit Foundation</b> automatic retry feature.
21+
*/
22+
public class RetryHandler {
23+
24+
private static final ServiceLoader<JUnitRetryAnalyzer> retryAnalyzerLoader;
25+
private static final Logger LOGGER = LoggerFactory.getLogger(RetryHandler.class);
26+
27+
static {
28+
retryAnalyzerLoader = ServiceLoader.load(JUnitRetryAnalyzer.class);
29+
}
30+
31+
private RetryHandler() {
32+
throw new AssertionError("RetryHandler is a static utility class that cannot be instantiated");
33+
}
34+
35+
/**
36+
* Run the specified method, retrying on failure.
37+
*
38+
* @param runner underlying test runner
39+
* @param method test method to be run
40+
* @param notifier run notifier through which events are published
41+
* @param maxRetry maximum number of retry attempts
42+
*/
43+
static void runChildWithRetry(Object runner, final FrameworkMethod method, RunNotifier notifier, int maxRetry) {
44+
boolean doRetry = false;
45+
Statement statement = invoke(runner, "methodBlock", method);
46+
Description description = invoke(runner, "describeChild", method);
47+
AtomicInteger count = new AtomicInteger(maxRetry);
48+
49+
do {
50+
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
51+
52+
eachNotifier.fireTestStarted();
53+
try {
54+
statement.evaluate();
55+
doRetry = false;
56+
} catch (AssumptionViolatedException thrown) {
57+
doRetry = doRetry(runner, method, thrown, count);
58+
if (doRetry) {
59+
description = RetriedTest.proxyFor(description, thrown);
60+
eachNotifier.fireTestIgnored();
61+
} else {
62+
eachNotifier.addFailedAssumption(thrown);
63+
}
64+
} catch (Throwable thrown) {
65+
doRetry = doRetry(runner, method, thrown, count);
66+
if (doRetry) {
67+
description = RetriedTest.proxyFor(description, thrown);
68+
eachNotifier.fireTestIgnored();
69+
} else {
70+
eachNotifier.addFailure(thrown);
71+
}
72+
} finally {
73+
eachNotifier.fireTestFinished();
74+
}
75+
} while (doRetry);
76+
}
77+
78+
/**
79+
* Determine if the indicated failure should be retried.
80+
*
81+
* @param method failed test method
82+
* @param thrown exception for this failed test
83+
* @param retryCounter retry counter (remaining attempts)
84+
* @return {@code true} if failed test should be retried; otherwise {@code false}
85+
*/
86+
static boolean doRetry(Object runner, FrameworkMethod method, Throwable thrown, AtomicInteger retryCounter) {
87+
boolean doRetry = false;
88+
if ((retryCounter.decrementAndGet() > -1) && isRetriable(method, thrown)) {
89+
LOGGER.warn("### RETRY ### {}", method);
90+
doRetry = true;
91+
}
92+
return doRetry;
93+
}
94+
95+
/**
96+
* Get the configured maximum retry count for failed tests ({@link JUnitSetting#MAX_RETRY MAX_RETRY}).
97+
* <p>
98+
* <b>NOTE</b>: If the specified method or the class that declares it are marked with the {@code @NoRetry}
99+
* annotation, this method returns zero (0).
100+
*
101+
* @param method test method for which retry is being considered
102+
* @return maximum retry attempts that will be made if the specified method fails
103+
*/
104+
static int getMaxRetry(Object runner, final FrameworkMethod method) {
105+
int maxRetry = 0;
106+
107+
// determine if retry is disabled for this method
108+
NoRetry noRetryOnMethod = method.getAnnotation(NoRetry.class);
109+
// determine if retry is disabled for the class that declares this method
110+
NoRetry noRetryOnClass = method.getDeclaringClass().getAnnotation(NoRetry.class);
111+
112+
// if method isn't ignored or excluded from retry attempts
113+
if (Boolean.FALSE.equals(invoke(runner, "isIgnored", method)) && (noRetryOnMethod == null) && (noRetryOnClass == null)) {
114+
// get configured maximum retry count
115+
maxRetry = JUnitConfig.getConfig().getInteger(JUnitSettings.MAX_RETRY.key(), Integer.valueOf(0));
116+
}
117+
118+
return maxRetry;
119+
}
120+
121+
/**
122+
* Determine if the specified failed test should be retried.
123+
*
124+
* @param method failed test method
125+
* @param thrown exception for this failed test
126+
* @return {@code true} if test should be retried; otherwise {@code false}
127+
*/
128+
static boolean isRetriable(final FrameworkMethod method, final Throwable thrown) {
129+
for (JUnitRetryAnalyzer analyzer : retryAnalyzerLoader) {
130+
if (analyzer.retry(method, thrown)) {
131+
return true;
132+
}
133+
}
134+
return false;
135+
}
136+
137+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.util.concurrent.Callable;
4+
5+
import org.junit.runner.notification.RunNotifier;
6+
import org.junit.runners.model.FrameworkMethod;
7+
8+
import net.bytebuddy.implementation.bind.annotation.Argument;
9+
import net.bytebuddy.implementation.bind.annotation.SuperCall;
10+
import net.bytebuddy.implementation.bind.annotation.This;
11+
12+
/**
13+
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#runChild runChild}
14+
* method.
15+
*/
16+
@SuppressWarnings("squid:S1118")
17+
public class RunChild {
18+
19+
/**
20+
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#runChild runChild} method.
21+
*
22+
* @param runner underlying test runner
23+
* @param proxy callable proxy for the intercepted method
24+
* @param method test method to be run
25+
* @param notifier run notifier through which events are published
26+
* @throws Exception if something goes wrong
27+
*/
28+
public static void intercept(@This Object runner, @SuperCall Callable<?> proxy, @Argument(0) FrameworkMethod method, @Argument(1) RunNotifier notifier) throws Exception {
29+
int count = RetryHandler.getMaxRetry(runner, method);
30+
31+
if (count > 0) {
32+
RetryHandler.runChildWithRetry(runner, method, notifier, count);
33+
} else {
34+
proxy.call();
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)