com.nordstrom.automation.junit.LifecycleHooks Maven / Gradle / Ivy
Show all versions of junit-foundation Show documentation
package com.nordstrom.automation.junit;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.runner.Description;
import org.junit.runner.notification.RunListener;
import org.junit.runners.model.TestClass;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.nordstrom.common.base.UncheckedThrow;
import com.nordstrom.common.file.PathUtils.ReportsDirectory;
/**
* This class implements the hooks and utility methods that activate the core functionality of JUnit Foundation.
*/
public class LifecycleHooks {
private static JUnitConfig config;
private static final List watchers;
private static final List runListeners;
private static final List runWatchers;
private static final List runnerWatchers;
private static final List objectWatchers;
private static final List> methodWatchers;
private LifecycleHooks() {
throw new AssertionError("LifecycleHooks is a static utility class that cannot be instantiated");
}
/**
* This static initializer installs a shutdown hook for each specified listener. It also rebases the ParentRunner
* and BlockJUnit4ClassRunner classes to enable the core functionality of JUnit Foundation.
*/
static {
WatcherClassifier classifier = new WatcherClassifier();
for (JUnitWatcher watcher : ServiceLoader.load(JUnitWatcher.class)) {
classifier.add(watcher);
}
for (RunListener listener : ServiceLoader.load(RunListener.class)) {
classifier.add(listener);
}
for (ShutdownListener watcher : ServiceLoader.load(ShutdownListener.class)) {
classifier.add(watcher);
}
for (RunWatcher watcher : ServiceLoader.load(RunWatcher.class)) {
classifier.add(watcher);
}
for (RunnerWatcher watcher : ServiceLoader.load(RunnerWatcher.class)) {
classifier.add(watcher);
}
for (TestObjectWatcher watcher : ServiceLoader.load(TestObjectWatcher.class)) {
classifier.add(watcher);
}
for (MethodWatcher> watcher : ServiceLoader.load(MethodWatcher.class)) {
classifier.add(watcher);
}
watchers = classifier.watchers;
runListeners = classifier.listeners;
runWatchers = new WatcherList<>(classifier.runWatcherIndexes);
runnerWatchers = new WatcherList<>(classifier.runnerWatcherIndexes);
objectWatchers = new WatcherList<>(classifier.objectWatcherIndexes);
methodWatchers = new WatcherList<>(classifier.methodWatcherIndexes);
}
private static class WatcherClassifier {
int i = 0;
List watchers = new ArrayList<>();
List> watcherClasses = new ArrayList<>();
List listeners = new ArrayList<>();
List> listenerClasses = new ArrayList<>();
List runWatcherIndexes = new ArrayList<>();
List runnerWatcherIndexes = new ArrayList<>();
List objectWatcherIndexes = new ArrayList<>();
List methodWatcherIndexes = new ArrayList<>();
boolean add(JUnitWatcher watcher) {
if ( ! watcherClasses.contains(watcher.getClass())) {
watchers.add(watcher);
watcherClasses.add(watcher.getClass());
if (watcher instanceof RunListener) add((RunListener) watcher);
if (watcher instanceof ShutdownListener) {
Runtime.getRuntime().addShutdownHook(getShutdownHook((ShutdownListener) watcher));
}
if (watcher instanceof RunWatcher) runWatcherIndexes.add(i);
if (watcher instanceof RunnerWatcher) runnerWatcherIndexes.add(i);
if (watcher instanceof TestObjectWatcher) objectWatcherIndexes.add(i);
if (watcher instanceof MethodWatcher) methodWatcherIndexes.add(i);
i++;
return true;
}
return false;
}
boolean add(RunListener listener) {
if ( ! listenerClasses.contains(listener.getClass())) {
listeners.add(listener);
listenerClasses.add(listener.getClass());
return true;
}
return false;
}
}
/**
* Create a {@link Thread} object that encapsulated the specified shutdown listener.
*
* @param listener shutdown listener object
* @return shutdown listener thread object
*/
static Thread getShutdownHook(final ShutdownListener listener) {
return new Thread() {
@Override
public void run() {
listener.onShutdown();
}
};
}
/**
* Get the configuration object for JUnit Foundation.
*
* @return JUnit Foundation configuration object
*/
static synchronized JUnitConfig getConfig() {
if (config == null) {
config = JUnitConfig.getConfig();
}
return config;
}
/**
* Get the atomic test associated with the specified instance.
*
* @param target instance of JUnit test class
* @return {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} for specified instance
*/
public static AtomicTest getAtomicTestOf(Object target) {
return EachTestNotifierInit.getAtomicTestOf(target);
}
/**
* 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)
*/
public static Object getParentOf(Object child) {
return Run.getParentOf(child);
}
/**
* Get the run notifier associated with the specified parent runner.
*
* @param runner JUnit parent runner
* @return {@link org.junit.runner.notification.RunNotifier RunNotifier} object for the specified parent runner
* (may be {@code null})
*/
public static Object getNotifierOf(final Object runner) {
return Run.getNotifierOf(runner);
}
/**
* Get the runner that owns the active thread context.
*
* @return active {@code ParentRunner} object (may be ({@code null})
*/
public static Object getThreadRunner() {
return Run.getThreadRunner();
}
/**
* Get the test class object associated with the specified parent runner.
*
* @param runner target {@link org.junit.runners.ParentRunner ParentRunner} object
* @return {@link TestClass} associated with specified runner
*/
public static TestClass getTestClassOf(Object runner) {
return invoke(runner, "getTestClass");
}
/**
* Get the atomic test object for the specified method description.
*
* @param description JUnit method description
* @return {@link AtomicTest} object (may be {@code null})
*/
public static AtomicTest getAtomicTestOf(Description description) {
return EachTestNotifierInit.getAtomicTestOf(description);
}
/**
* Get the test class instance for the specified method description.
*
* @param description JUnit method description
* @return test class instance (may be {@code null})
*/
public static Object getTargetOf(Description description) {
return EachTestNotifierInit.getTargetOf(description);
}
/**
* Get the description for the specified child object.
*
* @param runner target {@link org.junit.runners.ParentRunner ParentRunner} object
* @param child child object
* @return {@link Description} for the specified framework method (may be {@code null})
*/
public static Description describeChild(Object runner, Object child) {
if (runner != null && child != null) {
Class> runnerType = getSupportedType(runner);
if (runnerType != null && runnerType.isInstance(child)) {
return invoke(runner, "describeChild", child);
}
}
return null;
}
/**
* Get the type of children supported by the specified runner.
*
* @param runner target {@link org.junit.runners.ParentRunner ParentRunner} object
* @return supported child type; {@code null} if undetermined
*/
private static Class> getSupportedType(Object runner) {
for (Method method : runner.getClass().getDeclaredMethods()) {
if ("describeChild".equals(method.getName())) {
Class>[] paramTypes = method.getParameterTypes();
if ((paramTypes.length == 1) && (paramTypes[0] != Object.class)) {
return paramTypes[0];
}
}
}
return null;
}
/**
* Get the {@link ReflectiveCallable} object for the specified description.
*
* @param description JUnit method description
* @return ReflectiveCallable object (may be {@code null})
*/
public static ReflectiveCallable getCallableOf(Description description) {
return RunReflectiveCall.getCallableOf(description);
}
/**
* Synthesize a {@link ReflectiveCallable} closure with the specified parameters.
*
* @param method {@link Method} object to be invoked
* @param target test class instance to target
* @param params method invocation parameters
* @return ReflectiveCallable object as specified
*/
public static ReflectiveCallable encloseCallable(final Method method, final Object target, final Object... params) {
return new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return method.invoke(target, params);
}
};
}
/**
* Get class of specified test class instance.
*
* @param instance test class instance
* @return class of test class instance
*/
public static Class> getInstanceClass(Object instance) {
Class> clazz = instance.getClass();
return (instance instanceof Hooked) ? clazz.getSuperclass() : clazz;
}
/**
* Get fully-qualified name to use for hooked test class.
*
* @param testObj test class object being hooked
* @return fully-qualified name for hooked subclass
*/
static String getSubclassName(Object testObj) {
Class> testClass = testObj.getClass();
String testClassName = testClass.getSimpleName();
String testPackageName = testClass.getPackage().getName();
ReportsDirectory constant = ReportsDirectory.fromObject(testObj);
switch (constant) {
case FAILSAFE_2:
case FAILSAFE_3:
case SUREFIRE_2:
case SUREFIRE_3:
case SUREFIRE_4:
return testPackageName + ".Hooked" + testClassName;
default:
return testClass.getCanonicalName() + "Hooked";
}
}
/**
* Invoke the named method with the specified parameters on the specified target object.
*
* @param method return type
* @param target target object
* @param methodName name of the desired method
* @param parameters parameters for the method invocation
* @return result of method invocation
*/
@SuppressWarnings("unchecked")
static T invoke(Object target, String methodName, Object... parameters) {
try {
return (T) MethodUtils.invokeMethod(target, true, methodName, parameters);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw UncheckedThrow.throwUnchecked(e);
}
}
/**
* Get the specified field of the supplied object.
*
* @param target target object
* @param name field name
* @return {@link Field} object for the requested field
* @throws NoSuchFieldException if a field with the specified name is not found
* @throws SecurityException if the request is denied
*/
static Field getDeclaredField(Object target, String name) throws NoSuchFieldException {
Throwable thrown = null;
for (Class> current = target.getClass(); current != null; current = current.getSuperclass()) {
try {
return current.getDeclaredField(name);
} catch (NoSuchFieldException e) {
thrown = e;
} catch (SecurityException e) {
thrown = e;
break;
}
}
throw UncheckedThrow.throwUnchecked(thrown);
}
/**
* Get the value of the specified field from the supplied object.
*
* @param field value type
* @param target target object
* @param name field name
* @return {@code anything} - the value of the specified field in the supplied object
* @throws IllegalAccessException if the {@code Field} object is enforcing access control for an inaccessible field
* @throws NoSuchFieldException if a field with the specified name is not found
* @throws SecurityException if the request is denied
*/
@SuppressWarnings("unchecked")
public static T getFieldValue(Object target, String name) throws IllegalAccessException, NoSuchFieldException, SecurityException {
Field field = getDeclaredField(target, name);
field.setAccessible(true);
return (T) field.get(target);
}
/**
* Invoke an intercepted method through its callable proxy.
*
* NOTE: If the invoked method throws an exception, this method re-throws the original exception.
*
* @param proxy callable proxy for the intercepted method
* @return {@code anything} - value returned by the intercepted method
* @throws Exception {@code anything} (exception thrown by the intercepted method)
*/
@SuppressWarnings("unchecked")
static T callProxy(final Callable> proxy) throws Exception {
try {
return (T) proxy.call();
} catch (InvocationTargetException e) {
throw UncheckedThrow.throwUnchecked(e.getCause());
}
}
/**
* Get reference to an instance of the specified watcher type.
*
* @param watcher type
* @param watcherType watcher type
* @return optional watcher instance
*/
@SuppressWarnings("unchecked")
public static Optional getAttachedWatcher(Class watcherType) {
for (JUnitWatcher watcher : watchers) {
if (watcher.getClass() == watcherType) {
return Optional.of((T) watcher);
}
}
return Optional.absent();
}
/**
* Get reference to an instance of the specified listener type.
*
* @param listener type
* @param listenerType listener type
* @return optional listener instance
*/
public static Optional getAttachedListener(Class listenerType) {
// search for specified type among loader-attached listeners
Optional 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 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 listener type
* @param type listener type
* @param list listener list
* @return optional listener instance
*/
@SuppressWarnings("unchecked")
private static Optional findListener(Class type, List list) {
for (RunListener listener : list) {
if (listener.getClass() == type) {
return Optional.of((T) listener);
}
}
return Optional.absent();
}
/**
* If the specified key is not already associated with a value (or is mapped to {@code null}), attempts
* to compute its value using the given mapping function and enters it into this map unless {@code null}.
*
* @param data type of map keys
* @param data type of map values
* @param map concurrent map to be manipulated
* @param key key with which the specified value is to be associated
* @param fun the function to compute a value
* @return the current (existing or computed) value associated with the specified key;
* {@code null} if the computed value is {@code null}
*/
static T computeIfAbsent(ConcurrentMap map, K key, Function fun) {
T val = map.get(key);
if (val == null) {
T obj = fun.apply(key);
val = (val = map.putIfAbsent(key, obj)) == null ? obj : val;
}
return val;
}
/**
* Get the list of attached {@link RunListener} objects.
*
* @return run listener list
*/
static List getRunListeners() {
return runListeners;
}
/**
* Get the list of attached {@link RunWatcher} objects.
*
* @return run watcher list
*/
static List getRunWatchers() {
return runWatchers;
}
/**
* Get the list of attached {@link RunnerWatcher} objects.
*
* @return runner watcher list
*/
static List getRunnerWatchers() {
return runnerWatchers;
}
/**
* Get the list of attached {@link TestObjectWatcher} objects.
*
* @return test object watcher list
*/
static List getObjectWatchers() {
return objectWatchers;
}
/**
* Get the list of attached {@link MethodWatcher} objects.
*
* @return method watcher list
*/
static List> getMethodWatchers() {
return methodWatchers;
}
/**
* Create a unique map key string to represent the specified object.
*
* NOTE: The string returned by this method matches the output of
* the default {@link Object#toString()} implementation.
*
* @param obj target object
* @return map key string
*/
static String toMapKey(Object obj) {
return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj));
}
/**
* This class encapsulates the process of retrieving watcher objects of the target type from the collection of all
* attached watcher objects. This is a private nested class that directly accesses the main collection. It is also
* unmodifiable. Any attempts to alter the collection will trigger an {@link UnsupportedOperationException}.
*
* @param subclass of {@link JUnitWatcher} object supplied by this instance
*/
private static class WatcherList extends AbstractList {
private int[] indexes;
/**
* Constructor for a list of watcher objects of the target type retrieved from the collection of all attached
* {@link JUnitWatcher} objects.
*
* @param indexes indexes of watchers of the requisite type in the main collection
*/
private WatcherList(List indexes) {
int i = 0;
this.indexes = new int[indexes.size()];
for (int index : indexes) {
this.indexes[i++] = index;
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) watchers.get(indexes[index]);
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
return indexes.length;
}
}
}