Skip to content

Commit 3ac1344

Browse files
authored
Release references to JUnit objects (#91)
* Release references to JUnit objects * Remove unused imports * Add code comments * Release more references, and retain fewer * Discard runner-to-notifier mapping * Ensure mapping is always discarded * Release runner/child mappings
1 parent 376e651 commit 3ac1344

File tree

8 files changed

+196
-36
lines changed

8 files changed

+196
-36
lines changed

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1111
import net.bytebuddy.implementation.bind.annotation.This;
1212

13+
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
14+
1315
/**
1416
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest
1517
* createTest} method.
1618
*/
1719
@SuppressWarnings("squid:S1118")
1820
public class CreateTest {
1921

20-
private static final Map<Object, Object> TARGET_TO_RUNNER = new ConcurrentHashMap<>();
21-
private static final Map<Object, Object> RUNNER_TO_TARGET = new ConcurrentHashMap<>();
22+
private static final Map<String, Object> TARGET_TO_RUNNER = new ConcurrentHashMap<>();
23+
private static final Map<String, Object> RUNNER_TO_TARGET = new ConcurrentHashMap<>();
2224
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTest.class);
2325

2426
/**
@@ -37,9 +39,9 @@ public static Object intercept(@This final Object runner,
3739
// apply parameter-based global timeout
3840
TimeoutUtils.applyTestTimeout(runner, target);
3941

40-
if (null == TARGET_TO_RUNNER.put(target, runner)) {
42+
if (null == TARGET_TO_RUNNER.put(toMapKey(target), runner)) {
4143
LOGGER.debug("testObjectCreated: {}", target);
42-
RUNNER_TO_TARGET.put(runner, target);
44+
RUNNER_TO_TARGET.put(toMapKey(runner), target);
4345

4446
for (TestObjectWatcher watcher : LifecycleHooks.getObjectWatchers()) {
4547
watcher.testObjectCreated(target, runner);
@@ -56,7 +58,7 @@ public static Object intercept(@This final Object runner,
5658
* @return {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} for specified instance
5759
*/
5860
static Object getRunnerForTarget(Object target) {
59-
return TARGET_TO_RUNNER.get(target);
61+
return TARGET_TO_RUNNER.get(toMapKey(target));
6062
}
6163

6264
/**
@@ -66,6 +68,18 @@ static Object getRunnerForTarget(Object target) {
6668
* @return JUnit test class instance for specified runner
6769
*/
6870
static Object getTargetForRunner(Object runner) {
69-
return RUNNER_TO_TARGET.get(runner);
71+
return RUNNER_TO_TARGET.get(toMapKey(runner));
72+
}
73+
74+
/**
75+
* Release runner/target mappings.
76+
*
77+
* @param runner JUnit class runner
78+
*/
79+
static void releaseMappingsFor(Object runner) {
80+
Object target = RUNNER_TO_TARGET.remove(toMapKey(runner));
81+
if (target != null) {
82+
TARGET_TO_RUNNER.remove(toMapKey(target));
83+
}
7084
}
7185
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,19 @@ static List<MethodWatcher<?>> getMethodWatchers() {
575575
return methodWatchers;
576576
}
577577

578+
/**
579+
* Create a unique map key string to represent the specified object.
580+
* <p>
581+
* <b>NOTE</b>: The string returned by this method matches the output of
582+
* the default {@link Object#toString()} implementation.
583+
*
584+
* @param obj target object
585+
* @return map key string
586+
*/
587+
static String toMapKey(Object obj) {
588+
return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj));
589+
}
590+
578591
/**
579592
* This class encapsulates the process of retrieving watcher objects of the target type from the collection of all
580593
* attached watcher objects. This is a private nested class that directly accesses the main collection. It is also

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
import net.bytebuddy.implementation.bind.annotation.This;
1515
import net.bytebuddy.implementation.bind.annotation.Argument;
1616

17+
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
18+
1719
/**
1820
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#methodBlock
1921
* methodBlock} method.
2022
*/
2123
public class MethodBlock {
2224
private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> methodDepth;
2325
private static final Function<Integer, DepthGauge> newInstance;
24-
private static final Map<Object, Statement> RUNNER_TO_STATEMENT = new ConcurrentHashMap<>();
26+
private static final Map<String, Statement> RUNNER_TO_STATEMENT = new ConcurrentHashMap<>();
2527

2628
static {
2729
methodDepth = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() {
@@ -68,7 +70,7 @@ public static Statement intercept(@This final Object runner, @SuperCall final Ca
6870
// if child of TheoryAnchor statement
6971
if (parent instanceof TheoryAnchor) {
7072
// store actual statement of test runner
71-
RUNNER_TO_STATEMENT.put(runner, statement);
73+
RUNNER_TO_STATEMENT.put(toMapKey(runner), statement);
7274
// create lifecycle catalyst
7375
statement = new Statement() {
7476
final Object threadRunner = runner;
@@ -98,6 +100,6 @@ public void evaluate() throws Throwable {
98100
* @return {@link Statement} for the specified runner; may be {@code null}
99101
*/
100102
static Statement getStatementOf(final Object runner) {
101-
return RUNNER_TO_STATEMENT.get(runner);
103+
return RUNNER_TO_STATEMENT.remove(toMapKey(runner));
102104
}
103105
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import org.junit.internal.AssumptionViolatedException;
4+
5+
public class ReferenceRemover implements RunnerWatcher, RunWatcher<Object> {
6+
7+
@Override
8+
public void runStarted(Object runner) {
9+
}
10+
11+
@Override
12+
public void runFinished(Object runner) {
13+
// release callables associated with runner
14+
RunReflectiveCall.releaseCallablesOf(runner);
15+
16+
// release mapping of parent runner to atomic test
17+
AtomicTest<Object> atomicTest = RunAnnouncer.releaseAtomicTestOf(runner);
18+
// if mapping existed
19+
if (atomicTest != null) {
20+
// release mapping of method description to atomic test
21+
RunAnnouncer.releaseAtomicTestOf(atomicTest.getDescription());
22+
}
23+
24+
// release runner/child mappings
25+
Run.releaseChidrenOf(runner);
26+
27+
// release runner/target mappings
28+
CreateTest.releaseMappingsFor(runner);
29+
}
30+
31+
@Override
32+
public Class<Object> supportedType() {
33+
return Object.class;
34+
}
35+
36+
@Override
37+
public void testStarted(AtomicTest<Object> atomicTest) {
38+
}
39+
40+
@Override
41+
public void testFinished(AtomicTest<Object> atomicTest) {
42+
// release mapping of method description to atomic test
43+
RunAnnouncer.releaseAtomicTestOf(atomicTest.getDescription());
44+
}
45+
46+
@Override
47+
public void testFailure(AtomicTest<Object> atomicTest, Throwable thrown) {
48+
}
49+
50+
@Override
51+
public void testAssumptionFailure(AtomicTest<Object> atomicTest, AssumptionViolatedException thrown) {
52+
}
53+
54+
@Override
55+
public void testIgnored(AtomicTest<Object> atomicTest) {
56+
}
57+
}

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,23 @@
1515
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1616
import net.bytebuddy.implementation.bind.annotation.This;
1717

18+
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
19+
1820
/**
1921
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
2022
*/
2123
@SuppressWarnings("squid:S1118")
2224
public class Run {
23-
private static final ThreadLocal<Deque<Object>> runnerStack;
24-
private static final Set<String> startNotified = new CopyOnWriteArraySet<>();
25-
private static final Set<String> finishNotified = new CopyOnWriteArraySet<>();
26-
private static final Map<Object, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
27-
private static final Map<Object, RunNotifier> RUNNER_TO_NOTIFIER = new ConcurrentHashMap<>();
28-
private static final Set<RunNotifier> NOTIFIERS = new CopyOnWriteArraySet<>();
25+
private static final ThreadLocal<Deque<Object>> RUNNER_STACK;
26+
private static final Set<String> START_NOTIFIED = new CopyOnWriteArraySet<>();
27+
private static final Set<String> FINISH_NOTIFIED = new CopyOnWriteArraySet<>();
28+
private static final Map<String, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
29+
private static final Map<String, RunNotifier> RUNNER_TO_NOTIFIER = new ConcurrentHashMap<>();
30+
private static final Set<String> NOTIFIERS = new CopyOnWriteArraySet<>();
2931
private static final Logger LOGGER = LoggerFactory.getLogger(Run.class);
3032

3133
static {
32-
runnerStack = new ThreadLocal<Deque<Object>>() {
34+
RUNNER_STACK = new ThreadLocal<Deque<Object>>() {
3335
@Override
3436
protected Deque<Object> initialValue() {
3537
return new ArrayDeque<>();
@@ -48,17 +50,17 @@ protected Deque<Object> initialValue() {
4850
public static void intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
4951
@Argument(0) final RunNotifier notifier) throws Exception {
5052

51-
RUNNER_TO_NOTIFIER.put(runner, notifier);
52-
5353
attachRunListeners(runner, notifier);
5454

5555
try {
56+
RUNNER_TO_NOTIFIER.put(toMapKey(runner), notifier);
5657
pushThreadRunner(runner);
5758
fireRunStarted(runner);
5859
LifecycleHooks.callProxy(proxy);
5960
} finally {
6061
fireRunFinished(runner);
6162
popThreadRunner();
63+
RUNNER_TO_NOTIFIER.remove(toMapKey(runner));
6264
}
6365
}
6466

@@ -69,7 +71,7 @@ public static void intercept(@This final Object runner, @SuperCall final Callabl
6971
* @return {@code ParentRunner} object that owns the specified child ({@code null} for root objects)
7072
*/
7173
static Object getParentOf(final Object child) {
72-
return CHILD_TO_PARENT.get(child);
74+
return CHILD_TO_PARENT.get(toMapKey(child));
7375
}
7476

7577
/**
@@ -79,7 +81,7 @@ static Object getParentOf(final Object child) {
7981
* @return <b>RunNotifier</b> object (may be {@code null})
8082
*/
8183
static RunNotifier getNotifierOf(final Object runner) {
82-
return RUNNER_TO_NOTIFIER.get(runner);
84+
return RUNNER_TO_NOTIFIER.get(toMapKey(runner));
8385
}
8486

8587
/**
@@ -92,7 +94,7 @@ static RunNotifier getNotifierOf(final Object runner) {
9294
* @throws Exception if {@code run-started} notification
9395
*/
9496
static void attachRunListeners(Object runner, final RunNotifier notifier) throws Exception {
95-
if (NOTIFIERS.add(notifier)) {
97+
if (NOTIFIERS.add(toMapKey(notifier))) {
9698
Description description = LifecycleHooks.invoke(runner, "getDescription");
9799
for (RunListener listener : LifecycleHooks.getRunListeners()) {
98100
notifier.addListener(listener);
@@ -107,7 +109,7 @@ static void attachRunListeners(Object runner, final RunNotifier notifier) throws
107109
* @param runner JUnit test runner
108110
*/
109111
static void pushThreadRunner(final Object runner) {
110-
runnerStack.get().push(runner);
112+
RUNNER_STACK.get().push(runner);
111113
}
112114

113115
/**
@@ -117,7 +119,7 @@ static void pushThreadRunner(final Object runner) {
117119
* @throws EmptyStackException if called outside the scope of an active runner
118120
*/
119121
static Object popThreadRunner() {
120-
return runnerStack.get().pop();
122+
return RUNNER_STACK.get().pop();
121123
}
122124

123125
/**
@@ -126,7 +128,7 @@ static Object popThreadRunner() {
126128
* @return active {@code ParentRunner} object
127129
*/
128130
static Object getThreadRunner() {
129-
return runnerStack.get().peek();
131+
return RUNNER_STACK.get().peek();
130132
}
131133

132134
/**
@@ -137,10 +139,10 @@ static Object getThreadRunner() {
137139
* @return {@code true} if event the {@code runStarted} was fired; otherwise {@code false}
138140
*/
139141
static boolean fireRunStarted(Object runner) {
140-
if (startNotified.add(runner.toString())) {
141-
List<?> grandchildren = LifecycleHooks.invoke(runner, "getChildren");
142-
for (Object grandchild : grandchildren) {
143-
CHILD_TO_PARENT.put(grandchild, runner);
142+
if (START_NOTIFIED.add(toMapKey(runner))) {
143+
List<?> children = LifecycleHooks.invoke(runner, "getChildren");
144+
for (Object child : children) {
145+
CHILD_TO_PARENT.put(toMapKey(child), runner);
144146
}
145147
LOGGER.debug("runStarted: {}", runner);
146148
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
@@ -155,11 +157,12 @@ static boolean fireRunStarted(Object runner) {
155157
* Fire the {@link RunnerWatcher#runFinished(Object)} event for the specified runner.
156158
* <p>
157159
* <b>NOTE</b>: If {@code runFinished} for the specified runner has already been fired, do nothing.
160+
*
158161
* @param runner JUnit test runner
159162
* @return {@code true} if event the {@code runFinished} was fired; otherwise {@code false}
160163
*/
161164
static boolean fireRunFinished(Object runner) {
162-
if (finishNotified.add(runner.toString())) {
165+
if (FINISH_NOTIFIED.add(toMapKey(runner))) {
163166
LOGGER.debug("runFinished: {}", runner);
164167
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
165168
watcher.runFinished(runner);
@@ -168,4 +171,16 @@ static boolean fireRunFinished(Object runner) {
168171
}
169172
return false;
170173
}
174+
175+
/**
176+
* Release runner/child mappings.
177+
*
178+
* @param runner JUnit test runner
179+
*/
180+
static void releaseChidrenOf(Object runner) {
181+
List<?> children = LifecycleHooks.invoke(runner, "getChildren");
182+
for (Object child : children) {
183+
CHILD_TO_PARENT.remove(toMapKey(child));
184+
}
185+
}
171186
}

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.slf4j.Logger;
1111
import org.slf4j.LoggerFactory;
1212

13+
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
14+
1315
/**
1416
* This class implements a notification-enhancing extension of the standard {@link RunListener} class. This run
1517
* announcer is the source of notifications sent to attached implementations of the {@link RunWatcher} interface.
@@ -21,7 +23,7 @@
2123
*/
2224
public class RunAnnouncer extends RunListener implements JUnitWatcher {
2325

24-
private static final Map<Object, AtomicTest<?>> RUNNER_TO_ATOMICTEST = new ConcurrentHashMap<>();
26+
private static final Map<String, AtomicTest<?>> RUNNER_TO_ATOMICTEST = new ConcurrentHashMap<>();
2527
private static final Logger LOGGER = LoggerFactory.getLogger(RunAnnouncer.class);
2628

2729
/**
@@ -115,10 +117,18 @@ public void testIgnored(Description description) throws Exception {
115117
* @param identity identity for this atomic test
116118
* @return {@link AtomicTest} object
117119
*/
120+
@SuppressWarnings("unchecked")
118121
static <T> AtomicTest<T> newAtomicTest(Object runner, T identity) {
119122
AtomicTest<T> atomicTest = new AtomicTest<>(runner, identity);
120-
RUNNER_TO_ATOMICTEST.put(runner, atomicTest);
121-
RUNNER_TO_ATOMICTEST.put(atomicTest.getDescription(), atomicTest);
123+
// map parent runner to new atomic test, retaining prior mapping
124+
AtomicTest<T> priorValue = (AtomicTest<T>) RUNNER_TO_ATOMICTEST.put(toMapKey(runner), atomicTest);
125+
// if prior mapping found
126+
if (priorValue != null) {
127+
// release prior method description mapping
128+
releaseAtomicTestOf(priorValue.getDescription());
129+
}
130+
// map method description to atomic test
131+
RUNNER_TO_ATOMICTEST.put(toMapKey(atomicTest.getDescription()), atomicTest);
122132
return atomicTest;
123133
}
124134

@@ -135,7 +145,7 @@ static <T> AtomicTest<T> newAtomicTest(Description description) {
135145

136146
if (original != null) {
137147
atomicTest = new AtomicTest<>(original, description);
138-
RUNNER_TO_ATOMICTEST.put(description, atomicTest);
148+
RUNNER_TO_ATOMICTEST.put(toMapKey(description), atomicTest);
139149
}
140150

141151
return atomicTest;
@@ -153,7 +163,23 @@ static <T> AtomicTest<T> getAtomicTestOf(Object testKey) {
153163
AtomicTest<T> atomicTest = null;
154164
if (testKey != null) {
155165
// get atomic test for this runner/description
156-
atomicTest = (AtomicTest<T>) RUNNER_TO_ATOMICTEST.get(testKey);
166+
atomicTest = (AtomicTest<T>) RUNNER_TO_ATOMICTEST.get(toMapKey(testKey));
167+
}
168+
return atomicTest;
169+
}
170+
171+
/**
172+
* Release the atomic test object for the specified class runner or method description.
173+
*
174+
* @param <T> atomic test child object type
175+
* @param testKey JUnit class runner or method description
176+
*/
177+
@SuppressWarnings("unchecked")
178+
static <T> AtomicTest<T> releaseAtomicTestOf(Object testKey) {
179+
AtomicTest<T> atomicTest = null;
180+
if (testKey != null) {
181+
// get atomic test for this runner/description
182+
atomicTest = (AtomicTest<T>) RUNNER_TO_ATOMICTEST.remove(toMapKey(testKey));
157183
}
158184
return atomicTest;
159185
}

0 commit comments

Comments
 (0)