Skip to content

Commit

Permalink
Fix event sequencing issues
Browse files Browse the repository at this point in the history
  • Loading branch information
sbabcoc authored Apr 7, 2021
1 parent aed26fd commit 17d04f2
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class AtomicTest {
private static final Pattern PARAM = Pattern.compile("[(\\[]");

public AtomicTest(Description description) {
this.runner = RunChildren.getThreadRunner();
this.runner = Run.getThreadRunner();
this.description = description;
this.particles = getParticles(runner, description);
this.identity = this.particles.get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static Object intercept(@This final Object runner, @Argument(0) final Fra
Object target = LifecycleHooks.callProxy(proxy);

if (0 == depthGauge.decreaseDepth()) {
METHOD_DEPTH.remove();
METHOD_DEPTH.get().remove(hashCode);
LOGGER.debug("testObjectCreated: {}", target);
TARGET_TO_METHOD.put(toMapKey(target), method);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,

if (description.isTest()) {
newAtomicTestFor(description);
Object runner = RunChildren.getThreadRunner();
Object runner = Run.getThreadRunner();
FrameworkMethod method = null;
List<Object> children = LifecycleHooks.invoke(runner, "getChildren");
for (Object child : children) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/nordstrom/automation/junit/Finished.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public class Finished {
*/
public static void intercept(@This final Object scheduler, @SuperCall final Callable<?> proxy) throws Exception {
LifecycleHooks.callProxy(proxy);
RunChildren.finished();
RunChild.finished();
}
}
14 changes: 3 additions & 11 deletions src/main/java/com/nordstrom/automation/junit/LifecycleHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument
final TypeDescription fireTestFinished = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.FireTestFinished").resolve();
final TypeDescription runReflectiveCall = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunReflectiveCall").resolve();
final TypeDescription finished = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Finished").resolve();
final TypeDescription schedule = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Schedule").resolve();
final TypeDescription runChildren = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChildren").resolve();
final TypeDescription runChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.RunChild").resolve();
final TypeDescription run = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.Run").resolve();
final TypeDescription describeChild = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.DescribeChild").resolve();
Expand All @@ -178,7 +176,6 @@ public static ClassFileTransformer installTransformer(Instrumentation instrument

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();
final SignatureToken runChildrenToken = new SignatureToken("runChildren", TypeDescription.VOID, Arrays.asList(runNotifier));
final SignatureToken runToken = new SignatureToken("run", TypeDescription.VOID, Arrays.asList(runNotifier));

final TypeDescription frameworkMethod = TypePool.Default.ofSystemLoader().describe("org.junit.runners.model.FrameworkMethod").resolve();
Expand Down Expand Up @@ -210,8 +207,7 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
@Override
public Builder<?> transform(Builder<?> builder, TypeDescription type,
ClassLoader classloader, JavaModule module) {
return builder.method(named("schedule")).intercept(MethodDelegation.to(schedule))
.method(named("finished")).intercept(MethodDelegation.to(finished))
return builder.method(named("finished")).intercept(MethodDelegation.to(finished))
.implement(Hooked.class);
}
})
Expand All @@ -221,7 +217,6 @@ public Builder<?> transform(Builder<?> builder, TypeDescription type,
public Builder<?> transform(Builder<?> builder, TypeDescription type,
ClassLoader classloader, JavaModule module) {
return builder.method(named("runChild")).intercept(MethodDelegation.to(runChild))
.method(hasSignature(runChildrenToken)).intercept(MethodDelegation.to(runChildren))
.method(hasSignature(runToken)).intercept(MethodDelegation.to(run))
.method(named("describeChild")).intercept(MethodDelegation.to(describeChild))
// NOTE: The 'methodBlock', 'createTest', and 'getTestRules' methods
Expand Down Expand Up @@ -300,7 +295,7 @@ public static AtomicTest getAtomicTestOf(Object target) {
* objects)
*/
public static Object getParentOf(Object child) {
return RunChildren.getParentOf(child);
return Run.getParentOf(child);
}

/**
Expand All @@ -320,7 +315,7 @@ public static Object getNotifierOf(final Object runner) {
* @return active {@code ParentRunner} object (may be ({@code null})
*/
public static Object getThreadRunner() {
return RunChildren.getThreadRunner();
return Run.getThreadRunner();
}

/**
Expand Down Expand Up @@ -354,8 +349,6 @@ public static Description describeChild(Object runner, Object child) {
return invoke(runner, "describeChild", child);
}



/**
* Get the {@link ReflectiveCallable} object for the specified description.
*
Expand Down Expand Up @@ -652,6 +645,5 @@ public T get(int index) {
public int size() {
return indexes.length;
}

}
}
18 changes: 9 additions & 9 deletions src/main/java/com/nordstrom/automation/junit/MethodBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@
* methodBlock} method.
*/
public class MethodBlock {
private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> METHOD_DEPTH;
private static final Function<Integer, DepthGauge> NEW_INSTANCE;
private static final ThreadLocal<ConcurrentMap<String, DepthGauge>> METHOD_DEPTH;
private static final Function<String, DepthGauge> NEW_INSTANCE;
private static final Map<String, Statement> RUNNER_TO_STATEMENT = new ConcurrentHashMap<>();

static {
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() {
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<String, DepthGauge>>() {
@Override
protected ConcurrentMap<Integer, DepthGauge> initialValue() {
protected ConcurrentMap<String, DepthGauge> initialValue() {
return new ConcurrentHashMap<>();
}
};
NEW_INSTANCE = new Function<Integer, DepthGauge>() {
NEW_INSTANCE = new Function<String, DepthGauge>() {
@Override
public DepthGauge apply(Integer input) {
public DepthGauge apply(String input) {
return new DepthGauge();
}
};
Expand All @@ -56,14 +56,14 @@ public DepthGauge apply(Integer input) {
public static Statement intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
@Argument(0) final FrameworkMethod method) throws Exception {

DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), runner.hashCode(), NEW_INSTANCE);
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), toMapKey(runner), NEW_INSTANCE);
depthGauge.increaseDepth();

Statement statement = (Statement) LifecycleHooks.callProxy(proxy);

// if at ground level
if (0 == depthGauge.decreaseDepth()) {
METHOD_DEPTH.remove();
METHOD_DEPTH.get().remove(toMapKey(runner));
try {
// get parent of test runner
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
Expand All @@ -78,7 +78,7 @@ public static Statement intercept(@This final Object runner, @SuperCall final Ca
@Override
public void evaluate() throws Throwable {
// attach class runner to thread
RunChildren.pushThreadRunner(threadRunner);
Run.pushThreadRunner(threadRunner);
}
};
}
Expand Down
160 changes: 156 additions & 4 deletions src/main/java/com/nordstrom/automation/junit/Run.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@

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

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EmptyStackException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.junit.runner.Description;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;

import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
Expand All @@ -14,7 +29,35 @@
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
*/
public class Run {
private static final ThreadLocal<Deque<Object>> RUNNER_STACK;
private static final ThreadLocal<ConcurrentMap<String, DepthGauge>> METHOD_DEPTH;
private static final Function<String, DepthGauge> NEW_INSTANCE;
private static final Set<String> START_NOTIFIED = new CopyOnWriteArraySet<>();
private static final Map<String, Object> CHILD_TO_PARENT = new ConcurrentHashMap<>();
private static final Map<String, RunNotifier> RUNNER_TO_NOTIFIER = new ConcurrentHashMap<>();
private static final Set<String> NOTIFIERS = new CopyOnWriteArraySet<>();
private static final Logger LOGGER = LoggerFactory.getLogger(Run.class);

static {
RUNNER_STACK = new ThreadLocal<Deque<Object>>() {
@Override
protected Deque<Object> initialValue() {
return new ArrayDeque<>();
}
};
METHOD_DEPTH = new ThreadLocal<ConcurrentMap<String, DepthGauge>>() {
@Override
protected ConcurrentMap<String, DepthGauge> initialValue() {
return new ConcurrentHashMap<>();
}
};
NEW_INSTANCE = new Function<String, DepthGauge>() {
@Override
public DepthGauge apply(String input) {
return new DepthGauge();
}
};
}

/**
* Interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
Expand All @@ -27,13 +70,24 @@ public class Run {
public static void intercept(@This final Object runner, @SuperCall final Callable<?> proxy,
@Argument(0) final RunNotifier notifier) throws Exception {

DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), toMapKey(runner), NEW_INSTANCE);

try {
RUNNER_TO_NOTIFIER.put(toMapKey(runner), notifier);
RunChildren.pushThreadRunner(runner);
if (0 == depthGauge.increaseDepth()) {
RUNNER_TO_NOTIFIER.put(toMapKey(runner), notifier);
pushThreadRunner(runner);
attachRunListeners(runner, notifier);
fireRunStarted(runner);
}

LifecycleHooks.callProxy(proxy);
} finally {
RunChildren.popThreadRunner();
RUNNER_TO_NOTIFIER.remove(toMapKey(runner));
if (0 == depthGauge.decreaseDepth()) {
METHOD_DEPTH.get().remove(toMapKey(runner));
fireRunFinished(runner);
popThreadRunner();
RUNNER_TO_NOTIFIER.remove(toMapKey(runner));
}
}
}

Expand All @@ -46,4 +100,102 @@ public static void intercept(@This final Object runner, @SuperCall final Callabl
static RunNotifier getNotifierOf(final Object runner) {
return RUNNER_TO_NOTIFIER.get(toMapKey(runner));
}

/**
* Get the parent runner that owns specified child runner or framework method.
*
* @param child {@code ParentRunner} or {@code FrameworkMethod} object
* @return {@code ParentRunner} object that owns the specified child ({@code null} for root objects)
*/
static Object getParentOf(final Object child) {
return CHILD_TO_PARENT.get(toMapKey(child));
}

/**
* Push the specified JUnit test runner onto the stack for the current thread.
*
* @param runner JUnit test runner
*/
static void pushThreadRunner(final Object runner) {
RUNNER_STACK.get().push(runner);
}

/**
* Pop the top JUnit test runner from the stack for the current thread.
*
* @return {@code ParentRunner} object
* @throws EmptyStackException if called outside the scope of an active runner
*/
static Object popThreadRunner() {
return RUNNER_STACK.get().pop();
}

/**
* Get the runner that owns the active thread context.
*
* @return active {@code ParentRunner} object
*/
static Object getThreadRunner() {
return RUNNER_STACK.get().peek();
}

/**
* Fire the {@link RunnerWatcher#runStarted(Object)} event for the specified runner.
* <p>
* <b>NOTE</b>: If {@code runStarted} for the specified runner has already been fired, do nothing.
* @param runner JUnit test runner
* @return {@code true} if the {@code runStarted} event was fired; otherwise {@code false}
*/
static boolean fireRunStarted(Object runner) {
if (START_NOTIFIED.add(toMapKey(runner))) {
for (Object child : (List<?>) LifecycleHooks.invoke(runner, "getChildren")) {
CHILD_TO_PARENT.put(toMapKey(child), runner);
}

LOGGER.debug("runStarted: {}", runner);
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
watcher.runStarted(runner);
}
return true;
}
return false;
}

/**
* Fire the {@link RunnerWatcher#runFinished(Object)} event for the specified runner.
*
* @param runner JUnit test runner
*/
static void fireRunFinished(Object runner) {
LOGGER.debug("runFinished: {}", runner);
for (RunnerWatcher watcher : LifecycleHooks.getRunnerWatchers()) {
watcher.runFinished(runner);
}

START_NOTIFIED.remove(toMapKey(runner));
for (Object child : (List<?>) LifecycleHooks.invoke(runner, "getChildren")) {
CHILD_TO_PARENT.remove(toMapKey(child));
}
}

/**
* Attach registered run listeners to the specified run notifier.
* <p>
* <b>NOTE</b>: If the specified run notifier has already been seen, do nothing.
*
* @param runner JUnit test runner
* @param notifier JUnit {@link RunNotifier} object
* @throws Exception if {@code run-started} notification
*/
static void attachRunListeners(Object runner, final RunNotifier notifier) throws Exception {
if (NOTIFIERS.add(toMapKey(notifier))) {
Description description = LifecycleHooks.invoke(runner, "getDescription");
for (RunListener listener : LifecycleHooks.getRunListeners()) {
// prevent potential duplicates
notifier.removeListener(listener);
notifier.addListener(listener);
listener.testRunStarted(description);
}
}
}
}
Loading

0 comments on commit 17d04f2

Please sign in to comment.