Skip to content

Commit cf4c84d

Browse files
authored
Add lifecycle events for Theory method permutations (#80)
1 parent 03ad475 commit cf4c84d

File tree

13 files changed

+475
-56
lines changed

13 files changed

+475
-56
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.lang.reflect.Field;
4+
import java.util.Objects;
5+
import java.util.concurrent.Callable;
6+
7+
import org.junit.experimental.theories.PotentialAssignment.CouldNotGenerateValueException;
8+
import org.junit.experimental.theories.Theories.TheoryAnchor;
9+
import org.junit.experimental.theories.internal.Assignments;
10+
import org.junit.runner.Description;
11+
import net.bytebuddy.implementation.bind.annotation.Argument;
12+
import net.bytebuddy.implementation.bind.annotation.SuperCall;
13+
import net.bytebuddy.implementation.bind.annotation.This;
14+
15+
/**
16+
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#describeChild
17+
* describeChild} method.
18+
*/
19+
public class DescribeChild {
20+
21+
private static final Field uniqueId;
22+
23+
static {
24+
Field field = null;
25+
try {
26+
field = Description.class.getDeclaredField("fUniqueId");
27+
field.setAccessible(true);
28+
29+
} catch (NoSuchFieldException | SecurityException e) {
30+
field = null;
31+
}
32+
uniqueId = field;
33+
}
34+
35+
/**
36+
* Interceptor for the {@link org.junit.runners.ParentRunner#describeChild describeChild} method.
37+
*
38+
* @param runner underlying test runner
39+
* @param proxy callable proxy for the intercepted method
40+
* @param child child object of the test runner
41+
* @return a {@link Description} for {@code child}
42+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
43+
*/
44+
public static Description intercept(@This final Object runner,
45+
@SuperCall final Callable<?> proxy,
46+
@Argument(0) final Object child) throws Exception {
47+
48+
Description description = (Description) LifecycleHooks.callProxy(proxy);
49+
50+
// if [uniqueId] can be overridden and is test
51+
if ((uniqueId != null) && description.isTest()) {
52+
try {
53+
// get parent of test runner
54+
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
55+
// if child of TheoryAnchor statement
56+
if (parent instanceof TheoryAnchor) {
57+
// get assignments for this theory permutation
58+
Assignments assignments = LifecycleHooks.getFieldValue(runner, "val$complete");
59+
// inject permutation ID into description
60+
injectPermutationId(description, assignments);
61+
}
62+
} catch (IllegalAccessException | NoSuchFieldException | SecurityException | IllegalArgumentException e) {
63+
// nothing to do here
64+
}
65+
}
66+
return description;
67+
}
68+
69+
/**
70+
* Inject permutation ID into the specified description, overriding its default ID.
71+
*
72+
* @param description description of "theory" method
73+
* @param assignments arguments for this permutation
74+
*/
75+
private static void injectPermutationId(final Description description, final Assignments assignments) {
76+
try {
77+
int permutationId = Objects.hash(description.getDisplayName(), assignments.getMethodArguments());
78+
String theoryId = String.format("%08X", permutationId);
79+
uniqueId.set(description, theoryId);
80+
} catch (CouldNotGenerateValueException | SecurityException | IllegalArgumentException
81+
| IllegalAccessException eaten) {
82+
// nothing to do here
83+
}
84+
}
85+
86+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@
55
import net.bytebuddy.implementation.bind.annotation.SuperCall;
66
import net.bytebuddy.implementation.bind.annotation.This;
77

8+
/**
9+
* This class declares the interceptor for the {@link org.junit.runners.model.RunnerScheduler#finished
10+
* finished} method.
11+
*/
812
public class Finished {
13+
14+
/**
15+
* Interceptor for the {@link org.junit.runners.model.RunnerScheduler#finished finished} method.
16+
*
17+
* @param scheduler current {@link org.junit.runners.model.RunnerScheduler RunnerScheduler}
18+
* @param proxy callable proxy for the intercepted method
19+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
20+
*/
921
public static void intercept(@This final Object scheduler, @SuperCall final Callable<?> proxy) throws Exception {
1022
LifecycleHooks.callProxy(proxy);
1123
RunChild.finished();

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1111
import net.bytebuddy.implementation.bind.annotation.This;
1212

13+
/**
14+
* This class declares an interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#getTestRules
15+
* getTestRules} method.
16+
*/
1317
public class GetTestRules {
1418
/**
15-
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#getTestRules(Object)} getTestRules} method.
19+
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#getTestRules getTestRules} method.
1620
*
1721
* @param runner target {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} object
1822
* @param proxy callable proxy for the intercepted method

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.junit.internal.runners.model.ReflectiveCallable;
2020
import org.junit.runner.Description;
2121
import org.junit.runner.notification.RunListener;
22+
import org.junit.runner.notification.RunNotifier;
2223
import org.junit.runners.model.TestClass;
2324

2425
import com.google.common.base.Function;
@@ -162,10 +163,13 @@ public static void premain(String agentArgs, Instrumentation instrumentation) {
162163
public static ClassFileTransformer installTransformer(Instrumentation instrumentation) {
163164
final TypeDescription runReflectiveCall = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunReflectiveCall").resolve();
164165
final TypeDescription finished = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Finished").resolve();
165-
final TypeDescription createTest = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.CreateTest").resolve();
166166
final TypeDescription runChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChild").resolve();
167167
final TypeDescription run = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Run").resolve();
168+
final TypeDescription describeChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.DescribeChild").resolve();
169+
final TypeDescription methodBlock = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.MethodBlock").resolve();
170+
final TypeDescription createTest = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.CreateTest").resolve();
168171
final TypeDescription getTestRules = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.GetTestRules").resolve();
172+
final TypeDescription runWithCompleteAssignment = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunWithCompleteAssignment").resolve();
169173

170174
final TypeDescription runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
171175
final SignatureToken runToken = new SignatureToken("run", TypeDescription.VOID, Arrays.asList(runNotifier));
@@ -194,13 +198,27 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
194198
@Override
195199
public Builder<?> transform(Builder<?> builder, TypeDescription type,
196200
ClassLoader classloader, JavaModule module) {
197-
return builder.method(named("createTest")).intercept(MethodDelegation.to(createTest))
198-
.method(named("runChild")).intercept(MethodDelegation.to(runChild))
201+
return builder.method(named("runChild")).intercept(MethodDelegation.to(runChild))
199202
.method(hasSignature(runToken)).intercept(MethodDelegation.to(run))
203+
.method(named("describeChild")).intercept(MethodDelegation.to(describeChild))
204+
// NOTE: The 'methodBlock', 'createTest', and 'getTestRules' methods
205+
// are defined in BlockJUnit4ClassRunner, but I've been unable
206+
// to transform this ParentRunner subclass.
207+
.method(named("methodBlock")).intercept(MethodDelegation.to(methodBlock))
208+
.method(named("createTest").and(takesArguments(0))).intercept(MethodDelegation.to(createTest))
200209
.method(named("getTestRules")).intercept(MethodDelegation.to(getTestRules))
201210
.implement(Hooked.class);
202211
}
203212
})
213+
.type(hasSuperType(named("org.junit.experimental.theories.Theories$TheoryAnchor")))
214+
.transform(new Transformer() {
215+
@Override
216+
public Builder<?> transform(Builder<?> builder, TypeDescription type,
217+
ClassLoader classloader, JavaModule module) {
218+
return builder.method(named("runWithCompleteAssignment")).intercept(MethodDelegation.to(runWithCompleteAssignment))
219+
.implement(Hooked.class);
220+
}
221+
})
204222
.installOn(instrumentation);
205223
}
206224

@@ -262,6 +280,16 @@ public static Object getParentOf(Object child) {
262280
return Run.getParentOf(child);
263281
}
264282

283+
/**
284+
* Get the run notifier associated with the specified parent runner.
285+
*
286+
* @param runner JUnit parent runner
287+
* @return {@link RunNotifier} object for the specified parent runner (may be {@code null})
288+
*/
289+
public static RunNotifier getNotifierOf(final Object runner) {
290+
return Run.getNotifierOf(runner);
291+
}
292+
265293
/**
266294
* Get the runner that owns the active thread context.
267295
*
@@ -470,6 +498,7 @@ public static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> w
470498
* @param listenerType listener type
471499
* @return optional listener instance
472500
*/
501+
@SuppressWarnings("unchecked")
473502
public static <T extends RunListener> Optional<T> getAttachedListener(Class<T> listenerType) {
474503
for (RunListener listener : runListeners) {
475504
if (listener.getClass() == listenerType) {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.Callable;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.concurrent.ConcurrentMap;
7+
import org.junit.experimental.theories.Theories.TheoryAnchor;
8+
import org.junit.runners.model.FrameworkMethod;
9+
import org.junit.runners.model.Statement;
10+
11+
import com.google.common.base.Function;
12+
13+
import net.bytebuddy.implementation.bind.annotation.SuperCall;
14+
import net.bytebuddy.implementation.bind.annotation.This;
15+
import net.bytebuddy.implementation.bind.annotation.Argument;
16+
17+
/**
18+
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#methodBlock
19+
* methodBlock} method.
20+
*/
21+
public class MethodBlock {
22+
private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> methodDepth;
23+
private static final Function<Integer, DepthGauge> newInstance;
24+
private static final Map<Object, Statement> RUNNER_TO_STATEMENT = new ConcurrentHashMap<>();
25+
26+
static {
27+
methodDepth = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() {
28+
@Override
29+
protected ConcurrentMap<Integer, DepthGauge> initialValue() {
30+
return new ConcurrentHashMap<>();
31+
}
32+
};
33+
newInstance = new Function<Integer, DepthGauge>() {
34+
@Override
35+
public DepthGauge apply(Integer input) {
36+
return new DepthGauge();
37+
}
38+
};
39+
}
40+
41+
/**
42+
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#methodBlock methodBlock} method.
43+
* <p>
44+
* <b>NOTE</b>: For "theory" methods, the actual class runner statement is stored and a
45+
* "lifecycle catalyst" statement is returned instead. This enables the interceptor declared
46+
* in the {@link RunWithCompleteAssignment} class to manage the execution of the actual
47+
* statement, publishing a complete set of test lifecycle events.
48+
*
49+
* @param runner underlying test runner
50+
* @param proxy callable proxy for the intercepted method
51+
* @param method framework method that's the "identity" of an atomic test
52+
* @return {@link Statement} to execute the atomic test
53+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
54+
*/
55+
public static Statement intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
56+
@Argument(0) final FrameworkMethod method) throws Exception {
57+
58+
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(methodDepth.get(), runner.hashCode(), newInstance);
59+
depthGauge.increaseDepth();
60+
61+
Statement statement = (Statement) LifecycleHooks.callProxy(proxy);
62+
63+
// if at ground level
64+
if (0 == depthGauge.decreaseDepth()) {
65+
try {
66+
// get parent of test runner
67+
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
68+
// if child of TheoryAnchor statement
69+
if (parent instanceof TheoryAnchor) {
70+
// store actual statement of test runner
71+
RUNNER_TO_STATEMENT.put(runner, statement);
72+
// create lifecycle catalyst
73+
statement = new Statement() {
74+
final Object threadRunner = runner;
75+
final FrameworkMethod testMethod = method;
76+
77+
@Override
78+
public void evaluate() throws Throwable {
79+
// attach class runner to thread
80+
Run.pushThreadRunner(threadRunner);
81+
// create new atomic test for target method
82+
RunAnnouncer.newAtomicTest(threadRunner, testMethod);
83+
}
84+
};
85+
}
86+
} catch (IllegalAccessException | NoSuchFieldException | SecurityException | IllegalArgumentException e) {
87+
// nothing to do here
88+
}
89+
}
90+
91+
return statement;
92+
}
93+
94+
/**
95+
* Get the statement associated with the specified runner.
96+
*
97+
* @param runner JUnit class runner
98+
* @return {@link Statement} for the specified runner; may be {@code null}
99+
*/
100+
static Statement getStatementOf(final Object runner) {
101+
return RUNNER_TO_STATEMENT.get(runner);
102+
}
103+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class Run {
2424
private static final Set<String> startNotified = new CopyOnWriteArraySet<>();
2525
private static final Set<String> finishNotified = new CopyOnWriteArraySet<>();
2626
private static final Map<Object, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
27+
private static final Map<Object, RunNotifier> RUNNER_TO_NOTIFIER = new ConcurrentHashMap<>();
2728
private static final Set<RunNotifier> NOTIFIERS = new CopyOnWriteArraySet<>();
2829
private static final Logger LOGGER = LoggerFactory.getLogger(Run.class);
2930

@@ -47,6 +48,8 @@ protected Deque<Object> initialValue() {
4748
public static void intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
4849
@Argument(0) final RunNotifier notifier) throws Exception {
4950

51+
RUNNER_TO_NOTIFIER.put(runner, notifier);
52+
5053
attachRunListeners(runner, notifier);
5154

5255
try {
@@ -69,6 +72,16 @@ static Object getParentOf(final Object child) {
6972
return CHILD_TO_PARENT.get(child);
7073
}
7174

75+
/**
76+
* Get the run notifier associated with the specified parent runner.
77+
*
78+
* @param runner JUnit parent runner
79+
* @return <b>RunNotifier</b> object (may be {@code null})
80+
*/
81+
static RunNotifier getNotifierOf(final Object runner) {
82+
return RUNNER_TO_NOTIFIER.get(runner);
83+
}
84+
7285
/**
7386
* Attach registered run listeners to the specified run notifier.
7487
* <p>

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

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.nordstrom.automation.junit;
22

3-
import static com.nordstrom.automation.junit.LifecycleHooks.getFieldValue;
4-
53
import java.util.Map;
64
import java.util.concurrent.ConcurrentHashMap;
75

8-
import org.junit.experimental.theories.Theories.TheoryAnchor;
96
import org.junit.internal.AssumptionViolatedException;
107
import org.junit.runner.Description;
118
import org.junit.runner.notification.Failure;
@@ -121,6 +118,7 @@ public void testIgnored(Description description) throws Exception {
121118
static <T> AtomicTest<T> newAtomicTest(Object runner, T identity) {
122119
AtomicTest<T> atomicTest = new AtomicTest<>(runner, identity);
123120
RUNNER_TO_ATOMICTEST.put(runner, atomicTest);
121+
RUNNER_TO_ATOMICTEST.put(atomicTest.getDescription(), atomicTest);
124122
return atomicTest;
125123
}
126124

@@ -154,22 +152,8 @@ static <T> AtomicTest<T> newAtomicTest(Description description) {
154152
static <T> AtomicTest<T> getAtomicTestOf(Object testKey) {
155153
AtomicTest<T> atomicTest = null;
156154
if (testKey != null) {
157-
// get atomic test for this runner
155+
// get atomic test for this runner/description
158156
atomicTest = (AtomicTest<T>) RUNNER_TO_ATOMICTEST.get(testKey);
159-
// if none is found
160-
if (atomicTest == null) {
161-
try {
162-
// get object that created this runner
163-
Object anchor = getFieldValue(testKey, "this$0");
164-
// if created by TheoryAnchor
165-
if (anchor instanceof TheoryAnchor) {
166-
// create new atomic test for "theory" test method
167-
atomicTest = (AtomicTest<T>) newAtomicTest(testKey, getFieldValue(anchor, "testMethod"));
168-
}
169-
} catch (IllegalAccessException | NoSuchFieldException | SecurityException | IllegalArgumentException e) {
170-
// nothing to do here
171-
}
172-
}
173157
}
174158
return atomicTest;
175159
}

0 commit comments

Comments
 (0)