All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nordstrom.automation.junit.LifecycleHooks Maven / Gradle / Ivy

There is a newer version: 17.1.1
Show newest version
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.util.Arrays;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.lang3.reflect.MethodUtils;
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;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.AgentBuilder.Transformer;
import net.bytebuddy.description.method.MethodDescription.SignatureToken;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;

/**
 * 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) {
        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 createTest = TypePool.Default.ofSystemLoader().describe("com.nordstrom.automation.junit.CreateTest").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 runNotifier = TypePool.Default.ofSystemLoader().describe("org.junit.runner.notification.RunNotifier").resolve();
        final SignatureToken runToken = new SignatureToken("run", TypeDescription.VOID, Arrays.asList(runNotifier));
        
        return new AgentBuilder.Default()
                .type(hasSuperType(named("org.junit.internal.runners.model.ReflectiveCallable")))
                .transform(new Transformer() {
                    @Override
                    public Builder transform(Builder builder, TypeDescription type,
                                    ClassLoader classloader, JavaModule module) {
                        return builder.method(named("runReflectiveCall")).intercept(MethodDelegation.to(runReflectiveCall))
                                      .implement(Hooked.class);
                    }
                })
                .type(hasSuperType(named("org.junit.runners.model.RunnerScheduler")))
                .transform(new Transformer() {
                    @Override
                    public Builder transform(Builder builder, TypeDescription type,
                                    ClassLoader classloader, JavaModule module) {
                        return builder.method(named("finished")).intercept(MethodDelegation.to(finished))
                                      .implement(Hooked.class);
                    }
                })
                .type(hasSuperType(named("org.junit.runners.ParentRunner")))
                .transform(new Transformer() {
                    @Override
                    public Builder transform(Builder builder, TypeDescription type,
                                    ClassLoader classloader, JavaModule module) {
                        return builder.method(named("createTest")).intercept(MethodDelegation.to(createTest))
                                      .method(named("runChild")).intercept(MethodDelegation.to(runChild))
                                      .method(hasSignature(runToken)).intercept(MethodDelegation.to(run))
                                      .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;
    }
    
    /**
     * Get the class runner associated with the specified instance.
     * 
     * @param target instance of JUnit test class
     * @return {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} for specified instance
     */
    public static Object getRunnerForTarget(Object target) {
        return CreateTest.getRunnerForTarget(target);
    }
    
    /**
     * Get the JUnit test class instance for the specified class runner.
     * 
     * @param runner JUnit class runner
     * @return JUnit test class instance for specified runner
     */
    public static Object getTargetForRunner(Object runner) {
        return CreateTest.getTargetForRunner(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)
     */
    public static Object getParentOf(Object child) {
        return Run.getParentOf(child);
    }

    /**
     * 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 class runner.
     * 
     * @param runner JUnit class runner
     * @return {@link AtomicTest} object (may be {@code null})
     */
    public static  AtomicTest getAtomicTestOf(Object runner) {
        return RunAnnouncer.getAtomicTestOf(runner);
    }
    
    /**
     * 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) {
        Object runner = getRunnerForTarget(target);
        return invoke(runner, "describeChild", child);
    }
    
    /**
     * 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) */ static Object callProxy(final Callable proxy) throws Exception { try { return 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 */ public static Optional getAttachedWatcher(Class watcherType) { Optional watcher = CreateTest.getAttachedWatcher(watcherType); if (watcher.isPresent()) return watcher; watcher = Run.getAttachedWatcher(watcherType); if (watcher.isPresent()) return watcher; watcher = RunAnnouncer.getAttachedWatcher(watcherType); if (watcher.isPresent()) return watcher; return RunReflectiveCall.getAttachedWatcher(watcherType); } /** * 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) { return Run.getAttachedListener(listenerType); } /** * 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy