diff options
author | Christian W. Damus | 2015-03-26 15:23:01 +0000 |
---|---|---|
committer | Christian W. Damus | 2015-03-26 15:44:24 +0000 |
commit | 85cc48dc2e546bfdb9b583489903c9a5db2e5f7f (patch) | |
tree | b2e96e9ff7dae52b061317d4e617765457f0580b /tests/junit/plugins/junit | |
parent | 2562df47d1c3c5ec4b10a50c83ee623ac773a2dd (diff) | |
download | org.eclipse.papyrus-85cc48dc2e546bfdb9b583489903c9a5db2e5f7f.tar.gz org.eclipse.papyrus-85cc48dc2e546bfdb9b583489903c9a5db2e5f7f.tar.xz org.eclipse.papyrus-85cc48dc2e546bfdb9b583489903c9a5db2e5f7f.zip |
Bug 433206: Papyrus shall enable local synchronization between graphical element and element in the model
https://bugs.eclipse.org/bugs/show_bug.cgi?id=433206
Reduce execution time of canonical edit policy tests by combining related tests
(especially undo/redo variants) into scenarios comprising multiple verification points.
A new ScenarioRunner class runs tests annotated as @Scenario containing multiple
verification points, surfacing each verification point as a discrete test in the model.
Individual verification points may fail or pass independently according to whether any
of their assertions fails.
Diffstat (limited to 'tests/junit/plugins/junit')
2 files changed, 414 insertions, 0 deletions
diff --git a/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/Scenario.java b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/Scenario.java new file mode 100644 index 00000000000..6a21299ad24 --- /dev/null +++ b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/Scenario.java @@ -0,0 +1,34 @@ +/***************************************************************************** + * Copyright (c) 2015 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation on a public instance method that defines a test scenario having multiple distinct + * and potentially independent verification points. + * + * @see ScenarioRunner + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Scenario { + /** + * Specifies the labels of the verification points in the scenario, in order. + */ + String[] value(); +} diff --git a/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/ScenarioRunner.java b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/ScenarioRunner.java new file mode 100644 index 00000000000..9a1dfe80211 --- /dev/null +++ b/tests/junit/plugins/junit/org.eclipse.papyrus.junit.framework/src/org/eclipse/papyrus/junit/framework/runner/ScenarioRunner.java @@ -0,0 +1,380 @@ +/***************************************************************************** + * Copyright (c) 2015 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.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +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.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); + } + + // + // 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 { + if (scenarioMethod.getAnnotation(Scenario.class) != null) { + return new VerificationPointsRunner(scenarioMethod); + } else { + // It's just an @Test method + return new JUnitAccess(testClass).classicTest(scenarioMethod); + } + } + + 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 + notifier.fireTestStarted(description); + notifier.fireTestFailure(new Failure(description, lastFailure.getException())); + 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()); + } + }; + } + } +} |