Skip to content

Commit 324b20e

Browse files
authored
Fix #59: Consolidate rule-based and parameter-based timeout management (#60)
1 parent 803d5d9 commit 324b20e

File tree

9 files changed

+1026
-765
lines changed

9 files changed

+1026
-765
lines changed

README.md

Lines changed: 667 additions & 656 deletions
Large diffs are not rendered by default.

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
import java.util.ServiceLoader;
55
import java.util.concurrent.Callable;
66
import java.util.concurrent.ConcurrentHashMap;
7-
87
import org.slf4j.Logger;
98
import org.slf4j.LoggerFactory;
109

1110
import com.google.common.base.Optional;
12-
1311
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
1412
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1513
import net.bytebuddy.implementation.bind.annotation.This;
@@ -49,27 +47,29 @@ protected DepthGauge initialValue() {
4947
public static Object intercept(@This final Object runner,
5048
@SuperCall final Callable<?> proxy) throws Exception {
5149

52-
Object testObj;
50+
Object target;
5351
try {
5452
DEPTH.get().increaseDepth();
55-
testObj = LifecycleHooks.callProxy(proxy);
53+
target = LifecycleHooks.callProxy(proxy);
5654
} finally {
5755
DEPTH.get().decreaseDepth();
5856
}
5957

60-
TARGET_TO_RUNNER.put(testObj, runner);
61-
RUNNER_TO_TARGET.put(runner, testObj);
58+
TARGET_TO_RUNNER.put(target, runner);
59+
RUNNER_TO_TARGET.put(runner, target);
6260

6361
if (DEPTH.get().atGroundLevel()) {
64-
LOGGER.debug("testObjectCreated: {}", testObj);
62+
LOGGER.debug("testObjectCreated: {}", target);
6563
synchronized(objectWatcherLoader) {
6664
for (TestObjectWatcher watcher : objectWatcherLoader) {
67-
watcher.testObjectCreated(testObj, runner);
65+
watcher.testObjectCreated(target, runner);
6866
}
6967
}
7068
}
7169

72-
return testObj;
70+
// apply parameter-based global timeout
71+
TimeoutUtils.applyTestTimeout(runner, target);
72+
return target;
7373
}
7474

7575
/**
@@ -112,4 +112,5 @@ static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> watcherT
112112
}
113113
return Optional.absent();
114114
}
115+
115116
}

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

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@
22

33
import java.util.List;
44
import java.util.concurrent.Callable;
5-
import java.util.concurrent.TimeUnit;
65

7-
import org.junit.Test;
86
import org.junit.rules.TestRule;
9-
import org.junit.rules.Timeout;
10-
import org.junit.runners.model.FrameworkMethod;
11-
12-
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
137

148
import net.bytebuddy.implementation.bind.annotation.Argument;
159
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
@@ -21,82 +15,19 @@ public class GetTestRules {
2115
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#getTestRules(Object)} getTestRules} method.
2216
*
2317
* @param runner target {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} object
24-
* @param proxy callable proxy for the intercepted method
25-
* @param target the test case instance
18+
* @param proxy callable proxy for the intercepted method
19+
* @param target test class instance
2620
* @return {@code anything} - JUnit test class instance
2721
* @throws Exception {@code anything} (exception thrown by the intercepted method)
2822
*/
2923
@RuntimeType
3024
public static List<TestRule> intercept(@This final Object runner, @SuperCall final Callable<?> proxy, @Argument(0) final Object target) throws Exception {
31-
// get atomic test object for target class runner
32-
AtomicTest<FrameworkMethod> atomicTest = LifecycleHooks.getAtomicTestOf(runner);
33-
// get "identity" method of atomic test
34-
FrameworkMethod identity = atomicTest.getIdentity();
35-
// get "identity" method Test annotation
36-
Test annotation = identity.getAnnotation(Test.class);
37-
// get test method timeout interval
38-
long testTimeout = annotation.timeout();
39-
// initialize longest interval
40-
long longestTimeout = testTimeout;
41-
42-
long defaultTimeout = -1;
43-
// if default timeout rule interval is defined
44-
if (LifecycleHooks.getConfig().containsKey(JUnitSettings.TIMEOUT_RULE.key())) {
45-
// get default timeout rule interval
46-
defaultTimeout = LifecycleHooks.getConfig().getLong(JUnitSettings.TIMEOUT_RULE.key());
47-
48-
// if default rule timeout is longest
49-
if (defaultTimeout > longestTimeout) {
50-
// update longest interval
51-
longestTimeout = defaultTimeout;
52-
}
53-
}
54-
5525
@SuppressWarnings("unchecked")
5626
// get list of test rules for target class runner
5727
List<TestRule> testRules = (List<TestRule>) LifecycleHooks.callProxy(proxy);
58-
59-
int ruleIndex = -1;
60-
long ruleTimeout = -1;
61-
62-
// iterate over active test rules collection
63-
for (int i = 0; i < testRules.size(); i++) {
64-
// get current test rule
65-
TestRule testRule = testRules.get(i);
66-
// if this is a Timeout rule
67-
if (testRule instanceof Timeout) {
68-
// save index
69-
ruleIndex = i;
70-
// extract Timeout rule interval
71-
ruleTimeout = LifecycleHooks.invoke(testRule, "getTimeout", TimeUnit.MILLISECONDS);
72-
break;
73-
}
74-
}
75-
76-
// if Timeout found
77-
if (ruleIndex != -1) {
78-
// if longest interval exceeds rule
79-
if (longestTimeout > ruleTimeout) {
80-
// replace existing rule with Timeout of longest interval
81-
testRules.set(ruleIndex, Timeout.millis(longestTimeout));
82-
} else {
83-
// update longest interval
84-
longestTimeout = ruleTimeout;
85-
}
86-
// otherwise, if Timeout specified
87-
} else if (defaultTimeout != -1) {
88-
// update rule index
89-
ruleIndex = testRules.size();
90-
// add Timeout of longest interval
91-
testRules.add(Timeout.millis(longestTimeout));
92-
}
93-
94-
// if Timeout rule interval matches test method timeout
95-
if ((ruleIndex != -1) && (longestTimeout == testTimeout)) {
96-
// disable test method timeout
97-
MutableTest.proxyFor(identity.getMethod()).setTimeout(0L);
98-
}
99-
28+
// apply rule-based global timeout
29+
TimeoutUtils.applyRuleTimeout(runner, testRules);
30+
// return test rules
10031
return testRules;
10132
}
10233
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.*;
4+
5+
import java.lang.reflect.Constructor;
6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.InvocationTargetException;
8+
import org.junit.Rule;
9+
import org.junit.rules.Timeout;
10+
import org.junit.runners.model.FrameworkField;
11+
12+
import com.nordstrom.common.base.UncheckedThrow;
13+
14+
import net.bytebuddy.ByteBuddy;
15+
import net.bytebuddy.description.modifier.Visibility;
16+
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
17+
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
18+
import net.bytebuddy.implementation.FieldAccessor;
19+
import net.bytebuddy.implementation.MethodCall;
20+
21+
public class PhantomTimeout {
22+
23+
private static final Class<? extends FrameworkField> TYPE_PHANTOM;
24+
25+
static Constructor<FrameworkField> ctor;
26+
static Field field;
27+
28+
@Rule
29+
public Timeout timeoutRule;
30+
31+
static {
32+
try {
33+
ctor = FrameworkField.class.getDeclaredConstructor(Field.class);
34+
field = PhantomTimeout.class.getDeclaredField("timeoutRule");
35+
} catch (NoSuchMethodException | SecurityException | NoSuchFieldException e) {
36+
throw UncheckedThrow.throwUnchecked(e);
37+
}
38+
39+
TYPE_PHANTOM = new ByteBuddy()
40+
.subclass(FrameworkField.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
41+
.name("org.junit.runners.model.PhantomTimeout")
42+
.defineField("timeoutRule", Timeout.class, Visibility.PRIVATE)
43+
.defineConstructor(Visibility.PUBLIC)
44+
.withParameters(Timeout.class)
45+
.intercept(MethodCall.invoke(ctor).with(field)
46+
.andThen(FieldAccessor.ofField("timeoutRule").setsArgumentAt(0)))
47+
.method(named("get")).intercept(FieldAccessor.ofField("timeoutRule"))
48+
.make()
49+
.load(FrameworkField.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
50+
.getLoaded();
51+
}
52+
53+
public static FrameworkField create(Timeout timeoutRule) {
54+
try {
55+
return TYPE_PHANTOM.getConstructor(Timeout.class).newInstance(timeoutRule);
56+
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
57+
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
58+
throw UncheckedThrow.throwUnchecked(e);
59+
}
60+
}
61+
}

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

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@
44
import java.util.Map;
55
import java.util.concurrent.Callable;
66
import org.junit.Ignore;
7-
import org.junit.Test;
87
import org.junit.runner.notification.RunNotifier;
98
import org.junit.runners.model.FrameworkMethod;
10-
11-
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
12-
139
import net.bytebuddy.implementation.bind.annotation.Argument;
1410
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1511
import net.bytebuddy.implementation.bind.annotation.This;
@@ -54,7 +50,6 @@ public static void intercept(@This final Object runner, @SuperCall final Callabl
5450
if (child instanceof FrameworkMethod) {
5551
FrameworkMethod method = (FrameworkMethod) child;
5652

57-
applyTimeout(method);
5853
int count = RetryHandler.getMaxRetry(runner, method);
5954
boolean isIgnored = (null != method.getAnnotation(Ignore.class));
6055

@@ -85,25 +80,4 @@ static void finished() {
8580
}
8681
}
8782
}
88-
89-
/**
90-
* If configured for default test timeout, apply the timeout value to the specified framework method if it doesn't
91-
* already specify a longer timeout interval.
92-
*
93-
* @param method {@link FrameworkMethod} object
94-
*/
95-
private static void applyTimeout(FrameworkMethod method) {
96-
// if default test timeout is defined
97-
if (LifecycleHooks.getConfig().containsKey(JUnitSettings.TEST_TIMEOUT.key())) {
98-
// get default test timeout
99-
long defaultTimeout = LifecycleHooks.getConfig().getLong(JUnitSettings.TEST_TIMEOUT.key());
100-
// get @Test annotation
101-
Test annotation = method.getAnnotation(Test.class);
102-
// if annotation declared and current timeout is less than default
103-
if ((annotation != null) && (annotation.timeout() < defaultTimeout)) {
104-
// set test timeout interval
105-
MutableTest.proxyFor(method.getMethod()).setTimeout(defaultTimeout);
106-
}
107-
}
108-
}
10983
}

0 commit comments

Comments
 (0)