Skip to content

Commit d72e8d7

Browse files
authored
Implement global Timeout rule management (#58)
1 parent 9f99b6d commit d72e8d7

File tree

10 files changed

+332
-7
lines changed

10 files changed

+332
-7
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.util.List;
4+
import java.util.concurrent.Callable;
5+
import java.util.concurrent.TimeUnit;
6+
7+
import org.junit.Test;
8+
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;
13+
14+
import net.bytebuddy.implementation.bind.annotation.Argument;
15+
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
16+
import net.bytebuddy.implementation.bind.annotation.SuperCall;
17+
import net.bytebuddy.implementation.bind.annotation.This;
18+
19+
public class GetTestRules {
20+
/**
21+
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#getTestRules(Object)} getTestRules} method.
22+
*
23+
* @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
26+
* @return {@code anything} - JUnit test class instance
27+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
28+
*/
29+
@RuntimeType
30+
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+
55+
@SuppressWarnings("unchecked")
56+
// get list of test rules for target class runner
57+
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+
100+
return testRules;
101+
}
102+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public enum JUnitSettings implements SettingsCore.SettingsAPI {
2828
RULE_CHAIN_LIST("junit.rule.chain.list", "rulesStartingWithInnerMost"),
2929
/** name: <b>junit.timeout.test</b> <br> default: {@code null} */
3030
TEST_TIMEOUT("junit.timeout.test", null),
31+
/** name: <b>junit.timeout.rule</b> <br> default: {@code null} */
32+
TIMEOUT_RULE("junit.timeout.rule", null),
3133
/** name: <b>junit.max.retry</b> <br> default: <b>0</b> */
3234
MAX_RETRY("junit.max.retry", "0");
3335

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
7474
final TypeDescription createTest = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.CreateTest").resolve();
7575
final TypeDescription runChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChild").resolve();
7676
final TypeDescription run = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Run").resolve();
77+
final TypeDescription getTestRules = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.GetTestRules").resolve();
7778

7879
final TypeDescription runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
7980
final SignatureToken runToken = new SignatureToken("run", TypeDescription.VOID, Arrays.asList(runNotifier));
@@ -105,6 +106,7 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
105106
return builder.method(named("createTest")).intercept(MethodDelegation.to(createTest))
106107
.method(named("runChild")).intercept(MethodDelegation.to(runChild))
107108
.method(hasSignature(runToken)).intercept(MethodDelegation.to(run))
109+
.method(named("getTestRules")).intercept(MethodDelegation.to(getTestRules))
108110
.implement(Hooked.class);
109111
}
110112
})

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public long timeout() {
7979
* @return this mutable annotation object
8080
*/
8181
public MutableTest setTimeout(long timeout) {
82-
TestTimedOutException foo;
8382
this.timeout = timeout;
8483
return this;
8584
}

src/test/java/com/nordstrom/automation/junit/MethodTimeoutPassing.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,8 @@
66

77
import org.junit.Test;
88

9-
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
10-
119
public class MethodTimeoutPassing {
1210

13-
static {
14-
System.setProperty(JUnitSettings.TEST_TIMEOUT.key(), "500");
15-
}
16-
1711
@Test
1812
public void testPassed() {
1913
System.out.println("testPassed");
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.concurrent.TimeUnit;
6+
7+
import org.junit.Test;
8+
9+
public class RuleExpectedTimeout {
10+
11+
@Test
12+
public void testRuleTimeout() throws InterruptedException {
13+
System.out.println("testRuleTimeout");
14+
TimeUnit.MILLISECONDS.sleep(1000);
15+
assertTrue(true);
16+
}
17+
18+
@Test(timeout = 400)
19+
public void testRuleTimeoutWithShorterInterval() throws InterruptedException {
20+
System.out.println("testRuleTimeoutWithShorterInterval");
21+
TimeUnit.MILLISECONDS.sleep(1000);
22+
assertTrue(true);
23+
}
24+
25+
@Test(timeout = 600)
26+
public void testRuleTimeoutWithLongerInterval() throws InterruptedException {
27+
System.out.println("testRuleTimeoutWithLongerInterval");
28+
TimeUnit.MILLISECONDS.sleep(1000);
29+
assertTrue(true);
30+
}
31+
32+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.concurrent.TimeUnit;
6+
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.junit.rules.Timeout;
10+
11+
public class RuleOverrideExpectedTimeout {
12+
13+
@Rule
14+
public Timeout globalTimeout = Timeout.millis(1000);
15+
16+
@Test
17+
public void testRuleOverrideTimeout() throws InterruptedException {
18+
System.out.println("testRuleOverrideTimeout");
19+
TimeUnit.MILLISECONDS.sleep(1500);
20+
assertTrue(true);
21+
}
22+
23+
@Test(timeout = 1500)
24+
public void testRuleOverrideTimeoutWithSpecifiedTimeout() throws InterruptedException {
25+
System.out.println("testRuleOverrideTimeoutWithSpecifiedTimeout");
26+
TimeUnit.MILLISECONDS.sleep(1600);
27+
assertTrue(true);
28+
}
29+
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.concurrent.TimeUnit;
6+
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.junit.rules.Timeout;
10+
11+
public class TimeoutRuleOverridePassing {
12+
13+
@Rule
14+
public Timeout globalTimeout = Timeout.millis(1000);
15+
16+
@Test
17+
public void testRuleOverridePassed() throws InterruptedException {
18+
System.out.println("testRuleOverridePassed");
19+
TimeUnit.MILLISECONDS.sleep(600);
20+
assertTrue(true);
21+
}
22+
23+
@Test(timeout = 1500)
24+
public void testRuleOverridePassedWithSpecifiedTimeout() throws InterruptedException {
25+
System.out.println("testRuleOverridePassedWithSpecifiedTimeout");
26+
TimeUnit.MILLISECONDS.sleep(1100);
27+
assertTrue(true);
28+
}
29+
30+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.concurrent.TimeUnit;
6+
7+
import org.junit.Test;
8+
9+
public class TimeoutRulePassing {
10+
11+
@Test
12+
public void testRulePassed() {
13+
System.out.println("testRulePassed");
14+
assertTrue(true);
15+
}
16+
17+
@Test(timeout = 1000)
18+
public void testRulePassedWithSpecifiedTimeout() throws InterruptedException {
19+
System.out.println("testRulePassedWithSpecifiedTimeout");
20+
TimeUnit.MILLISECONDS.sleep(600);
21+
assertTrue(true);
22+
}
23+
24+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.testng.Assert.assertEquals;
4+
import static org.testng.Assert.assertTrue;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import org.junit.runner.JUnitCore;
10+
import org.junit.runner.Result;
11+
import org.junit.runner.notification.Failure;
12+
import org.junit.runners.model.TestTimedOutException;
13+
import org.testng.annotations.AfterClass;
14+
import org.testng.annotations.BeforeClass;
15+
import org.testng.annotations.Test;
16+
17+
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
18+
19+
public class TimeoutRuleTest {
20+
21+
private static final Map<String, String> MESSAGE_MAP;
22+
23+
static {
24+
MESSAGE_MAP = new HashMap<>();
25+
MESSAGE_MAP.put("testRuleTimeout", "test timed out after 500 milliseconds");
26+
MESSAGE_MAP.put("testRuleTimeoutWithShorterInterval", "test timed out after 400 milliseconds");
27+
MESSAGE_MAP.put("testRuleTimeoutWithLongerInterval", "test timed out after 600 milliseconds");
28+
MESSAGE_MAP.put("testRuleOverrideTimeout", "test timed out after 1000 milliseconds");
29+
MESSAGE_MAP.put("testRuleOverrideTimeoutWithSpecifiedTimeout", "test timed out after 1500 milliseconds");
30+
}
31+
32+
@BeforeClass
33+
public static void before() {
34+
System.setProperty(JUnitSettings.TIMEOUT_RULE.key(), "500");
35+
}
36+
37+
@Test
38+
public void verifyHappyPath() {
39+
RunListenerAdapter rla = new RunListenerAdapter();
40+
41+
JUnitCore runner = new JUnitCore();
42+
runner.addListener(rla);
43+
Result result = runner.run(TimeoutRulePassing.class);
44+
assertTrue(result.wasSuccessful());
45+
46+
assertEquals(rla.getPassedTests().size(), 2, "Incorrect passed test count");
47+
assertEquals(rla.getFailedTests().size(), 0, "Incorrect failed test count");
48+
assertEquals(rla.getIgnoredTests().size(), 0, "Incorrect ignored test count");
49+
}
50+
51+
@Test
52+
public void verifyOverrideHappyPath() {
53+
RunListenerAdapter rla = new RunListenerAdapter();
54+
55+
JUnitCore runner = new JUnitCore();
56+
runner.addListener(rla);
57+
Result result = runner.run(TimeoutRuleOverridePassing.class);
58+
assertTrue(result.wasSuccessful());
59+
60+
assertEquals(rla.getPassedTests().size(), 2, "Incorrect passed test count");
61+
assertEquals(rla.getFailedTests().size(), 0, "Incorrect failed test count");
62+
assertEquals(rla.getIgnoredTests().size(), 0, "Incorrect ignored test count");
63+
}
64+
65+
@Test
66+
public void verifyExpectedTimeout() {
67+
RunListenerAdapter rla = new RunListenerAdapter();
68+
69+
JUnitCore runner = new JUnitCore();
70+
runner.addListener(rla);
71+
Result result = runner.run(RuleExpectedTimeout.class);
72+
73+
assertEquals(rla.getPassedTests().size(), 0, "Incorrect passed test count");
74+
assertEquals(rla.getFailedTests().size(), 3, "Incorrect failed test count");
75+
assertEquals(rla.getIgnoredTests().size(), 0, "Incorrect ignored test count");
76+
77+
verifyFailureMessages(result);
78+
}
79+
80+
@Test
81+
public void verifyOverrideExpectedTimeout() {
82+
RunListenerAdapter rla = new RunListenerAdapter();
83+
84+
JUnitCore runner = new JUnitCore();
85+
runner.addListener(rla);
86+
Result result = runner.run(RuleOverrideExpectedTimeout.class);
87+
88+
assertEquals(rla.getPassedTests().size(), 0, "Incorrect passed test count");
89+
assertEquals(rla.getFailedTests().size(), 2, "Incorrect failed test count");
90+
assertEquals(rla.getIgnoredTests().size(), 0, "Incorrect ignored test count");
91+
92+
verifyFailureMessages(result);
93+
}
94+
95+
@AfterClass
96+
public static void after() {
97+
System.clearProperty(JUnitSettings.TIMEOUT_RULE.key());
98+
}
99+
100+
private static void verifyFailureMessages(Result result) {
101+
for (Failure failure : result.getFailures()) {
102+
String methodName = failure.getDescription().getMethodName();
103+
String expect = MESSAGE_MAP.get(methodName);
104+
Throwable thrown = failure.getException();
105+
assertEquals(thrown.getClass(), TestTimedOutException.class, "Exception class for: " + methodName);
106+
assertEquals(thrown.getMessage(), expect, "Failure message for: " + methodName);
107+
}
108+
}
109+
110+
}

0 commit comments

Comments
 (0)