Skip to content

Commit

Permalink
Add reference checks; fix miscellaneous bugs (#109)
Browse files Browse the repository at this point in the history
Resolves #96
  • Loading branch information
sbabcoc authored Jun 24, 2021
1 parent 1040443 commit 1d45bf0
Show file tree
Hide file tree
Showing 35 changed files with 1,639 additions and 94 deletions.
26 changes: 22 additions & 4 deletions src/main/java/com/nordstrom/automation/junit/AtomicTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
Expand All @@ -14,6 +16,7 @@
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.theories.Theory;
import org.junit.runner.Description;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.TestClass;
Expand All @@ -38,7 +41,7 @@ public AtomicTest(Description description) {
this.runner = Run.getThreadRunner();
this.description = description;
this.particles = getParticles(runner, description);
this.identity = (particles.isEmpty()) ? null : particles.get(0);
this.identity = particles.isEmpty() ? null : particles.get(0);
}

/**
Expand Down Expand Up @@ -144,7 +147,21 @@ public static boolean isTheory(Description description) {
* @return {@code true} if this atomic test represents a test method; otherwise {@code false}
*/
public boolean isTest() {
return description.isTest();
return isTest(description);
}

/**
* Determine if the specified description represents a test method.
*
* @param description JUnit description object
* @return {@code true} if description represents a test method; otherwise {@code false}
*/
public static boolean isTest(Description description) {
for (Annotation annotation : description.getAnnotations()) {
if (annotation instanceof Test) return true;
if (annotation instanceof Theory) return true;
}
return false;
}

/**
Expand All @@ -161,7 +178,8 @@ public String toString() {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (o == null) return false;
if ( ! (o instanceof AtomicTest)) return false;
AtomicTest that = (AtomicTest) o;
return Objects.equals(runner, that.runner) &&
Objects.equals(identity, that.identity);
Expand All @@ -184,7 +202,7 @@ public int hashCode() {
*/
private List<FrameworkMethod> getParticles(Object runner, Description description) {
List<FrameworkMethod> particles = new ArrayList<>();
if (description.isTest()) {
if (isTest(description)) {
TestClass testClass = LifecycleHooks.getTestClassOf(runner);

String methodName = description.getMethodName();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/nordstrom/automation/junit/DepthGauge.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class DepthGauge {
*
* @return {@code true} if depth is 0; otherwise {@code false}
*/
public boolean atGroundLevel() {
public synchronized boolean atGroundLevel() {
return (0 == counter);
}

Expand All @@ -18,7 +18,7 @@ public boolean atGroundLevel() {
*
* @return current depth count
*/
public int currentDepth() {
public synchronized int currentDepth() {
return counter;
}

Expand Down
47 changes: 14 additions & 33 deletions src/main/java/com/nordstrom/automation/junit/DescribeChild.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;

import junitparams.JUnitParamsRunner;

/**
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#describeChild
* describeChild} method.
Expand All @@ -29,7 +27,6 @@ public class DescribeChild {
try {
field = Description.class.getDeclaredField("fUniqueId");
field.setAccessible(true);

} catch (NoSuchFieldException | SecurityException e) {
field = null;
}
Expand All @@ -49,14 +46,21 @@ public static Description intercept(@This final Object runner,
@SuperCall final Callable<?> proxy,
@Argument(0) final Object child) throws Exception {

Description description = LifecycleHooks.callProxy(proxy);
Description description = null;

try {
// invoke original implementation
description = LifecycleHooks.callProxy(proxy);
} catch (NullPointerException eaten) { // from JUnitParams
// JUnitParams choked on a configuration method
FrameworkMethod method = (FrameworkMethod) child;
// call JUnit API to create a standard method description
description = Description.createTestDescription(method.getDeclaringClass(),
method.getName(), method.getAnnotations());
}

// if running with JUnitParams
if (runner instanceof JUnitParamsRunner) {
// fix description, adding test class and annotations
description = augmentDescription(child, description);
// otherwise, if able to override [uniqueId] of test
} else if ((uniqueId != null) && description.isTest()) {
// if able to override [uniqueId] of test
if ((uniqueId != null) && AtomicTest.isTest(description)) {
try {
// get parent of test runner
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
Expand Down Expand Up @@ -113,27 +117,4 @@ static Description makeChildlessCopyOf(final Description description) {
return descripCopy;
}

/**
* Augment incomplete description created by JUnitParams runner.
* <p>
* <b>NOTE</b>: The description built by JUnitParams lack the test class and annotations.
*
* @param child child object of the test runner
* @param description JUnit description built by JUnitParams
* @return new augmented description object; if augmentation fails, returns original description
*/
private static Description augmentDescription(final Object child, final Description description) {
if ((child instanceof FrameworkMethod) && (uniqueId != null)) {
Description augmented = Description.createTestDescription(description.getTestClass(),
description.getMethodName(), ((FrameworkMethod) child).getAnnotations());
try {
uniqueId.set(augmented, uniqueId.get(description));
return augmented;
} catch (IllegalArgumentException | IllegalAccessException eaten) {
// nothing to do here
}
}
return description;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,
@Argument(1) final Description description) {

// if notifier for test
if (description.isTest()) {
if (AtomicTest.isTest(description)) {
// create new atomic test object
AtomicTest atomicTest = newAtomicTestFor(description);
// get current thread runner
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/nordstrom/automation/junit/JUnitConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public enum JUnitSettings implements SettingsCore.SettingsAPI {
*/
MAX_RETRY("junit.max.retry", "0");

private String propertyName;
private String defaultValue;
private final String propertyName;
private final String defaultValue;

JUnitSettings(String propertyName, String defaultValue) {
this.propertyName = propertyName;
Expand Down
62 changes: 59 additions & 3 deletions src/main/java/com/nordstrom/automation/junit/LifecycleHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
final TypeDescription getTestRules = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.GetTestRules").resolve();
final TypeDescription runWithCompleteAssignment = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunWithCompleteAssignment").resolve();
final TypeDescription nextCount = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.NextCount").resolve();
final TypeDescription parameterizedDescription = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.ParameterizedDescription").resolve();

final TypeDescription runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
final TypeDescription description = TypePool.Default.ofSystemLoader().describe("org.junit.runner.Description").resolve();
Expand Down Expand Up @@ -257,6 +258,15 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
.implement(Hooked.class);
}
})
.type(hasSuperType(named("junitparams.internal.ParametrizedDescription")))
.transform(new Transformer() {
@Override
public Builder<?> transform(Builder<?> builder, TypeDescription type,
ClassLoader classloader, JavaModule module) {
return builder.method(named("parametrizedDescription")).intercept(MethodDelegation.to(parameterizedDescription))
.implement(Hooked.class);
}
})
.installOn(instrumentation);
}

Expand Down Expand Up @@ -560,10 +570,56 @@ public static <T extends JUnitWatcher> Optional<T> getAttachedWatcher(Class<T> w
* @param listenerType listener type
* @return optional listener instance
*/
@SuppressWarnings("unchecked")
public static <T extends RunListener> Optional<T> getAttachedListener(Class<T> listenerType) {
for (RunListener listener : runListeners) {
if (listener.getClass() == listenerType) {
// search for specified type among loader-attached listeners
Optional<T> optListener = findListener(listenerType, runListeners);
// if specified type not found
if ( ! optListener.isPresent()) {
// search for specified type among API-attached listeners
optListener = findListener(listenerType, getAttachedListeners());
}

return optListener;
}

/**
* Retrieve run listener collection from active notifier.
*
* @return run listener collection
*/
private static List<RunListener> getAttachedListeners() {
// get active thread runner
Object runner = getThreadRunner();
// if runner acquired
if (runner != null) {
// get active run notifier
Object notifier = getNotifierOf(runner);
// if notifier acquired
if (notifier != null) {
try {
// get attached run listener collection
return getFieldValue(notifier, "listeners");
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
// nothing to do here
}
}
}
// default to empty list
return new ArrayList<>();
}

/**
* Get reference to an instance of the specified listener type from the supplied list.
*
* @param <T> listener type
* @param type listener type
* @param list listener list
* @return optional listener instance
*/
@SuppressWarnings("unchecked")
private static <T extends RunListener> Optional<T> findListener(Class<T> type, List<RunListener> list) {
for (RunListener listener : list) {
if (listener.getClass() == type) {
return Optional.of((T) listener);
}
}
Expand Down
83 changes: 47 additions & 36 deletions src/main/java/com/nordstrom/automation/junit/MutableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@

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

private static final String DECLARED_ANNOTATIONS = "declaredAnnotations";

private Class<? extends Throwable> expected;
private long timeout;
private final Class<? extends Throwable> expected;
private final long timeout;

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

/**
* Constructor: Populate the fields of this object from the parameters of the specified {@link Test &#64;Test}
* annotation.
*
* @param annotation {@link Test &#64;Test} annotation specifying desired parameters
* @param timeout timeout interval in milliseconds
*/
private MutableTest(Test annotation, long timeout) {
this.expected = annotation.expected();
this.timeout = timeout;
}

/**
* {@inheritDoc}
*/
Expand All @@ -48,48 +60,19 @@ public Class<? extends Throwable> expected() {
return expected;
}

/**
* Specify the class of exception that the annotated test method is expected to throw. If you need to verify the
* message or properties of the exception, use the {@link ExpectedException} rule instead.
*
* @param expected expected exception class
* @return this mutable annotation object
*/
public MutableTest setExpected(Class<? extends Throwable> expected) {
this.expected = expected;
return this;
}

@Override
public long timeout() {
return timeout;
}

/**
* Specify maximum test execution interval in milliseconds. If execution time exceeds this interval, the test will
* fail with {@link TestTimedOutException}.
* <p>
* <b>THREAD SAFETY WARNING</b>: Test methods with a timeout parameter are run in a thread other than the thread
* which runs the fixture's {@code @Before} and {@code @After} methods. This may yield different behavior
* for code that is not thread safe when compared to the same test method without a timeout parameter. <b>Consider
* using the {@link org.junit.rules.Timeout} rule instead</b>, which ensures a test method is run on the same
* thread as the fixture's {@code @Before} and {@code @After} methods.
*
* @param timeout timeout interval in milliseconds
* @return this mutable annotation object
*/
public MutableTest setTimeout(long timeout) {
this.timeout = timeout;
return this;
}

/**
* Create a {@link Test &#64;Test} annotation proxy for the specified test method.
*
* @param testMethod test method to which {@code @Test} annotation proxy will be attached
* @param timeout timeout interval in milliseconds
* @return mutable proxy for {@code @Test} annotation
*/
public static MutableTest proxyFor(Method testMethod) {
public static MutableTest proxyFor(Method testMethod, long timeout) {
Test declared = testMethod.getAnnotation(Test.class);
if (declared instanceof MutableTest) {
return (MutableTest) declared;
Expand All @@ -102,7 +85,7 @@ public static MutableTest proxyFor(Method testMethod) {
@SuppressWarnings("unchecked")
Map<Class<? extends Annotation>, Annotation> map =
(Map<Class<? extends Annotation>, Annotation>) field.get(testMethod);
MutableTest mutable = new MutableTest(declared);
MutableTest mutable = new MutableTest(declared, timeout);
map.put(Test.class, mutable);
return mutable;
} catch (IllegalArgumentException | IllegalAccessException e) {
Expand All @@ -115,4 +98,32 @@ public static MutableTest proxyFor(Method testMethod) {
}
throw new IllegalArgumentException("Specified method is not a JUnit @Test: " + testMethod);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((expected == null) ? 0 : expected.hashCode());
result = prime * result + (int) (timeout ^ (timeout >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if ( ! (obj instanceof MutableTest))
return false;
MutableTest other = (MutableTest) obj;
if (expected == null) {
if (other.expected != null)
return false;
} else if (!expected.equals(other.expected))
return false;
if (timeout != other.timeout)
return false;
return true;
}
}
Loading

0 comments on commit 1d45bf0

Please sign in to comment.