/*
* JBoss, Home of Professional Open Source
* Copyright 2009 Red Hat Inc. and/or its affiliates and other contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.arquillian.junit;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.jboss.arquillian.junit.event.AfterRules;
import org.jboss.arquillian.junit.event.BeforeRules;
import org.jboss.arquillian.junit.event.RulesEnrichment;
import org.jboss.arquillian.test.spi.LifecycleMethodExecutor;
import org.jboss.arquillian.test.spi.TestMethodExecutor;
import org.jboss.arquillian.test.spi.TestResult;
import org.jboss.arquillian.test.spi.TestResult.Status;
import org.jboss.arquillian.test.spi.TestRunnerAdaptor;
import org.jboss.arquillian.test.spi.TestRunnerAdaptorBuilder;
import org.jboss.arquillian.test.spi.execution.SkippedTestExecutionException;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.MultipleFailureException;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.internal.runners.statements.Fail;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
/**
* Main Arquillian JUnit runner
*
* @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a>
* @version $Revision: $
*/
public class Arquillian extends BlockJUnit4ClassRunner {
private TestRunnerAdaptor adaptor;
public Arquillian(Class<?> testClass) throws InitializationError {
super(testClass);
if (State.isRunningInEclipse()) {
State.runnerStarted();
}
}
@Override
protected List<FrameworkMethod> getChildren() {
List<FrameworkMethod> children = super.getChildren();
// Only sort if InOrder is defined, else keep them in original order returned by parent
boolean hasDefinedOrder = false;
for (FrameworkMethod method : children) {
if (method.getAnnotation(InSequence.class) != null) {
hasDefinedOrder = true;
}
}
if (hasDefinedOrder) {
List<FrameworkMethod> sorted = new ArrayList<FrameworkMethod>(children);
Collections.sort(sorted, new InSequenceSorter());
return sorted;
}
return children;
}
@Override
public void run(final RunNotifier notifier) {
if (State.isNotRunningInEclipse()) {
State.runnerStarted();
}
// first time we're being initialized
if (!State.hasTestAdaptor()) {
// no, initialization has been attempted before and failed, refuse to do anything else
if (State.hasInitializationException()) {
// failed on suite level, ignore children
//notifier.fireTestIgnored(getDescription());
notifier.fireTestFailure(
new Failure(getDescription(),
new RuntimeException(
"Arquillian has previously been attempted initialized, but failed. See cause for previous exception",
State.getInitializationException())));
} else {
try {
// ARQ-1742 If exceptions happen during boot
TestRunnerAdaptor adaptor = TestRunnerAdaptorBuilder.build();
// don't set it if beforeSuite fails
adaptor.beforeSuite();
State.testAdaptor(adaptor);
} catch (Exception e) {
// caught exception during BeforeSuite, mark this as failed
State.caughtInitializationException(e);
notifier.fireTestFailure(new Failure(getDescription(), e));
}
}
}
notifier.addListener(new RunListener() {
@Override
public void testRunFinished(Result result) throws Exception {
State.runnerFinished();
shutdown();
}
private void shutdown() {
try {
if (State.isLastRunner()) {
try {
if (adaptor != null) {
adaptor.afterSuite();
adaptor.shutdown();
}
} finally {
State.clean();
}
}
adaptor = null;
} catch (Exception e) {
throw new RuntimeException("Could not run @AfterSuite", e);
}
}
});
// initialization ok, run children
if (State.hasTestAdaptor()) {
adaptor = State.getTestAdaptor();
super.run(notifier);
}
}
/**
* Override to allow test methods with arguments
*/
@Override
protected void validatePublicVoidNoArgMethods(Class<? extends Annotation> annotation, boolean isStatic,
List<Throwable> errors) {
List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(annotation);
for (FrameworkMethod eachTestMethod : methods) {
eachTestMethod.validatePublicVoid(isStatic, errors);
}
}
/*
* Override BeforeClass/AfterClass and Before/After handling.
*
* Let super create the Before/After chain against a EmptyStatement so our newly created Statement
* only contains the method that are of interest to us(@Before..etc).
* They can then optionally be executed if we get expected callback.
*
*/
@Override
protected Statement withBeforeClasses(final Statement originalStatement) {
final Statement onlyBefores = super.withBeforeClasses(new EmptyStatement());
return new Statement() {
@Override
public void evaluate() throws Throwable {
adaptor.beforeClass(
Arquillian.this.getTestClass().getJavaClass(),
new StatementLifecycleExecutor(onlyBefores));
originalStatement.evaluate();
}
};
}
@Override
protected Statement withAfterClasses(final Statement originalStatement) {
final Statement onlyAfters = super.withAfterClasses(new EmptyStatement());
return new Statement() {
@Override
public void evaluate() throws Throwable {
multiExecute
(
originalStatement,
new Statement() {
@Override
public void evaluate() throws Throwable {
adaptor.afterClass(
Arquillian.this.getTestClass().getJavaClass(),
new StatementLifecycleExecutor(onlyAfters));
}
}
);
}
};
}
@Override
protected Statement withBefores(final FrameworkMethod method, final Object target,
final Statement originalStatement) {
final Statement onlyBefores = super.withBefores(method, target, new EmptyStatement());
return new Statement() {
@Override
public void evaluate() throws Throwable {
adaptor.before(
target,
method.getMethod(),
new StatementLifecycleExecutor(onlyBefores));
originalStatement.evaluate();
}
};
}
@Override
protected Statement withAfters(final FrameworkMethod method, final Object target, final Statement originalStatement) {
final Statement onlyAfters = super.withAfters(method, target, new EmptyStatement());
return new Statement() {
@Override
public void evaluate() throws Throwable {
multiExecute
(
originalStatement,
new Statement() {
@Override
public void evaluate() throws Throwable {
adaptor.after(
target,
method.getMethod(),
new StatementLifecycleExecutor(onlyAfters));
}
}
);
}
};
}
@Override
@SuppressWarnings("deprecation")
protected Statement methodBlock(final FrameworkMethod method) {
Object temp;
try {
temp = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
final Object test = temp;
try {
Method withRules = BlockJUnit4ClassRunner.class.getDeclaredMethod("withRules",
FrameworkMethod.class, Object.class, Statement.class);
withRules.setAccessible(true);
Statement statement = methodInvoker(method, test);
statement = possiblyExpectingExceptions(method, test, statement);
statement = withPotentialTimeout(method, test, statement);
Statement arounds = withBefores(method, test, statement);
arounds = withAfters(method, test, arounds);
final Statement stmtWithLifecycle = arounds;
adaptor.fireCustomLifecycle(
new RulesEnrichment(test, getTestClass(), method.getMethod(), LifecycleMethodExecutor.NO_OP));
final Statement stmtWithRules = (Statement) withRules.invoke(this, method, test, arounds);
return new Statement() {
@Override
public void evaluate() throws Throwable {
State.caughtExceptionAfterJunit(null);
final AtomicInteger integer = new AtomicInteger();
List<Throwable> exceptions = new ArrayList<Throwable>();
try {
adaptor.fireCustomLifecycle(
new BeforeRules(test, getTestClass(), stmtWithRules, method.getMethod(),
new LifecycleMethodExecutor() {
@Override
public void invoke() throws Throwable {
integer.incrementAndGet();
stmtWithRules.evaluate();
}
}));
// If AroundRules (includes lifecycles) were not executed, invoke only lifecycles+test
if (integer.get() == 0) {
try {
stmtWithLifecycle.evaluate();
} catch (Throwable t) {
State.caughtExceptionAfterJunit(t);
throw t;
}
}
} catch (Throwable t) {
State.caughtExceptionAfterJunit(t);
exceptions.add(t);
} finally {
try {
adaptor.fireCustomLifecycle(
new AfterRules(test, method.getMethod(), LifecycleMethodExecutor.NO_OP));
} catch (Throwable t) {
State.caughtExceptionAfterJunit(t);
exceptions.add(t);
}
}
if (exceptions.isEmpty()) {
return;
}
if (exceptions.size() == 1) {
throw exceptions.get(0);
}
throw new MultipleFailureException(exceptions);
}
};
} catch (Exception e) {
throw new RuntimeException("Could not create statement", e);
}
}
@Override
protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
TestResult result = adaptor.test(new TestMethodExecutor() {
@Override
public void invoke(Object... parameters) throws Throwable {
try {
method.invokeExplosively(test, parameters);
} catch (Throwable e) {
// Force a way to return the thrown Exception from the Container to the client.
State.caughtTestException(e);
throw e;
}
}
public Method getMethod() {
return method.getMethod();
}
public Object getInstance() {
return test;
}
});
Throwable throwable = result.getThrowable();
if (throwable != null) {
if (result.getStatus() == Status.SKIPPED) {
if (throwable instanceof SkippedTestExecutionException) {
result.setThrowable(new AssumptionViolatedException(throwable.getMessage()));
}
}
throw result.getThrowable();
}
}
};
}
/**
* A helper to safely execute multiple statements in one.<br/>
* <p>
* Will execute all statements even if they fail, all exceptions will be kept. If multiple {@link Statement}s
* fail, a {@link MultipleFailureException} will be thrown.
*
* @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
* @version $Revision: $
*/
private void multiExecute(Statement... statements) throws Throwable {
List<Throwable> exceptions = new ArrayList<Throwable>();
for (Statement command : statements) {
try {
command.evaluate();
} catch (Throwable e) {
exceptions.add(e);
}
}
if (exceptions.isEmpty()) {
return;
}
if (exceptions.size() == 1) {
throw exceptions.get(0);
}
throw new MultipleFailureException(exceptions);
}
private static class EmptyStatement extends Statement {
@Override
public void evaluate() throws Throwable {
}
}
private static class StatementLifecycleExecutor implements LifecycleMethodExecutor {
private Statement statement;
public StatementLifecycleExecutor(Statement statement) {
this.statement = statement;
}
public void invoke() throws Throwable {
statement.evaluate();
}
}
}