Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.nordstrom.automation.junit.LifecycleHooks Maven / Gradle / Ivy
package com.nordstrom.automation.junit;
import static net.bytebuddy.matcher.ElementMatchers.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.TestClass;
import com.nordstrom.automation.junit.JUnitConfig.JUnitSettings;
import com.nordstrom.common.base.UncheckedThrow;
import com.nordstrom.common.file.PathUtils.ReportsDirectory;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.pool.TypePool;
/**
* This class implements the hooks and utility methods that activate the core functionality of JUnit Foundation .
*/
public class LifecycleHooks {
private static JUnitConfig config;
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 {
for (ShutdownListener listener : ServiceLoader.load(ShutdownListener.class)) {
Runtime.getRuntime().addShutdownHook(getShutdownHook(listener));
}
}
/**
* This is the main entry point for the Java agent used to transform {@code ParentRunner} and
* {@code BlockJUnit4ClassRunner}.
*
* @param agentArgs agent options
* @param instrumentation {@link Instrumentation} object used to transform JUnit core classes
*/
public static void premain(String agentArgs, Instrumentation instrumentation) {
installTransformer(instrumentation);
}
/**
* Install the {@code Byte Buddy} byte code transformations that provide test fine-grained test lifecycle hooks.
*
* @param instrumentation {@link Instrumentation} object used to transform JUnit core classes
* @return The installed class file transformer
*/
public static ClassFileTransformer installTransformer(Instrumentation instrumentation) {
TypeDescription reflectiveCallable = TypePool.Default.ofClassPath().describe("org.junit.internal.runners.model.ReflectiveCallable").resolve();
TypeDescription parentRunner = TypePool.Default.ofClassPath().describe("org.junit.runners.ParentRunner").resolve();
TypeDescription blockJUnit4ClassRunner = TypePool.Default.ofClassPath().describe("org.junit.runners.BlockJUnit4ClassRunner").resolve();
return new AgentBuilder.Default()
.type(isSubTypeOf(reflectiveCallable))
.transform((builder, type, classLoader, module) ->
builder.method(named("runReflectiveCall")).intercept(MethodDelegation.to(RunReflectiveCall.class))
.implement(Hooked.class))
.type(is(parentRunner))
.transform((builder, type, classLoader, module) ->
builder.method(named("createTestClass")).intercept(MethodDelegation.to(CreateTestClass.class))
.method(named("run")).intercept(MethodDelegation.to(Run.class))
.implement(Hooked.class))
.type(is(blockJUnit4ClassRunner))
.transform((builder, type, classLoader, module) ->
builder.method(named("createTest")).intercept(MethodDelegation.to(CreateTest.class))
.method(named("runChild")).intercept(MethodDelegation.to(RunChild.class))
.implement(Hooked.class))
.installOn(instrumentation);
}
/**
* 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;
}
/**
* This class declares the interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
*/
@SuppressWarnings("squid:S1118")
public static class Run {
private static final ServiceLoader runListenerLoader;
private static final ServiceLoader runnerWatcherLoader;
private static final Set NOTIFIERS = new HashSet<>();
private static final Map CHILD_TO_PARENT = new ConcurrentHashMap<>();
static {
runListenerLoader = ServiceLoader.load(RunListener.class);
runnerWatcherLoader = ServiceLoader.load(RunnerWatcher.class);
}
/**
* Interceptor for the {@link org.junit.runners.ParentRunner#run run} method.
*
* @param runner underlying test runner
* @param proxy callable proxy for the intercepted method
* @param notifier run notifier through which events are published
* @throws Exception {@code anything} (exception thrown by the intercepted method)
*/
public static void intercept(@This final Object runner, @SuperCall final Callable> proxy,
@Argument(0) final RunNotifier notifier) throws Exception {
List> children = invoke(runner, "getChildren");
for (Object child : children) {
CHILD_TO_PARENT.put(child, runner);
}
if (NOTIFIERS.add(notifier)) {
Description description = invoke(runner, "getDescription");
for (RunListener listener : runListenerLoader) {
notifier.addListener(listener);
listener.testRunStarted(description);
}
}
for (RunnerWatcher watcher : runnerWatcherLoader) {
watcher.runStarted(runner);
}
callProxy(proxy);
for (RunnerWatcher watcher : runnerWatcherLoader) {
watcher.runFinished(runner);
}
}
/**
* Get the parent runner that owns specified child runner.
*
* @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(Object child) {
return CHILD_TO_PARENT.get(child);
}
}
/**
* This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest
* createTest} method.
*/
@SuppressWarnings("squid:S1118")
public static class CreateTest {
private static final ServiceLoader objectWatcherLoader;
private static final Map TARGET_TO_TESTCLASS = new ConcurrentHashMap<>();
static {
objectWatcherLoader = ServiceLoader.load(TestObjectWatcher.class);
}
/**
* Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest createTest} method.
*
* @param runner target {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} object
* @param proxy callable proxy for the intercepted method
* @return {@code anything} - JUnit test class instance
* @throws Exception {@code anything} (exception thrown by the intercepted method)
*/
@RuntimeType
public static Object intercept(@This final Object runner,
@SuperCall final Callable> proxy) throws Exception {
Object testObj = callProxy(proxy);
TARGET_TO_TESTCLASS.put(testObj, getTestClassOf(runner));
applyTimeout(testObj);
for (TestObjectWatcher watcher : objectWatcherLoader) {
watcher.testObjectCreated(testObj, TARGET_TO_TESTCLASS.get(testObj));
}
return testObj;
}
/**
* Get the test class object that wraps the specified instance.
*
* @param target instance of JUnit test class
* @return {@link TestClass} associated with specified instance
*/
static TestClass getTestClassFor(Object target) {
TestClass testClass = TARGET_TO_TESTCLASS.get(target);
if (testClass != null) {
return testClass;
}
throw new IllegalArgumentException("No associated test class was found for specified instance");
}
}
/**
* Get the test class object that wraps the specified instance.
*
* @param target instance of JUnit test class
* @return {@link TestClass} associated with specified instance object
*/
public static TestClass getTestClassFor(Object target) {
return CreateTest.getTestClassFor(target);
}
/**
* Get the parent runner associated with the specified test class object.
*
* @param testClass {@link TestClass} object
* @return {@link org.junit.runners.ParentRunner ParentRunner} that owns the specified test class object
*/
public static Object getRunnerFor(TestClass testClass) {
return CreateTestClass.getRunnerFor(testClass);
}
/**
* Get the test class associated with the specified framework method.
*
* @param method {@code FrameworkMethod} object
* @return {@link TestClass} object associated with the specified framework method
*/
public static TestClass getTestClassWith(Object method) {
return CreateTestClass.getTestClassWith(method);
}
/**
* Get the parent runner that owns specified child runner.
*
* @param child {@link org.junit.runners.ParentRunner ParentRunner} 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 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");
}
/**
* Determine if the atomic test associated with the specified test class has configuration methods.
*
* @param testClass {@link TestClass} object
* @return {@code true} if the atomic test has configuration; otherwise {@code false}
*/
public static boolean hasConfiguration(TestClass testClass) {
AtomicTest atomicTest = RunReflectiveCall.getAtomicTestFor(testClass);
return atomicTest.hasConfiguration();
}
/**
* Get the description of the indicated child object from the runner for the specified test class instance.
*
* @param target test class instance
* @param child child object
* @return {@link Description} object for the indicated child
*/
public static Description describeChild(Object target, Object child) {
TestClass testClass = getTestClassFor(target);
Object runner = getRunnerFor(testClass);
return invoke(runner, "describeChild", child);
}
/**
* If configured for default test timeout, apply this value to every test that doesn't already specify a longer
* timeout interval.
*
* @param testObj test class object
*/
static void applyTimeout(Object testObj) {
// if default test timeout is defined
if (getConfig().containsKey(JUnitSettings.TEST_TIMEOUT.key())) {
// get default test timeout
long defaultTimeout = getConfig().getLong(JUnitSettings.TEST_TIMEOUT.key());
// iterate over test object methods
for (Method method : testObj.getClass().getDeclaredMethods()) {
// get @Test annotation
Test annotation = method.getDeclaredAnnotation(Test.class);
// if annotation declared and current timeout is less than default
if ((annotation != null) && (annotation.timeout() < defaultTimeout)) {
// set test timeout interval
MutableTest.proxyFor(method).setTimeout(defaultTimeout);
}
}
}
}
/**
* 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) {
Class>[] parameterTypes = new Class>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
Throwable thrown = null;
for (Class> current = target.getClass(); current != null; current = current.getSuperclass()) {
try {
Method method = current.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return (T) method.invoke(target, parameters);
} catch (NoSuchMethodException e) {
thrown = e;
} catch (SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
thrown = e;
break;
}
}
throw UncheckedThrow.throwUnchecked(thrown);
}
/**
* 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, SecurityException {
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")
static T getFieldValue(Object target, String name) throws IllegalAccessException, NoSuchFieldException, SecurityException {
Field field = getDeclaredField(target, name);
field.setAccessible(true);
return (T) field.get(target);
}
/**
* Set the value of the specified field of the supplied object.
*
* @param target target object
* @param name field name
* @param value value to set in the specified field of 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
*/
static void setFieldValue(Object target, String name, Object value) throws IllegalAccessException, NoSuchFieldException, SecurityException {
Field field = getDeclaredField(target, name);
field.setAccessible(true);
field.set(target, value);
}
/**
* 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)
*/
static Object callProxy(final Callable> proxy) throws Exception {
try {
return proxy.call();
} catch (InvocationTargetException e) {
throw UncheckedThrow.throwUnchecked(e.getCause());
}
}
}