| /***************************************************************************** |
| * 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; |
| * |
| * // ... |
| * |
| * @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()); |
| } |
| }; |
| } |
| } |
| } |