patterntesting.concurrent.junit.JUnitExecutor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of patterntesting-concurrent Show documentation
Show all versions of patterntesting-concurrent Show documentation
PatternTesting Concurrent (patterntesting-concurrent) is a collection
of useful thread aspects. It has support for testing, for
sychnronization and for concurrent programming.
Some of the ideas used in this library comes from reading
Brian Goetz's book "Java Concurrency in Practice".
package patterntesting.concurrent.junit;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import org.apache.commons.logging.*;
import patterntesting.annotation.check.runtime.*;
import patterntesting.annotation.concurrent.RunTestsParallel;
import patterntesting.runtime.util.Environment;
// TODO: Auto-generated Javadoc
/**
* 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 Log log = LogFactory.getLog(JUnit4Executor.class);
private static final Log runLog = LogFactory.getLog(RunTestsParallel.class);
private static final Set> setUpClasses = 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 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;
}
Annotation a = this.testClass.getAnnotation(RunTestsParallel.class);
RunTestsParallel rtp = (RunTestsParallel) a;
return rtp.enabled();
}
/**
* 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 boolean isRunParallelEnabled() {
return this.runParallel;
}
/**
* Gets the test class.
*
* @return the test class
*/
protected Class> getTestClass() {
return this.testClass;
}
/**
* Record test methods.
*/
protected 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 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) {
Thrower.provoke(e);
} catch (ExecutionException 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(setUpClasses) {
if (setUpClasses.contains(this.testClass)) {
if (log.isTraceEnabled()) {
log.trace(this.setupBeforeClassMethod
+ " skipped (already called");
}
} else {
setUpClasses.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) {
throwAssertionErrorFor(this.teardownMethod, e);
} catch (InvocationTargetException 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) {
throwAssertionErrorFor(method, e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (e != 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 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()) {
runLog.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)
*/
@NullArgsAllowed
private void logMethod(@NotNull final Method method, @NotNull 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.debug("can't instantiate Thrower class", unexpected);
} catch (IllegalAccessException unexpected) {
log.debug("can't access Thrower constructor", unexpected);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy