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

patterntesting.concurrent.junit.JUnitExecutor Maven / Gradle / Ivy

package patterntesting.concurrent.junit;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import patterntesting.annotation.check.runtime.MayReturnNull;
import patterntesting.runtime.util.Environment;

/**
 * This class is responsible for starting a JUnit test method. This means
 * that it must also be able to find the setup method which is called before
 * the JUnit test.
 *
 * @author oliver
 * @since 18.12.2009
 */
public abstract class JUnitExecutor {

    private static final Logger LOG = LoggerFactory.getLogger(JUnitExecutor.class);
    private static final Logger RUNLOG = LoggerFactory.getLogger(ParallelRunner.class);
    private static final Set> SETUP_CLASSES = new HashSet>();
    private final Class testClass;
    private boolean runParallel;

    /** The setup before class method. */
    protected Method setupBeforeClassMethod;

    /** The setup method. */
    protected Method setupMethod;

    /** The results. */
    protected final Map results = new HashMap();

    /** The teardown method. */
    protected Method teardownMethod;
    private Executor executor;

    /**
     * The Class Result.
     */
    protected static class Result {

        /**
         * Instantiates a new result.
         *
         * @param method the method
         */
        public Result(final Method method) {
            this.method = method;
        }

        /** the called method. */
        public Method method;

        /** the (future) result. */
        public FutureTask future;
    }

    /**
     * Instantiates a new j unit executor.
     *
     * @param clazz the class with the test methods
     */
    public JUnitExecutor(final Class clazz) {
        this.testClass = clazz;
        this.init();
    }

    /**
     * This method clears the recorded results.
     *
     * @since 1.0
     */
    public final void reset() {
        this.init();
        this.recordResults();
    }

    /**
     * Record results.
     */
    protected abstract void recordResults();

    private void init() {
        this.runParallel = this.initRunParallel();
        if (this.runParallel) {
            executor = Executors.newCachedThreadPool();
        } else if (LOG.isTraceEnabled()) {
            LOG.trace("RunTestsParallel is disabled for " + this.testClass);
        }
    }

    /**
     * In Google's App Engine environment (and maybe in other JEE environments)
     * multithreading is not allowed. So we can't start parallel tests in these
     * environments.
     *
     * @return true if RunTestsParallel is enabled
     */
    private boolean initRunParallel() {
        if (!Environment.areThreadsAllowed()) {
            return false;
        }
        if (Environment.isPropertyEnabled(Environment.RUN_TESTS_PARALLEL)) {
            return true;
        }
        return false;
    }

    /**
     * The default is "true". But you can disable the parallel runs of the test
     * method via the system property "patterntesting.runTestsParallel" or via the attribute
     * "enabled" in the RunTestsParallel annotation.
     *
     * @return true (default)
     */
    public final boolean isRunParallelEnabled() {
        return this.runParallel;
    }

    /**
     * Gets the test class.
     *
     * @return the test class
     */
    protected final Class getTestClass() {
        return this.testClass;
    }

    /**
     * Record test methods.
     */
    protected final void recordTestMethods() {
        if (this.isRunParallelEnabled()) {
            for(Result result : results.values()) {
                triggerTest(result);
            }
        }
    }

    /**
     * Here we trigger the test only and store the result (a Throwable if the
     * JUnit test fails) in a "Future" object.
     *
     * @param result this contains the JUnit method
     */
    @MayReturnNull
    private void triggerTest(final Result result) {
        Callable callable = new Callable() {
            public Throwable call() throws Exception {
                try {
                    invokeTest(result.method);
                    return null;
                } catch (Throwable t) {
                    return t;
                }
            }
        };
        result.future = new FutureTask(callable);
        executor.execute(result.future);
    }

    /**
     * Replays the given test i.e. it looks for the test result and throws
     * an exception if the found test has thrown one.
     *
     * @param methodName
     *            the method name
     * @param obj
     *            the object
     */
    public final void playTest(final String methodName, final Object obj) {
        Result result = this.results.get(methodName);
        if ((result == null) || (result.future == null)) {
            LOG.trace("starting now " + methodName + "...");
            invokeTest(methodName);
            return;
        }
        try {
            Throwable t = result.future.get();
            if (t != null) {
                Thrower.provoke(t);
            }
        } catch (InterruptedException e) {
            LOG.debug("Getting result from " + result.future + " was interrupted:", e);
            Thrower.provoke(e);
        } catch (ExecutionException e) {
            LOG.debug("Cannot execute " + result.future + ":", e);
            Thrower.provoke(e);
        }
    }

    private void invokeTest(final String methodName) {
        try {
            Method method = this.testClass.getMethod(methodName);
            invokeTest(method);
        } catch (SecurityException e) {
            throw new IllegalArgumentException(this.testClass + "."
                    + methodName + " can't be invoked", e);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(methodName + " not found in "
                    + this.testClass, e);
        }
    }

    /**
     * Calls the given test method. But before the setup method of the JUnit
     * test class is called (and afterwards the teardown method).
     * 
* For each test a new instance is created. Why? To be sure that the tests * can run in parallel. * * @param methodName * the method name */ private void invokeTest(final Method method) { Throwable thrown = null; long[] t = new long[5]; t[0] = System.currentTimeMillis(); try { Object obj = this.testClass.newInstance(); t[1] = System.currentTimeMillis(); try { this.callSetup(obj); t[2] = System.currentTimeMillis(); this.call(method, obj); } finally { t[3] = System.currentTimeMillis(); this.callTeardown(obj); t[4] = System.currentTimeMillis(); } } catch (InstantiationException e) { t[1] = System.currentTimeMillis(); thrown = e; throw new AssertionError("can't instantiate " + this.testClass + " (" + e + ")"); } catch (IllegalAccessException e) { t[1] = System.currentTimeMillis(); thrown = e; throw new AssertionError("can't access ctor of " + this.testClass + " (" + e + ")"); } catch (AssertionError e) { t[4] = System.currentTimeMillis(); thrown = e; throw e; } finally { if (t[2] == 0) { t[2] = t[1]; } if (t[3] == 0) { t[3] = t[2]; } logMethod(method, t, thrown); } } /** * If the JUnit test class have a setupBeforeClass method we will call it * here. */ protected void callSetupBeforeClass() { if (this.setupBeforeClassMethod != null) { synchronized(SETUP_CLASSES) { if (SETUP_CLASSES.contains(this.testClass)) { if (LOG.isTraceEnabled()) { LOG.trace(this.setupBeforeClassMethod + " skipped (already called"); } } else { SETUP_CLASSES.add(this.testClass); if (LOG.isTraceEnabled()) { LOG.trace("calling " + this.setupBeforeClassMethod + "..."); } String result = "unknown"; long t0 = System.currentTimeMillis(); try { call(this.setupBeforeClassMethod); result = "SUCCESS"; } catch (AssertionError e) { result = "FAILURE"; throw e; } finally { long t = System.currentTimeMillis() - t0; logMethod(result, this.setupBeforeClassMethod, t); } } } } } /** * If the JUnit test class have a setup method we will call it here. * * @param obj the object for the method invoke */ private void callSetup(final Object obj) { if (this.setupMethod != null) { call(this.setupMethod, obj); } } /** * If the JUnit test class have a teardown method we will call it here. * * @param obj the object for the method invoke */ private void callTeardown(final Object obj) { if (this.teardownMethod != null) { call(this.teardownMethod, obj); } } /** * Calls the (static) method but throws an AssertionError if an exception * happens. * * @param method method to be called * @param obj the object for the method */ private void call(final Method method) { try { method.invoke(null); } catch (IllegalAccessException e) { LOG.debug("Cannot access " + method + ":", e); throwAssertionErrorFor(this.teardownMethod, e); } catch (InvocationTargetException e) { LOG.debug("Cannot invoke " + method + ":", e); throwAssertionErrorFor(this.teardownMethod, e); } } /** * Calls the method but throws an AssertionError if an exception happens. * * @param method method to be called * @param obj the object for the method */ private void call(final Method method, final Object obj) { try { method.setAccessible(true); method.invoke(obj); } catch (IllegalAccessException e) { LOG.debug("Cannot access " + method + ":", e); throwAssertionErrorFor(method, e); } catch (InvocationTargetException e) { LOG.debug("Cannot invoke " + method + " with " + obj + ":", e); Throwable t = e.getTargetException(); if (t != null) { Thrower.provoke(t); } else { throwAssertionErrorFor(method, e); } } } private void throwAssertionErrorFor(final Method method, final Throwable t) { String detailedMessage = "invoke of " + testClass.getSimpleName() + "." + method + "() failed\n" + t; throw new AssertionError(detailedMessage); } /** * To string. * * @return the simple class name and the name of the test class */ @Override public final String toString() { return this.getClass().getSimpleName() + " for " + this.testClass.getSimpleName() + " " + this.results.size() + " test methods"; } private static void logMethod(final String result, final Method method, final long time) { if (RUNLOG.isInfoEnabled()) { LOG.info(result + ": " + method.getDeclaringClass().getName() + "." + method.getName() + " (" + time + " ms)"); } } /** * Logs the test method with the different times for setup(), testXxx() * and tearDown() method. And if the constructor call needs to long this * time is also logged. * * @param method * the called method * @param t * t[0]: start time * t[1]: time after test class was instantiated * t[2]: time after setup method was called * t[3]: time after test method was called * t[4]: time after tear down method was called * @param thrown * the thrown exception (can be null) */ private void logMethod(final Method method, final long[] t, final Throwable thrown) { if (RUNLOG.isInfoEnabled()) { String classname = method.getDeclaringClass().getName(); long tInstantiated = t[1] - t[0]; long tTestTotal = t[4] - t[1]; if (tInstantiated > tTestTotal) { RUNLOG.info(classname + ". (" + tInstantiated + "ms)"); } StringBuffer msg = new StringBuffer(classname + "." + method.getName() + " ("); if (this.setupMethod == null) { msg.append("-/"); } else { msg.append(t[2] - t[1]); msg.append("/"); } msg.append(t[3] - t[2]); if (this.teardownMethod == null) { msg.append("/- ms)"); } else { msg.append("/"); msg.append(t[4] - t[3]); msg.append(" ms)"); } if (thrown != null) { msg.append(" - "); msg.append(thrown.getClass().getSimpleName()); } RUNLOG.info("{}", msg); } } /** * The trick here is to use the constructor to throw any desired exception. * So you can throw any exception without the need to have it as throws * clause. * * @author oliver */ private static class Thrower { private static Throwable throwable; private Thrower() throws Throwable { throw throwable; } /** * Provoke an exception. * * @param t the Throwable which should be used as provoked exception. */ public static void provoke(final Throwable t) { throwable = t; try { Thrower.class.newInstance(); } catch (InstantiationException unexpected) { LOG.info("Cannot instantiate Thrower class:", unexpected); } catch (IllegalAccessException unexpected) { LOG.info("Cannot access Thrower constructor:", unexpected); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy