Skip to content

Commit 1d45bf0

Browse files
authored
Add reference checks; fix miscellaneous bugs (#109)
Resolves #96
1 parent 1040443 commit 1d45bf0

35 files changed

+1639
-94
lines changed

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

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

5+
import java.lang.annotation.Annotation;
56
import java.util.ArrayList;
7+
import java.util.Collection;
68
import java.util.List;
79
import java.util.Objects;
810
import java.util.regex.Matcher;
@@ -14,6 +16,7 @@
1416
import org.junit.BeforeClass;
1517
import org.junit.Ignore;
1618
import org.junit.Test;
19+
import org.junit.experimental.theories.Theory;
1720
import org.junit.runner.Description;
1821
import org.junit.runners.model.FrameworkMethod;
1922
import org.junit.runners.model.TestClass;
@@ -38,7 +41,7 @@ public AtomicTest(Description description) {
3841
this.runner = Run.getThreadRunner();
3942
this.description = description;
4043
this.particles = getParticles(runner, description);
41-
this.identity = (particles.isEmpty()) ? null : particles.get(0);
44+
this.identity = particles.isEmpty() ? null : particles.get(0);
4245
}
4346

4447
/**
@@ -144,7 +147,21 @@ public static boolean isTheory(Description description) {
144147
* @return {@code true} if this atomic test represents a test method; otherwise {@code false}
145148
*/
146149
public boolean isTest() {
147-
return description.isTest();
150+
return isTest(description);
151+
}
152+
153+
/**
154+
* Determine if the specified description represents a test method.
155+
*
156+
* @param description JUnit description object
157+
* @return {@code true} if description represents a test method; otherwise {@code false}
158+
*/
159+
public static boolean isTest(Description description) {
160+
for (Annotation annotation : description.getAnnotations()) {
161+
if (annotation instanceof Test) return true;
162+
if (annotation instanceof Theory) return true;
163+
}
164+
return false;
148165
}
149166

150167
/**
@@ -161,7 +178,8 @@ public String toString() {
161178
@Override
162179
public boolean equals(Object o) {
163180
if (this == o) return true;
164-
if (o == null || getClass() != o.getClass()) return false;
181+
if (o == null) return false;
182+
if ( ! (o instanceof AtomicTest)) return false;
165183
AtomicTest that = (AtomicTest) o;
166184
return Objects.equals(runner, that.runner) &&
167185
Objects.equals(identity, that.identity);
@@ -184,7 +202,7 @@ public int hashCode() {
184202
*/
185203
private List<FrameworkMethod> getParticles(Object runner, Description description) {
186204
List<FrameworkMethod> particles = new ArrayList<>();
187-
if (description.isTest()) {
205+
if (isTest(description)) {
188206
TestClass testClass = LifecycleHooks.getTestClassOf(runner);
189207

190208
String methodName = description.getMethodName();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class DepthGauge {
99
*
1010
* @return {@code true} if depth is 0; otherwise {@code false}
1111
*/
12-
public boolean atGroundLevel() {
12+
public synchronized boolean atGroundLevel() {
1313
return (0 == counter);
1414
}
1515

@@ -18,7 +18,7 @@ public boolean atGroundLevel() {
1818
*
1919
* @return current depth count
2020
*/
21-
public int currentDepth() {
21+
public synchronized int currentDepth() {
2222
return counter;
2323
}
2424

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

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import net.bytebuddy.implementation.bind.annotation.SuperCall;
1515
import net.bytebuddy.implementation.bind.annotation.This;
1616

17-
import junitparams.JUnitParamsRunner;
18-
1917
/**
2018
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#describeChild
2119
* describeChild} method.
@@ -29,7 +27,6 @@ public class DescribeChild {
2927
try {
3028
field = Description.class.getDeclaredField("fUniqueId");
3129
field.setAccessible(true);
32-
3330
} catch (NoSuchFieldException | SecurityException e) {
3431
field = null;
3532
}
@@ -49,14 +46,21 @@ public static Description intercept(@This final Object runner,
4946
@SuperCall final Callable<?> proxy,
5047
@Argument(0) final Object child) throws Exception {
5148

52-
Description description = LifecycleHooks.callProxy(proxy);
49+
Description description = null;
50+
51+
try {
52+
// invoke original implementation
53+
description = LifecycleHooks.callProxy(proxy);
54+
} catch (NullPointerException eaten) { // from JUnitParams
55+
// JUnitParams choked on a configuration method
56+
FrameworkMethod method = (FrameworkMethod) child;
57+
// call JUnit API to create a standard method description
58+
description = Description.createTestDescription(method.getDeclaringClass(),
59+
method.getName(), method.getAnnotations());
60+
}
5361

54-
// if running with JUnitParams
55-
if (runner instanceof JUnitParamsRunner) {
56-
// fix description, adding test class and annotations
57-
description = augmentDescription(child, description);
58-
// otherwise, if able to override [uniqueId] of test
59-
} else if ((uniqueId != null) && description.isTest()) {
62+
// if able to override [uniqueId] of test
63+
if ((uniqueId != null) && AtomicTest.isTest(description)) {
6064
try {
6165
// get parent of test runner
6266
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
@@ -113,27 +117,4 @@ static Description makeChildlessCopyOf(final Description description) {
113117
return descripCopy;
114118
}
115119

116-
/**
117-
* Augment incomplete description created by JUnitParams runner.
118-
* <p>
119-
* <b>NOTE</b>: The description built by JUnitParams lack the test class and annotations.
120-
*
121-
* @param child child object of the test runner
122-
* @param description JUnit description built by JUnitParams
123-
* @return new augmented description object; if augmentation fails, returns original description
124-
*/
125-
private static Description augmentDescription(final Object child, final Description description) {
126-
if ((child instanceof FrameworkMethod) && (uniqueId != null)) {
127-
Description augmented = Description.createTestDescription(description.getTestClass(),
128-
description.getMethodName(), ((FrameworkMethod) child).getAnnotations());
129-
try {
130-
uniqueId.set(augmented, uniqueId.get(description));
131-
return augmented;
132-
} catch (IllegalArgumentException | IllegalAccessException eaten) {
133-
// nothing to do here
134-
}
135-
}
136-
return description;
137-
}
138-
139120
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,
3636
@Argument(1) final Description description) {
3737

3838
// if notifier for test
39-
if (description.isTest()) {
39+
if (AtomicTest.isTest(description)) {
4040
// create new atomic test object
4141
AtomicTest atomicTest = newAtomicTestFor(description);
4242
// get current thread runner

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ public enum JUnitSettings implements SettingsCore.SettingsAPI {
4848
*/
4949
MAX_RETRY("junit.max.retry", "0");
5050

51-
private String propertyName;
52-
private String defaultValue;
51+
private final String propertyName;
52+
private final String defaultValue;
5353

5454
JUnitSettings(String propertyName, String defaultValue) {
5555
this.propertyName = propertyName;

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
174174
final TypeDescription getTestRules = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.GetTestRules").resolve();
175175
final TypeDescription runWithCompleteAssignment = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunWithCompleteAssignment").resolve();
176176
final TypeDescription nextCount = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.NextCount").resolve();
177+
final TypeDescription parameterizedDescription = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.ParameterizedDescription").resolve();
177178

178179
final TypeDescription runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
179180
final TypeDescription description = TypePool.Default.ofSystemLoader().describe("org.junit.runner.Description").resolve();
@@ -257,6 +258,15 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
257258
.implement(Hooked.class);
258259
}
259260
})
261+
.type(hasSuperType(named("junitparams.internal.ParametrizedDescription")))
262+
.transform(new Transformer() {
263+
@Override
264+
public Builder<?> transform(Builder<?> builder, TypeDescription type,
265+
ClassLoader classloader, JavaModule module) {
266+
return builder.method(named("parametrizedDescription")).intercept(MethodDelegation.to(parameterizedDescription))
267+
.implement(Hooked.class);
268+
}
269+
})
260270
.installOn(instrumentation);
261271
}
262272

@@ -560,10 +570,56 @@ public static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> w
560570
* @param listenerType listener type
561571
* @return optional listener instance
562572
*/
563-
@SuppressWarnings("unchecked")
564573
public static <T extends RunListener> Optional<T> getAttachedListener(Class<T> listenerType) {
565-
for (RunListener listener : runListeners) {
566-
if (listener.getClass() == listenerType) {
574+
// search for specified type among loader-attached listeners
575+
Optional<T> optListener = findListener(listenerType, runListeners);
576+
// if specified type not found
577+
if ( ! optListener.isPresent()) {
578+
// search for specified type among API-attached listeners
579+
optListener = findListener(listenerType, getAttachedListeners());
580+
}
581+
582+
return optListener;
583+
}
584+
585+
/**
586+
* Retrieve run listener collection from active notifier.
587+
*
588+
* @return run listener collection
589+
*/
590+
private static List<RunListener> getAttachedListeners() {
591+
// get active thread runner
592+
Object runner = getThreadRunner();
593+
// if runner acquired
594+
if (runner != null) {
595+
// get active run notifier
596+
Object notifier = getNotifierOf(runner);
597+
// if notifier acquired
598+
if (notifier != null) {
599+
try {
600+
// get attached run listener collection
601+
return getFieldValue(notifier, "listeners");
602+
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
603+
// nothing to do here
604+
}
605+
}
606+
}
607+
// default to empty list
608+
return new ArrayList<>();
609+
}
610+
611+
/**
612+
* Get reference to an instance of the specified listener type from the supplied list.
613+
*
614+
* @param <T> listener type
615+
* @param type listener type
616+
* @param list listener list
617+
* @return optional listener instance
618+
*/
619+
@SuppressWarnings("unchecked")
620+
private static <T extends RunListener> Optional<T> findListener(Class<T> type, List<RunListener> list) {
621+
for (RunListener listener : list) {
622+
if (listener.getClass() == type) {
567623
return Optional.of((T) listener);
568624
}
569625
}

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

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212

1313
/**
1414
* This class is a mutable implementation of the {@link Test &#64;Test} annotation interface. It includes a static
15-
* {@link #proxyFor(Method)} method that replaces the immutable annotation attached to a JUnit test method with an
16-
* instance of this class to apply the global test timeout.
15+
* {@link #proxyFor(Method, long)} method that replaces the immutable annotation attached to a JUnit test method with
16+
* an instance of this class to apply the global test timeout.
1717
*/
1818
@Ignore
1919
@SuppressWarnings("all")
2020
public class MutableTest implements Test {
2121

2222
private static final String DECLARED_ANNOTATIONS = "declaredAnnotations";
2323

24-
private Class<? extends Throwable> expected;
25-
private long timeout;
24+
private final Class<? extends Throwable> expected;
25+
private final long timeout;
2626

2727
/**
2828
* Constructor: Populate the fields of this object from the parameters of the specified {@link Test &#64;Test}
@@ -35,6 +35,18 @@ protected MutableTest(Test annotation) {
3535
this.timeout = annotation.timeout();
3636
}
3737

38+
/**
39+
* Constructor: Populate the fields of this object from the parameters of the specified {@link Test &#64;Test}
40+
* annotation.
41+
*
42+
* @param annotation {@link Test &#64;Test} annotation specifying desired parameters
43+
* @param timeout timeout interval in milliseconds
44+
*/
45+
private MutableTest(Test annotation, long timeout) {
46+
this.expected = annotation.expected();
47+
this.timeout = timeout;
48+
}
49+
3850
/**
3951
* {@inheritDoc}
4052
*/
@@ -48,48 +60,19 @@ public Class<? extends Throwable> expected() {
4860
return expected;
4961
}
5062

51-
/**
52-
* Specify the class of exception that the annotated test method is expected to throw. If you need to verify the
53-
* message or properties of the exception, use the {@link ExpectedException} rule instead.
54-
*
55-
* @param expected expected exception class
56-
* @return this mutable annotation object
57-
*/
58-
public MutableTest setExpected(Class<? extends Throwable> expected) {
59-
this.expected = expected;
60-
return this;
61-
}
62-
6363
@Override
6464
public long timeout() {
6565
return timeout;
6666
}
6767

68-
/**
69-
* Specify maximum test execution interval in milliseconds. If execution time exceeds this interval, the test will
70-
* fail with {@link TestTimedOutException}.
71-
* <p>
72-
* <b>THREAD SAFETY WARNING</b>: Test methods with a timeout parameter are run in a thread other than the thread
73-
* which runs the fixture's {@code @Before} and {@code @After} methods. This may yield different behavior
74-
* for code that is not thread safe when compared to the same test method without a timeout parameter. <b>Consider
75-
* using the {@link org.junit.rules.Timeout} rule instead</b>, which ensures a test method is run on the same
76-
* thread as the fixture's {@code @Before} and {@code @After} methods.
77-
*
78-
* @param timeout timeout interval in milliseconds
79-
* @return this mutable annotation object
80-
*/
81-
public MutableTest setTimeout(long timeout) {
82-
this.timeout = timeout;
83-
return this;
84-
}
85-
8668
/**
8769
* Create a {@link Test &#64;Test} annotation proxy for the specified test method.
8870
*
8971
* @param testMethod test method to which {@code @Test} annotation proxy will be attached
72+
* @param timeout timeout interval in milliseconds
9073
* @return mutable proxy for {@code @Test} annotation
9174
*/
92-
public static MutableTest proxyFor(Method testMethod) {
75+
public static MutableTest proxyFor(Method testMethod, long timeout) {
9376
Test declared = testMethod.getAnnotation(Test.class);
9477
if (declared instanceof MutableTest) {
9578
return (MutableTest) declared;
@@ -102,7 +85,7 @@ public static MutableTest proxyFor(Method testMethod) {
10285
@SuppressWarnings("unchecked")
10386
Map<Class<? extends Annotation>, Annotation> map =
10487
(Map<Class<? extends Annotation>, Annotation>) field.get(testMethod);
105-
MutableTest mutable = new MutableTest(declared);
88+
MutableTest mutable = new MutableTest(declared, timeout);
10689
map.put(Test.class, mutable);
10790
return mutable;
10891
} catch (IllegalArgumentException | IllegalAccessException e) {
@@ -115,4 +98,32 @@ public static MutableTest proxyFor(Method testMethod) {
11598
}
11699
throw new IllegalArgumentException("Specified method is not a JUnit @Test: " + testMethod);
117100
}
101+
102+
@Override
103+
public int hashCode() {
104+
final int prime = 31;
105+
int result = 1;
106+
result = prime * result + ((expected == null) ? 0 : expected.hashCode());
107+
result = prime * result + (int) (timeout ^ (timeout >>> 32));
108+
return result;
109+
}
110+
111+
@Override
112+
public boolean equals(Object obj) {
113+
if (this == obj)
114+
return true;
115+
if (obj == null)
116+
return false;
117+
if ( ! (obj instanceof MutableTest))
118+
return false;
119+
MutableTest other = (MutableTest) obj;
120+
if (expected == null) {
121+
if (other.expected != null)
122+
return false;
123+
} else if (!expected.equals(other.expected))
124+
return false;
125+
if (timeout != other.timeout)
126+
return false;
127+
return true;
128+
}
118129
}

0 commit comments

Comments
 (0)