Skip to content

Commit dcce914

Browse files
authored
Release framework objects ASAP to minimize memory consumption
1 parent b429289 commit dcce914

27 files changed

+865
-619
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.util.concurrent.Callable;
4+
5+
import org.junit.runners.model.MultipleFailureException;
6+
import org.junit.internal.runners.model.EachTestNotifier;
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.internal.runners.model.EachTestNotifier#addFailure
14+
* addFailure} method.
15+
*/
16+
public class AddFailure {
17+
18+
/**
19+
* Interceptor for the {@link org.junit.internal.runners.model.EachTestNotifier#addFailure addFailure} method.
20+
*
21+
* @param notifier underlying run notifier
22+
* @param proxy callable proxy for the intercepted method
23+
* @param targetException the exception thrown by the test
24+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
25+
*/
26+
public static void intercept(@This final EachTestNotifier notifier, @SuperCall final Callable<?> proxy,
27+
@Argument(0) final Throwable targetException) throws Exception {
28+
29+
if ( ! (targetException instanceof MultipleFailureException)) {
30+
AtomicTest atomicTest = EachTestNotifierInit.getAtomicTestOf(notifier);
31+
if (atomicTest != null) {
32+
atomicTest.setThrowable(targetException);
33+
}
34+
}
35+
36+
LifecycleHooks.callProxy(proxy);
37+
}
38+
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
*/
2121
public class ArtifactCollector<T extends ArtifactType> extends AtomIdentity {
2222

23-
private static final ConcurrentHashMap<Description, List<ArtifactCollector<? extends ArtifactType>>> watcherMap;
24-
private static final Function<Description, List<ArtifactCollector<? extends ArtifactType>>> newInstance;
23+
private static final ConcurrentHashMap<Description, List<ArtifactCollector<? extends ArtifactType>>> WATCHER_MAP;
24+
private static final Function<Description, List<ArtifactCollector<? extends ArtifactType>>> NEW_INSTANCE;
2525

2626
static {
27-
watcherMap = new ConcurrentHashMap<>();
28-
newInstance = new Function<Description, List<ArtifactCollector<? extends ArtifactType>>>() {
27+
WATCHER_MAP = new ConcurrentHashMap<>();
28+
NEW_INSTANCE = new Function<Description, List<ArtifactCollector<? extends ArtifactType>>>() {
2929
@Override
3030
public List<ArtifactCollector<? extends ArtifactType>> apply(Description input) {
3131
return new ArrayList<>();
@@ -48,7 +48,7 @@ public ArtifactCollector(Object instance, T provider) {
4848
public void starting(Description description) {
4949
super.starting(description);
5050
List<ArtifactCollector<? extends ArtifactType>> watcherList =
51-
LifecycleHooks.computeIfAbsent(watcherMap, description, newInstance);
51+
LifecycleHooks.computeIfAbsent(WATCHER_MAP, description, NEW_INSTANCE);
5252
watcherList.add(this);
5353
}
5454

@@ -189,7 +189,7 @@ public T getArtifactProvider() {
189189
@SuppressWarnings("unchecked")
190190
public static <S extends ArtifactCollector<? extends ArtifactType>> Optional<S>
191191
getWatcher(Description description, Class<S> watcherType) {
192-
List<ArtifactCollector<? extends ArtifactType>> watcherList = watcherMap.get(description);
192+
List<ArtifactCollector<? extends ArtifactType>> watcherList = WATCHER_MAP.get(description);
193193
if (watcherList != null) {
194194
for (ArtifactCollector<? extends ArtifactType> watcher : watcherList) {
195195
if (watcher.getClass() == watcherType) {

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

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import static com.nordstrom.automation.junit.LifecycleHooks.invoke;
44

5+
import java.util.ArrayList;
56
import java.util.List;
67
import java.util.Objects;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
710

811
import org.junit.After;
912
import org.junit.AfterClass;
@@ -12,6 +15,7 @@
1215
import org.junit.Ignore;
1316
import org.junit.Test;
1417
import org.junit.runner.Description;
18+
import org.junit.runners.model.FrameworkMethod;
1519
import org.junit.runners.model.TestClass;
1620

1721
/**
@@ -21,25 +25,20 @@
2125
*/
2226
@Ignore
2327
@SuppressWarnings("all")
24-
public class AtomicTest<T> {
28+
public class AtomicTest {
2529
private final Object runner;
2630
private final Description description;
27-
private final T identity;
28-
private final List<T> particles;
31+
private final FrameworkMethod identity;
32+
private final List<FrameworkMethod> particles;
2933
private Throwable thrown;
30-
31-
public AtomicTest(Object runner, T identity) {
32-
this.runner = runner;
33-
this.identity = identity;
34-
this.description = invoke(runner, "describeChild", identity);
35-
this.particles = invoke(runner, "getChildren");
36-
}
3734

38-
public AtomicTest(AtomicTest<T> original, Description description) {
39-
this.runner = original.runner;
40-
this.identity = original.identity;
35+
private static final Pattern PARAM = Pattern.compile("[(\\[]");
36+
37+
public AtomicTest(Description description) {
38+
this.runner = RunChildren.getThreadRunner();
4139
this.description = description;
42-
this.particles = original.particles;
40+
this.particles = getParticles(runner, description);
41+
this.identity = this.particles.get(0);
4342
}
4443

4544
/**
@@ -65,7 +64,7 @@ public Description getDescription() {
6564
*
6665
* @return core method associated with this atomic test
6766
*/
68-
public T getIdentity() {
67+
public FrameworkMethod getIdentity() {
6968
return identity;
7069
}
7170

@@ -74,7 +73,7 @@ public T getIdentity() {
7473
*
7574
* @return list of methods that compose this atomic test
7675
*/
77-
public List<T> getParticles() {
76+
public List<FrameworkMethod> getParticles() {
7877
return particles;
7978
}
8079

@@ -111,10 +110,24 @@ public Throwable getThrowable() {
111110
* @param method method object
112111
* @return {@code true} if this atomic test includes the specified method; otherwise {@code false}
113112
*/
114-
public boolean includes(T method) {
113+
public boolean includes(FrameworkMethod method) {
115114
return particles.contains(method);
116115
}
117116

117+
/**
118+
* Determine if this atomic test represents a "theory" method.
119+
*
120+
* @return {@code true} if this atomic test represents a "theory" method; otherwise {@code false}
121+
*/
122+
public boolean isTheory() {
123+
try {
124+
String uniqueId = LifecycleHooks.getFieldValue(description, "fUniqueId");
125+
return ((uniqueId != null) && (uniqueId.startsWith("theory-id: ")));
126+
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
127+
return false;
128+
}
129+
}
130+
118131
/**
119132
* {@inheritDoc}
120133
*/
@@ -130,7 +143,7 @@ public String toString() {
130143
public boolean equals(Object o) {
131144
if (this == o) return true;
132145
if (o == null || getClass() != o.getClass()) return false;
133-
AtomicTest<T> that = (AtomicTest<T>) o;
146+
AtomicTest that = (AtomicTest) o;
134147
return Objects.equals(runner, that.runner) &&
135148
Objects.equals(identity, that.identity);
136149
}
@@ -140,6 +153,44 @@ public boolean equals(Object o) {
140153
*/
141154
@Override
142155
public int hashCode() {
143-
return Objects.hash(runner, identity);
156+
return description.hashCode();
157+
}
158+
159+
/**
160+
* Get the "particle" method for this atomic test.
161+
*
162+
* @param runner JUnit test runner
163+
* @param description JUnit method description
164+
* @return list of "particle" methods
165+
*/
166+
private List<FrameworkMethod> getParticles(Object runner, Description description) {
167+
List<FrameworkMethod> particles = new ArrayList<>();
168+
if (description.isTest()) {
169+
TestClass testClass = LifecycleHooks.getTestClassOf(runner);
170+
171+
String methodName = description.getMethodName();
172+
Matcher matcher = PARAM.matcher(methodName);
173+
if (matcher.find()) {
174+
methodName = methodName.substring(0, matcher.start());
175+
}
176+
177+
FrameworkMethod identity = null;
178+
for (FrameworkMethod method : testClass.getAnnotatedMethods()) {
179+
if (method.getName().equals(methodName)) {
180+
identity = method;
181+
break;
182+
}
183+
}
184+
185+
if (identity != null) {
186+
particles.add(identity);
187+
particles.addAll(testClass.getAnnotatedMethods(Before.class));
188+
particles.addAll(testClass.getAnnotatedMethods(After.class));
189+
} else {
190+
throw new IllegalStateException("Identity method not found");
191+
}
192+
}
193+
194+
return particles;
144195
}
145196
}
Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,126 @@
11
package com.nordstrom.automation.junit;
22

3+
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
4+
35
import java.util.Map;
6+
import java.util.Objects;
47
import java.util.concurrent.Callable;
58
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
10+
11+
import org.junit.runners.model.FrameworkMethod;
612
import org.slf4j.Logger;
713
import org.slf4j.LoggerFactory;
814

15+
import com.google.common.base.Function;
16+
17+
import net.bytebuddy.implementation.bind.annotation.Argument;
918
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
1019
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1120
import net.bytebuddy.implementation.bind.annotation.This;
1221

13-
import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey;
14-
1522
/**
1623
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest
1724
* createTest} method.
1825
*/
19-
@SuppressWarnings("squid:S1118")
2026
public class CreateTest {
2127

22-
private static final Map<String, Object> TARGET_TO_RUNNER = new ConcurrentHashMap<>();
23-
private static final Map<String, Object> RUNNER_TO_TARGET = new ConcurrentHashMap<>();
28+
private static final Map<Integer, Object> HASHCODE_TO_TARGET = new ConcurrentHashMap<>();
29+
private static final Map<String, FrameworkMethod> TARGET_TO_METHOD = new ConcurrentHashMap<>();
30+
private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> METHOD_DEPTH;
31+
private static final Function<Integer, DepthGauge> NEW_INSTANCE;
2432
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTest.class);
33+
34+
static {
35+
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() {
36+
@Override
37+
protected ConcurrentMap<Integer, DepthGauge> initialValue() {
38+
return new ConcurrentHashMap<>();
39+
}
40+
};
41+
NEW_INSTANCE = new Function<Integer, DepthGauge>() {
42+
@Override
43+
public DepthGauge apply(Integer input) {
44+
return new DepthGauge();
45+
}
46+
};
47+
}
2548

2649
/**
2750
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest createTest} method.
2851
*
2952
* @param runner target {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} object
53+
* @param method {@link FrameworkMethod} for which this test class instance is being created
3054
* @param proxy callable proxy for the intercepted method
3155
* @return {@code anything} - JUnit test class instance
3256
* @throws Exception {@code anything} (exception thrown by the intercepted method)
3357
*/
3458
@RuntimeType
35-
public static Object intercept(@This final Object runner,
59+
public static Object intercept(@This final Object runner, @Argument(0) final FrameworkMethod method,
3660
@SuperCall final Callable<?> proxy) throws Exception {
3761

62+
Integer hashCode = Objects.hash(runner, method);
63+
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), hashCode, NEW_INSTANCE);
64+
depthGauge.increaseDepth();
65+
3866
Object target = LifecycleHooks.callProxy(proxy);
39-
// apply parameter-based global timeout
40-
TimeoutUtils.applyTestTimeout(runner, target);
4167

42-
if (null == TARGET_TO_RUNNER.put(toMapKey(target), runner)) {
68+
if (0 == depthGauge.decreaseDepth()) {
69+
METHOD_DEPTH.remove();
4370
LOGGER.debug("testObjectCreated: {}", target);
44-
RUNNER_TO_TARGET.put(toMapKey(runner), target);
71+
TARGET_TO_METHOD.put(toMapKey(target), method);
72+
73+
// apply parameter-based global timeout
74+
TimeoutUtils.applyTestTimeout(runner, method, target);
75+
76+
// if notifier hasn't been initialized yet
77+
if ( ! EachTestNotifierInit.setTestTarget(runner, method, target)) {
78+
// store target for subsequent retrieval
79+
HASHCODE_TO_TARGET.put(hashCode, target);
80+
}
4581

4682
for (TestObjectWatcher watcher : LifecycleHooks.getObjectWatchers()) {
47-
watcher.testObjectCreated(target, runner);
83+
watcher.testObjectCreated(runner, method, target);
4884
}
4985
}
5086

5187
return target;
5288
}
5389

5490
/**
55-
* Get the class runner associated with the specified instance.
91+
* Get test class instance associated with the specified runner/method pair.
92+
* <p>
93+
* <b>NOTE</b>: This method can only be called once per target, as it removes the mapping.
5694
*
57-
* @param target instance of JUnit test class
58-
* @return {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} for specified instance
95+
* @param runner JUnit class runner
96+
* @param method JUnit framework method
97+
* @return target test class instance
5998
*/
60-
static Object getRunnerForTarget(Object target) {
61-
return TARGET_TO_RUNNER.get(toMapKey(target));
99+
static Object getTargetFor(Object runner, FrameworkMethod method) {
100+
return HASHCODE_TO_TARGET.remove(Objects.hash(runner, method));
62101
}
63102

64103
/**
65-
* Get the JUnit test class instance for the specified class runner.
104+
* Get the method for which the specified test class instance was created.
66105
*
67-
* @param runner JUnit class runner
68-
* @return JUnit test class instance for specified runner
106+
* @param target test class instance
107+
* @return JUnit framework method
69108
*/
70-
static Object getTargetForRunner(Object runner) {
71-
return RUNNER_TO_TARGET.get(toMapKey(runner));
109+
static FrameworkMethod getMethodFor(Object target) {
110+
return TARGET_TO_METHOD.get(toMapKey(target));
72111
}
73112

74113
/**
75-
* Release runner/target mappings.
114+
* Release the mappings associated with the specified runner/method/target group.
76115
*
77116
* @param runner JUnit class runner
117+
* @param method JUnit framework method
118+
* @param target test class instance
78119
*/
79-
static void releaseMappingsFor(Object runner) {
80-
Object target = RUNNER_TO_TARGET.remove(toMapKey(runner));
120+
static void releaseMappingsFor(Object runner, FrameworkMethod method, Object target) {
121+
HASHCODE_TO_TARGET.remove(Objects.hash(runner, method));
81122
if (target != null) {
82-
TARGET_TO_RUNNER.remove(toMapKey(target));
123+
TARGET_TO_METHOD.remove(toMapKey(target));
83124
}
84125
}
85126
}

0 commit comments

Comments
 (0)