blob: 5f3142832bf27cc866150ebfc51b843c3441206b [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2015, 2016 Christian W. Damus and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Christian W. Damus - Initial API and implementation
*
*****************************************************************************/
package org.eclipse.papyrus.junit.framework.runner;
import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import org.eclipse.papyrus.junit.framework.classification.ClassificationConfig;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.junit.runners.model.Statement;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
* <p>
* A scenario-based test runner. A method annotated with {@link Scenario @Scenario} lays out a scenario and at various places where something is to be verified, calls this class's static {@link #verificationPoint()} method as an {@code if} condition to guard a
* block of assertion statements. The {@link Scenario @Scenario} annotation provides the labels of the verification points, in the order in which they appear. Each verification point is surfaced as a separate test, which may pass or fail independently of the
* others.
* </p>
* <p>
* Classic {@link Test @Test} methods are supported by this runner, also. They are run in the usual way, not as scenarios with multiple verification points.
* </p>
*
* @see Scenario
* @see #verificationPoint()
*/
public class ScenarioRunner extends ParentRunner<Runner> {
private static final Deque<VerificationPointsRunner> runnerStack = new ArrayDeque<VerificationPointsRunner>();
public ScenarioRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
protected List<Runner> getChildren() {
Iterable<FrameworkMethod> methods = Iterables.concat(
getTestClass().getAnnotatedMethods(Test.class),
getTestClass().getAnnotatedMethods(Scenario.class));
return ImmutableList.copyOf(Iterables.transform(methods, new Function<FrameworkMethod, Runner>() {
@Override
public Runner apply(FrameworkMethod input) {
return new VerificationPointsRunnerBuilder(input).build();
}
}));
}
@Override
protected Description describeChild(Runner child) {
return child.getDescription();
}
@Override
protected void runChild(Runner child, RunNotifier notifier) {
if (!(child instanceof VerificationPointsRunner)) {
// Probably the error-reporting runner
child.run(notifier);
} else {
VerificationPointsRunner points = (VerificationPointsRunner) child;
pushRunner(points);
points.start();
try {
points.run(notifier);
} finally {
points.finish();
popRunner();
}
}
}
/**
* Declares the next verification point in the scenario. Use as the condition of an {@code if} block
* enclosing the verification point's assertion statements. There must be one verification-point
* block per verification-point label declared in the {@link Scenario @Scenario} annotation. e.g.,
*
* <pre>
* import static org.eclipse.papyrus.junit.framework.runners.ScenarioRunner.verificationPoint;
*
* // ...
*
* &#64;Scenario({ "first", "second" })
* public void myLongAndIntricateScenario() {
* // Setup stuff ...
*
* if (verificationPoint()) {
* // Assertions here
* }
*
* // More stuff ...
*
* if (verificationPoint()) {
* // More assertions here
* }
* }
* </pre>
*/
public static boolean verificationPoint() {
return currentRunner().verificationPoint();
}
private static VerificationPointsRunner currentRunner() {
return runnerStack.getLast();
}
private static VerificationPointsRunner popRunner() {
return runnerStack.removeLast();
}
private static void pushRunner(VerificationPointsRunner runner) {
runnerStack.addLast(runner);
}
/**
* Queries whether a test's annotations indicate that it is to be ignored in the
* current run. That may is if any of the annotations is the {@code @Ignore} annotation
* or if none of the {@link ClassificationConfig} annotations match the current run.
*
* @param testAnnotations
* a test's annotations (including those inherited from its class)
*
* @return whether the test should be skipped
*/
static boolean isIgnored(Annotation[] testAnnotations) {
boolean result = !ClassificationConfig.shouldRun(testAnnotations);
if (!result) {
// Look for the @Ignore annotation
result = Iterators.filter(Arrays.asList(testAnnotations).iterator(), Ignore.class).hasNext();
}
return result;
}
//
// Nested types
//
private class VerificationPointsRunnerBuilder extends RunnerBuilder {
private final FrameworkMethod scenarioMethod;
VerificationPointsRunnerBuilder(FrameworkMethod scenarioMethod) {
super();
this.scenarioMethod = scenarioMethod;
}
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
Runner result;
if (scenarioMethod.getAnnotation(Scenario.class) != null) {
result = new VerificationPointsRunner(scenarioMethod);
} else {
// It's just an @Test method
result = new JUnitAccess(testClass).classicTest(scenarioMethod);
}
List<Annotation> allAnnotations = Lists.newArrayList(scenarioMethod.getAnnotations());
allAnnotations.addAll(Arrays.asList(scenarioMethod.getMethod().getDeclaringClass().getAnnotations()));
if (isIgnored(Iterables.toArray(allAnnotations, Annotation.class))) {
result = new IgnoreRunner(result.getDescription());
}
return result;
}
public Runner build() {
return safeRunnerForClass(scenarioMethod.getMethod().getDeclaringClass());
}
}
private class VerificationPointsRunner extends ParentRunner<String> {
private final FrameworkMethod scenarioMethod;
private final Scenario scenario;
private final JUnitAccess junit;
private List<String> verpts = Lists.newArrayList();
private int nextVerificationPoint;
private Failure lastFailure;
VerificationPointsRunner(FrameworkMethod scenarioMethod) throws InitializationError {
super(scenarioMethod.getMethod().getDeclaringClass());
this.scenarioMethod = scenarioMethod;
this.scenario = scenarioMethod.getAnnotation(Scenario.class);
this.junit = new JUnitAccess(scenarioMethod.getMethod().getDeclaringClass());
}
@Override
public Description getDescription() {
Description result = Description.createSuiteDescription(scenarioMethod.getName(), scenarioMethod.getAnnotations());
for (Description child : super.getDescription().getChildren()) {
result.addChild(child);
}
return result;
}
@Override
protected List<String> getChildren() {
return Arrays.asList(scenario.value());
}
@Override
protected Description describeChild(String child) {
return Description.createTestDescription(getTestClass().getJavaClass(), scenarioMethod.getName() + ":" + child);
}
@Override
protected void runChild(String child, RunNotifier notifier) {
Description description = describeChild(child);
if (verpts.contains(child)) {
if (verpts.get(0).equals(child)) {
// This is the first verification point. It needs to run the scenario method.
// If all verification points pass, this will be the only execution of that
// method. Otherwise, if some verification point fails, the next one (if
// any) will run the scenario again, skipping all verifications before it.
if (!runScenario(child, description, notifier)) {
// This run failed, so run again from the beginning of the scenario
nextVerificationPoint = 0;
}
} else {
// This test failed in a previous execution
Throwable last = (lastFailure == null)
? new AssertionError("Previous execution failed")
: lastFailure.getException();
notifier.fireTestStarted(description);
notifier.fireTestFailure(new Failure(description, last));
lastFailure = null;
notifier.fireTestFinished(description);
}
} else if (failedLastTime(child)) {
// This test failed in a previous execution
notifier.fireTestStarted(description);
notifier.fireTestFailure(new Failure(description, lastFailure.getException()));
lastFailure = null;
notifier.fireTestFinished(description);
} else {
// This verification point passed in a previous execution of the scenario
notifier.fireTestStarted(description);
notifier.fireTestFinished(description);
}
}
void start() {
verpts.addAll(getChildren());
nextVerificationPoint = 0;
}
boolean verificationPoint() {
final String[] points = scenario.value();
boolean result = ((nextVerificationPoint < points.length)
&& verpts.contains(points[nextVerificationPoint]));
nextVerificationPoint++;
int limit = Math.min(nextVerificationPoint, points.length);
// We have passed all verifications up to this point
for (int i = 0; i < limit; i++) {
verpts.remove(points[i]);
}
return result;
}
String currentVerificationPoint() {
final String[] points = scenario.value();
int index = Math.max(0, Math.min(nextVerificationPoint - 1, points.length - 1));
return points[index];
}
boolean failedLastTime(String child) {
boolean result = lastFailure != null;
if (result) {
final String[] points = scenario.value();
// If there are no verification points remaining, then is this the last one
// and it failed in the previous run?
if (verpts.isEmpty()) {
result = points[points.length - 1].equals(child);
} else {
int successor = Math.max(Arrays.asList(points).indexOf(child) + 1, points.length - 1);
result = verpts.contains(points[successor]);
}
}
return result;
}
void finish() {
verpts.clear();
nextVerificationPoint = scenario.value().length;
lastFailure = null;
}
private boolean runScenario(final String child, Description description, final RunNotifier notifier) {
final boolean[] result = { true };
RunNotifier notifierWrapper = new RunNotifier() {
@Override
public void fireTestFailure(Failure failure) {
result[0] = false;
if (child.equals(currentVerificationPoint())) {
// This verification point failed
notifier.fireTestFailure(failure);
} else {
// A subsequent verification point failed. This one passed
lastFailure = failure;
}
}
@Override
public void fireTestAssumptionFailed(Failure failure) {
result[0] = false;
if (child.equals(currentVerificationPoint())) {
// This verification point failed
notifier.fireTestAssumptionFailed(failure);
} else {
// A subsequent verification point failed. This one passed
lastFailure = failure;
}
}
@Override
public void fireTestIgnored(Description description) {
notifier.fireTestIgnored(description);
}
@Override
public void fireTestStarted(Description description) throws StoppedByUserException {
notifier.fireTestStarted(description);
}
@Override
public void fireTestFinished(Description description) {
notifier.fireTestFinished(description);
}
};
runLeaf(junit.methodBlock(scenarioMethod), description, notifierWrapper);
return result[0];
}
}
static class JUnitAccess extends BlockJUnit4ClassRunner {
public JUnitAccess(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
protected Statement methodBlock(FrameworkMethod method) {
return super.methodBlock(method);
}
// Our test methods are annotated with @Scenario or @Test
@Override
protected List<FrameworkMethod> computeTestMethods() {
List<FrameworkMethod> result = Lists.newArrayList(getTestClass().getAnnotatedMethods(Scenario.class));
result.addAll(getTestClass().getAnnotatedMethods(Test.class));
return result;
}
Runner classicTest(final FrameworkMethod testMethod) {
return new Runner() {
@Override
public void run(RunNotifier notifier) {
runLeaf(methodBlock(testMethod), getDescription(), notifier);
}
@Override
public Description getDescription() {
return Description.createTestDescription(
getTestClass().getJavaClass(),
testMethod.getName(),
testMethod.getAnnotations());
}
};
}
}
}