diff --git a/com.avaloq.tools.ddk.test.core/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.test.core/META-INF/MANIFEST.MF
index 3acdf2e74e..2273c266ef 100644
--- a/com.avaloq.tools.ddk.test.core/META-INF/MANIFEST.MF
+++ b/com.avaloq.tools.ddk.test.core/META-INF/MANIFEST.MF
@@ -15,7 +15,9 @@ Require-Bundle: org.eclipse.core.runtime,
org.apache.commons.lang,
org.eclipse.emf.common,
com.avaloq.tools.ddk,
- junit-jupiter-api
+ junit-jupiter-api,
+ org.opentest4j,
+ org.eclipse.jdt.annotation
Export-Package: com.avaloq.tools.ddk.test.core,
com.avaloq.tools.ddk.test.core.data,
com.avaloq.tools.ddk.test.core.junit.runners,
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/AbstractTestStep.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/AbstractTestStep.java
index 97dbabbd22..d03b9980af 100644
--- a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/AbstractTestStep.java
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/AbstractTestStep.java
@@ -208,7 +208,7 @@ protected void runAfter() {
* whether preconditions shall be checked
*/
@SuppressFBWarnings("AT_STALE_THREAD_WRITE_OF_PRIMITIVE")
- protected static void setCheckPreconditions(final boolean checkPreconditions) {
+ public static void setCheckPreconditions(final boolean checkPreconditions) {
AbstractTestStep.checkPreconditions = checkPreconditions;
}
@@ -219,7 +219,7 @@ protected static void setCheckPreconditions(final boolean checkPreconditions) {
* whether postconditions shall be checked
*/
@SuppressFBWarnings("AT_STALE_THREAD_WRITE_OF_PRIMITIVE")
- protected static void setCheckPostconditions(final boolean checkPostconditions) {
+ public static void setCheckPostconditions(final boolean checkPostconditions) {
AbstractTestStep.checkPostconditions = checkPostconditions;
}
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/CompoundStep.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/CompoundStep.java
index 502f64c325..24d4d52165 100644
--- a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/CompoundStep.java
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/CompoundStep.java
@@ -59,7 +59,7 @@ public void run() {
* Runs the plannedSteps of this {@link CompoundStep}, making sure that any exception is ignored and the next step executed.
* In the end however, all logged exceptions are reported (thrown as a {@link MultipleTestProblems} exception).
*/
- protected void runIgnoreAndContinue() {
+ public void runIgnoreAndContinue() {
getExecutedSteps().clear();
MultipleTestProblems problemsEncountered = new MultipleTestProblems();
for (final AbstractStep step : getSteps()) {
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractSystemTest.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractSystemTest.java
new file mode 100644
index 0000000000..204855cca5
--- /dev/null
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractSystemTest.java
@@ -0,0 +1,325 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Avaloq Group AG 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:
+ * Avaloq Group AG - initial API and implementation
+ *******************************************************************************/
+package com.avaloq.tools.ddk.test.core.jupiter;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.extension.TestWatcher;
+
+import com.avaloq.tools.ddk.test.core.AbstractStep;
+import com.avaloq.tools.ddk.test.core.AbstractTestStep;
+import com.avaloq.tools.ddk.test.core.TestStepListener;
+
+
+/**
+ * Abstract base class for system tests.
+ */
+@SuppressWarnings({"nls"})
+public abstract class AbstractSystemTest implements TestStepListener {
+ // The particular suppress warnings annotation is needed because we WANT to have a unique logger for each of the subclasses
+ @SuppressWarnings("PMD.LoggerIsNotStaticFinal")
+ private final Logger logger = LogManager.getLogger(getClass());
+ private int stepCounter = 1;
+ private final MultipleTestProblems multipleTestProblems = new MultipleTestProblems();
+
+ /** The {@link TestPlan}, that holds all setup- and test-steps of the current test. */
+ private final TestPlan testPlan = TestPlan.create();
+
+ /** The {@link TestPlan}, that holds all setup- and test-steps, which are actually executed. */
+ private TestPlan executedTestPlan = TestPlan.create();
+
+ private static TestPlan previousTestPlan;
+ private static boolean lastExecutedTestFailed;
+ private static boolean lastExecutedTestWasSystemTest;
+ private boolean executingSystemTest;
+
+ /**
+ * Indicates the current state of this {@link AbstractSystemTest}.
+ */
+ private enum TestRunState {
+ SETUP,
+ TEST,
+ TEARDOWN
+ }
+
+ private TestRunState testRunState = TestRunState.SETUP;
+
+ /**
+ * Returns the logger for this test class.
+ *
+ * @return the logger for this test class.
+ */
+ protected Logger log() {
+ return logger;
+ }
+
+ /**
+ * Registers a junit {@link AfterEachCallback} extension, which is called after a test executes, and checks additional possible error sources.
+ */
+ @RegisterExtension
+ // CHECKSTYLE:OFF
+ public AfterEachCallback abstractSystemTestVerifier = new AfterEachCallback() {
+
+ @Override
+ public void afterEach(final ExtensionContext context) throws Exception {
+ multipleTestProblems.assertEmpty();
+
+ }
+ }; // CHECKSTYLE:ON
+
+ /**
+ * Enables support for unresolved bug tests.
+ * This declaration must textually be located after the {@code abstractSystemTestVerifier} above.
+ * This influences the nesting of rules in the chain and bug test aware rule depends on exceptions thrown by the Verifier to succeed.
+ */
+ @RegisterExtension
+ // CHECKSTYLE:OFF
+ public BugTestAwareRule bugTestRule = BugTestAwareRule.getInstance();
+ // CHECKSTYLE:ON
+
+ @RegisterExtension
+ // CHECKSTYLE:OFF
+ public TestWatcher testWatchman = new AbstractTestWatchman() {
+ @Override
+ public void testSuccessful(final ExtensionContext context) {
+ if (multipleTestProblems.hasProblems()) {
+ logger.info(context.getDisplayName() + " failed.");
+ } else {
+ logger.info(context.getDisplayName() + " succeeded.");
+ }
+ }
+ };
+
+ /**
+ * Setup of the system test.
+ * Implementations need to specify the setup steps in this method and also provide the @Before annotation.
+ */
+ @BeforeEach
+ public void setUp() {
+ AbstractTestStep.registerTestStepListener(this);
+ }
+
+ /**
+ * Executes the test plan starting with the setup steps and subsequently executing the test steps.
+ */
+ protected final void executeTestPlan() {
+ cleanUpPreviousTestPlan();
+ // If current test is no system Test or first executed test, current test plan does not have to be changed
+ if (!executingSystemTest || previousTestPlan == null || !lastExecutedTestWasSystemTest || lastExecutedTestFailed) {
+ executedTestPlan = testPlan;
+ } else {
+ executedTestPlan = TestPlan.createExecutableTestPlan(testPlan, previousTestPlan);
+ }
+ previousTestPlan = testPlan;
+ try {
+ AbstractTestStep.setCheckPreconditions(true);
+ AbstractTestStep.setCheckPostconditions(true);
+ executedTestPlan.getCompoundSetupStep().run();
+ AbstractTestStep.setCheckPreconditions(true);
+ AbstractTestStep.setCheckPostconditions(true);
+ testRunState = TestRunState.TEST;
+ executedTestPlan.getCompoundTestStep().run();
+ lastExecutedTestFailed = false;
+ // CHECKSTYLE:OFF
+ } catch (Throwable t) {
+ // CHECKSTYLE:ON
+ lastExecutedTestFailed = true;
+ addTestProblem(t);
+ } finally {
+ lastExecutedTestWasSystemTest = executingSystemTest;
+ testRunState = TestRunState.TEARDOWN; // cannot be put at start of tearDown(), because subclasses may override that method.
+ }
+ }
+
+ /**
+ * Undo the steps of the previous test, that are not needed by the current test.
+ */
+ private void cleanUpPreviousTestPlan() {
+ // No clean up is necessary if one of the following is true:
+ // - there was no previous test
+ // - the last test failed (then everything is cleaned up in the previous test)
+ // - the last test was not a "system test" (then the test cleans up by itself)
+ if (previousTestPlan == null || lastExecutedTestFailed || !lastExecutedTestWasSystemTest) {
+ return;
+ }
+ TestPlan undoTestPlan = TestPlan.createUndoTestPlan(testPlan, previousTestPlan, executingSystemTest);
+ AbstractTestStep.setCheckPreconditions(true);
+ AbstractTestStep.setCheckPostconditions(true);
+ try {
+ undoTestPlan.getCompoundTestStep().runIgnoreAndContinue();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+ try {
+ undoTestPlan.getCompoundSetupStep().runIgnoreAndContinue();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+
+ }
+
+ /**
+ * Executes a system test plan.
+ */
+ protected final void executeSystemTestPlan() {
+ executingSystemTest = true;
+ executeTestPlan();
+ }
+
+ /**
+ * Cleans up the workbench state after a test.
+ *
+ * Note: Undoes all test and setup steps in the correct order, if the test is not marked as a System Test.
+ *
+ */
+ @AfterEach
+ public void tearDown() {
+ try {
+ AbstractTestStep.setCheckPreconditions(true);
+ AbstractTestStep.setCheckPostconditions(true);
+ if (!executingSystemTest) {
+ try {
+ executedTestPlan.getCompoundTestStep().undo();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+ try {
+ executedTestPlan.getCompoundSetupStep().undo();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+ } else if (lastExecutedTestFailed) {
+ Set filter = executedTestPlan.getAllExecutedSteps();
+ filter.addAll(TestPlan.getAllStepsWithPreExistingTestEntities(previousTestPlan, testPlan));
+ TestPlan undoTestPlan = TestPlan.createUndoStepsTestPlan(TestPlan.createReverseTestPlan(TestPlan.createFilteredTestPlan(testPlan, filter)));
+ try {
+ undoTestPlan.getCompoundTestStep().runIgnoreAndContinue();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+ try {
+ undoTestPlan.getCompoundSetupStep().runIgnoreAndContinue();
+ // CHECKSTYLE:CHECK-OFF IllegalCatch
+ } catch (Throwable t) {
+ // CHECKSTYLE:CHECK-ON IllegalCatch
+ // ignore problems during tear down
+ }
+ }
+ } finally {
+ AbstractTestStep.removeTestStepListener(this);
+ }
+ }
+
+ /**
+ * Adds a step as setup step. Setup steps are run before the test starts. Therefore, it is not possible to add a setup step after adding a test step.
+ *
+ * @param
+ * the type of the {@link AbstractStep}
+ * @param setupStep
+ * the step to append to the list of setup steps
+ * @return the added {@link AbstractStep}
+ */
+ protected T addSetupStep(final T setupStep) {
+ return testPlan.addSetupStep(setupStep);
+ }
+
+ /**
+ * Adds a step as test step.
+ *
+ * @param
+ * the type of the {@link AbstractStep}
+ * @param testStep
+ * the step to append to the list of test steps
+ * @return the added {@link AbstractStep}
+ */
+ protected T addTestStep(final T testStep) {
+ return testPlan.addTestStep(testStep);
+ }
+
+ /**
+ * Adds a new test problem.
+ *
+ * @param problem
+ * the new {@link Throwable} to add
+ */
+ protected void addTestProblem(final Throwable problem) {
+ multipleTestProblems.addProblem(problem);
+ logger.error("Error: " + problem.getLocalizedMessage());
+ }
+
+ /**
+ * Adds a new test problem with a message.
+ *
+ * @param message
+ * the message of the problem
+ */
+ protected void addTestProblem(final String message) {
+ addTestProblem(new AssertionError(message));
+ }
+
+ /**
+ * Adds new test problems.
+ *
+ * @param problems
+ * the new {@link Throwable}s to add
+ */
+ protected void addTestProblems(final List problems) {
+ for (Throwable problem : problems) {
+ addTestProblem(problem);
+ }
+ }
+
+ @Override
+ public void stepStateChanged(final AbstractTestStep testStep, final TestStepState testStepState, final Throwable throwable) {
+ switch (testStepState) {
+ case START:
+ switch (testRunState) {
+ case SETUP:
+ logger.info("Setup " + stepCounter++ + ": " + testStep.getName());
+ break;
+ case TEST:
+ logger.info("Test " + stepCounter++ + ": " + testStep.getName());
+ break;
+ case TEARDOWN:
+ logger.info("Teardown: " + testStep.getName());
+ break;
+ }
+ break;
+ case ERRORED:
+ logger.error("ERRORED", throwable);
+ break;
+ case FAILED:
+ logger.error("FAILED", throwable);
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractTestWatchman.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractTestWatchman.java
new file mode 100644
index 0000000000..eb4a4945e0
--- /dev/null
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractTestWatchman.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Avaloq Group AG 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:
+ * Avaloq Evolution AG - initial API and implementation
+ *******************************************************************************/
+
+package com.avaloq.tools.ddk.test.core.jupiter;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.TestWatcher;
+import org.opentest4j.TestAbortedException;
+
+
+@SuppressWarnings("nls")
+public abstract class AbstractTestWatchman implements TestWatcher, BeforeEachCallback {
+
+ private final Logger logger = LogManager.getLogger(getClass());
+
+ @Override
+ public void beforeEach(final ExtensionContext context) throws Exception {
+ logger.info(context.getDisplayName() + " started.");
+ }
+
+ @Override
+ public abstract void testSuccessful(ExtensionContext context);
+
+ @Override
+ public void testFailed(final ExtensionContext context, @Nullable final Throwable cause) {
+ if (cause instanceof TestAbortedException) {
+ logger.warn(context.getDisplayName() + " skipped because of failing assumption: " + cause.toString());
+ } else {
+ logger.warn(context.getDisplayName() + " failed.");
+ }
+ }
+
+}
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/MultipleTestProblems.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/MultipleTestProblems.java
new file mode 100644
index 0000000000..aa1db05978
--- /dev/null
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/MultipleTestProblems.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Avaloq Group AG 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:
+ * Avaloq Group AG - initial API and implementation
+ *******************************************************************************/
+package com.avaloq.tools.ddk.test.core.jupiter;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.opentest4j.MultipleFailuresError;
+
+import com.google.common.collect.Lists;
+
+
+/**
+ * Contains a list of problems.
+ */
+@SuppressWarnings("nls")
+public class MultipleTestProblems extends AssertionError {
+ private static final long serialVersionUID = 1L;
+ private final List problems = Lists.newArrayList();
+
+ /**
+ * Creates a new instance of {@link MultipleTestProblems}.
+ */
+ public MultipleTestProblems() {
+ this(null);
+ }
+
+ /**
+ * Creates a new instance of {@link MultipleTestProblems}.
+ *
+ * @param problems
+ * an initial set of problems, may be {@code null}
+ */
+ public MultipleTestProblems(final List problems) {
+ super("Multiple Test Problems occurred, see stacktrace for info.");
+ if (problems != null) {
+ addProblems(problems);
+ }
+ }
+
+ /**
+ * Adds a new problem.
+ *
+ * @param problem
+ * the {@link Throwable} to be added, must not be {@code null}
+ */
+ public final void addProblem(final Throwable problem) {
+ if (problem instanceof MultipleTestProblems) {
+ addProblems((MultipleTestProblems) problem);
+ } else if (problem instanceof MultipleFailuresError) {
+ addProblems(((MultipleFailuresError) problem).getFailures());
+ } else {
+ problems.add(problem);
+ }
+ }
+
+ /**
+ * Adds new problems.
+ *
+ * @param additionalProblems
+ * the list of {@link Throwable} to be added, must not be {@code null}
+ */
+ public final void addProblems(final List additionalProblems) {
+ for (final Throwable problem : additionalProblems) {
+ addProblem(problem);
+ }
+ }
+
+ /**
+ * Adds new problems of another {@link MutlipleTestProblems} instance.
+ *
+ * @param multipleTestProblems
+ * the list of {@link Throwable} to be added, must not be {@code null}
+ */
+ public final void addProblems(final MultipleTestProblems multipleTestProblems) {
+ addProblems(multipleTestProblems.getProblems());
+ }
+
+ /**
+ * Returns all problems.
+ *
+ * @return all problems, never {@code null}
+ */
+ public List getProblems() {
+ return Lists.newArrayList(problems);
+ }
+
+ /**
+ * Returns {@code true} if there are problems.
+ *
+ * @return {@code true} if there are problems, {@code false} otherwise
+ */
+ public boolean hasProblems() {
+ return !problems.isEmpty();
+ }
+
+ @Override
+ public void printStackTrace(final PrintWriter writer) {
+ int i = 1;
+ writer.println(getMessage());
+ for (final Throwable problem : problems) {
+ writer.print(i++ + ". ");
+ problem.printStackTrace(writer);
+ }
+ }
+
+ @Override
+ public void printStackTrace(final PrintStream stream) {
+ int i = 1;
+ stream.println(getMessage());
+ for (final Throwable problem : problems) {
+ stream.print(i++ + ". ");
+ problem.printStackTrace(stream);
+ }
+ }
+
+ /**
+ * Checks if this {@link MultipleTestProblem} has any problems.
+ *
+ * Note: If there is only one problem, and it is either a {@link RuntimeException} or an {@link Error}, then that problem is thrown. Otherwise this
+ * {@link MultipleTestProblem} is thrown.
+ *
+ */
+ public void assertEmpty() {
+ if (problems.isEmpty()) {
+ return;
+ }
+ if (problems.size() == 1) {
+ final Throwable problem = problems.get(0);
+ if (problem instanceof RuntimeException) {
+ throw (RuntimeException) problem;
+ }
+ if (problem instanceof Error) {
+ throw (Error) problem;
+ }
+ }
+ printStackTrace(); // NOPMD
+ throw this;
+ }
+}
diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/TestPlan.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/TestPlan.java
new file mode 100644
index 0000000000..9a3854c774
--- /dev/null
+++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/TestPlan.java
@@ -0,0 +1,414 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Avaloq Group AG 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:
+ * Avaloq Group AG - initial API and implementation
+ *******************************************************************************/
+package com.avaloq.tools.ddk.test.core.jupiter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import org.junit.jupiter.api.Assertions;
+
+import com.avaloq.tools.ddk.test.core.AbstractStep;
+import com.avaloq.tools.ddk.test.core.CompoundStep;
+import com.avaloq.tools.ddk.test.core.ITestEntity;
+import com.avaloq.tools.ddk.test.core.ITestEntityActionProvider;
+import com.avaloq.tools.ddk.test.core.NullStep;
+import com.avaloq.tools.ddk.test.core.TestEntityAction;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+
+
+/**
+ * The test plan containing the setup and test steps and the {@link TestEntityAction}s of a test.
+ */
+final class TestPlan {
+ /** A {@link CompoundStep}, which is used to hold the setup steps for this {@link TestPlan}. */
+ private final CompoundStep compoundSetupStep = new CompoundStep();
+ /** A {@link CompoundStep}, which is used to hold the test steps for this {@link TestPlan}. */
+ private final CompoundStep compoundTestStep = new CompoundStep();
+ /** The {@link TestEntityAction}s of the test steps of this {@link TestPlan}. */
+ private List testEntityActions;
+
+ /**
+ * Instantiates a new {@link TestPlan} with empty {@link CompoundStep}s.
+ */
+ private TestPlan() {
+ }
+
+ /**
+ * Instantiates a new {@link TestPlan} with the given test-steps and setup-steps.
+ *
+ * @param setupSteps
+ * the setup steps, must not be {@code null}
+ * @param testSteps
+ * the test steps, must not be {@code null}
+ */
+ private TestPlan(final List setupSteps, final List testSteps) {
+ compoundSetupStep.addSteps(setupSteps);
+ compoundTestStep.addSteps(testSteps);
+ }
+
+ /**
+ * Returns a new instance of {@link TestPlan} with empty {@link CompoundStep}s.
+ *
+ * @return a new test plan instance, never {@code null}
+ */
+ static TestPlan create() {
+ return new TestPlan();
+ }
+
+ /**
+ * Adds a setup step to this {@link TestPlan} and asserts that it is not added after a test step.
+ *
+ * @param
+ * the generic type
+ * @param setupStep
+ * the setup step, must not be {@code null}
+ * @return the newly added {@link AbstractStep}, never {@code null}
+ */
+ T addSetupStep(final T setupStep) {
+ Assertions.assertTrue(getCompoundTestStep().getSteps().isEmpty(), "Must not add a setup step after adding a test step."); //$NON-NLS-1$
+ getCompoundSetupStep().addStep(setupStep);
+ return setupStep;
+ }
+
+ /**
+ * Adds a test step to this {@link TestPlan}.
+ *
+ * @param
+ * the generic type
+ * @param testStep
+ * the test step, must not be {@code null}
+ * @return the newly added {@link AbstractStep}, never {@code null}
+ */
+ T addTestStep(final T testStep) {
+ getCompoundTestStep().addStep(testStep);
+ return testStep;
+ }
+
+ /**
+ * Returns the {@link TestEntityAction}s contained in the given {@link CompoundStep}.
+ *
+ * @param compoundStep
+ * the compound step, must not be {@code null}
+ * @return the {@link TestEntityAction}s, never {@code null}
+ */
+ private List getTestEntityActions(final CompoundStep compoundStep) {
+ List allTestEntityActions = new ArrayList();
+ for (final AbstractStep step : compoundStep.getSteps()) {
+ if (step instanceof ITestEntityActionProvider) {
+ allTestEntityActions.addAll(((ITestEntityActionProvider) step).getTestEntityActions());
+ }
+ }
+ return allTestEntityActions;
+ }
+
+ /**
+ * Returns all {@link TestEntityAction}s contained in the setup and test compound steps.
+ *
+ * @return all {@link TestEntityAction}s, never {@code null}
+ */
+ private List getAllTestEntityActions() {
+ if (testEntityActions == null) {
+ testEntityActions = getTestEntityActions(compoundSetupStep);
+ testEntityActions.addAll(getTestEntityActions(compoundTestStep));
+ }
+ return testEntityActions;
+ }
+
+ /**
+ * Creates a {@link TestPlan} which contains the previous test steps that need to be undone, before the current test can be executed.
+ *
+ * @param testPlan
+ * current {@link TestPlan}, must not be {@code null}
+ * @param previousTestPlan
+ * previous {@link TestPlan}, may be {@code null}
+ * @param systemTest
+ * whether a system test plan shall be created
+ * @return test plan containing previous test steps that need to be undone, never {@code null}
+ */
+ static TestPlan createUndoTestPlan(final TestPlan testPlan, final TestPlan previousTestPlan, final boolean systemTest) {
+ if (systemTest) {
+ // If the current and the previous test are system tests, compute the steps to undo.
+ List setupStepsToUndo = computeStepsToUndo(previousTestPlan.getCompoundSetupStep().getSteps(), testPlan);
+ List testStepsToUndo = computeStepsToUndo(previousTestPlan.getCompoundTestStep().getSteps(), testPlan);
+ return new TestPlan(setupStepsToUndo, testStepsToUndo);
+ }
+ // else: If current test is not a System Test and the step before was, all steps have to be undone.
+ List setupStepsToUndo = getAllUndoSteps(previousTestPlan.getCompoundSetupStep().getExecutedSteps());
+ Collections.reverse(setupStepsToUndo);
+ List testStepsToUndo = getAllUndoSteps(previousTestPlan.getCompoundTestStep().getExecutedSteps());
+ Collections.reverse(setupStepsToUndo);
+ return new TestPlan(setupStepsToUndo, testStepsToUndo);
+ }
+
+ /**
+ * Returns a set containing all steps of a {@link TestPlan} that need a {@link ITestEntity} that is still available from a previous test.
+ *
+ * @param previousTestPlan
+ * the previous test plan, must not be {@code null}
+ * @param testPlan
+ * the test plan, must not be {@code null}
+ * @return a set of all steps with previously existing test entities, never {@code null}
+ */
+ static Set getAllStepsWithPreExistingTestEntities(final TestPlan previousTestPlan, final TestPlan testPlan) {
+ Set stepsWithPreExistingTestEntities = new HashSet();
+ stepsWithPreExistingTestEntities.addAll(getStepsWithPreExistingEntities(testPlan, previousTestPlan.compoundSetupStep));
+ stepsWithPreExistingTestEntities.addAll(getStepsWithPreExistingEntities(testPlan, previousTestPlan.compoundTestStep));
+ return stepsWithPreExistingTestEntities;
+ }
+
+ /**
+ * Returns a set containing all steps of a {@link CompoundStep} that need a {@link ITestEntity} that is still available from a previous test.
+ *
+ * @param testPlan
+ * the test plan, must not be {@code null}
+ * @param compoundStep
+ * the compound step, must not be {@code null}
+ * @return the steps with pre existing entities, never {@code null}
+ */
+ private static Set getStepsWithPreExistingEntities(final TestPlan testPlan, final CompoundStep compoundStep) {
+ Set preExistingTestEntities = new HashSet();
+ for (final AbstractStep step : compoundStep.getSteps()) {
+ if (step instanceof ITestEntityActionProvider && testPlan.hasAllTestEntities((ITestEntityActionProvider) step)) {
+ preExistingTestEntities.add(step);
+ }
+ }
+ return preExistingTestEntities;
+ }
+
+ /**
+ * Utility method that checks if the {@link TestPlan} contains all {@link ITestEntity}s of the given {@link ITestEntityActionProvider}.
+ *
+ * @param step
+ * the step, must not be {@code null}
+ * @return {@code true}, if this {@link TestPlan} contains all {@link ITestEntity}s
+ */
+ private boolean hasAllTestEntities(final ITestEntityActionProvider step) {
+ boolean hasAllEntites = false;
+ for (TestEntityAction action : step.getTestEntityActions()) {
+ if (hasTestEntity(action)) {
+ hasAllEntites = true;
+ } else {
+ return false;
+ }
+ }
+ return hasAllEntites;
+ }
+
+ /**
+ * Returns all executed steps of the given {@link TestPlan}.
+ *
+ * @return all executed steps, never {@code null}
+ */
+ Set getAllExecutedSteps() {
+ Set executedSteps = new HashSet();
+ executedSteps.addAll(getCompoundSetupStep().getExecutedSteps());
+ executedSteps.addAll(getCompoundTestStep().getExecutedSteps());
+ return executedSteps;
+ }
+
+ /**
+ * Creates a {@link TestPlan} that contains only the {@link AbstractStep}s contained in the filter.
+ *
+ * @param testPlan
+ * the test plan, must not be {@code null}
+ * @param filter
+ * the filter, must not be {@code null}
+ * @return the new filtered test plan, never {@code null}
+ */
+ static TestPlan createFilteredTestPlan(final TestPlan testPlan, final Collection filter) {
+ List filteredSetupSteps = filterCompoundStep(filter, testPlan.getCompoundSetupStep());
+ List filteredTestSteps = filterCompoundStep(filter, testPlan.getCompoundTestStep());
+ return new TestPlan(filteredSetupSteps, filteredTestSteps);
+ }
+
+ /**
+ * Creates a {@link TestPlan} where all the {@link AbstractStep}s of the {@link CompoundStep}s of the given {@link TestPlan} are put in reverse order.
+ *
+ * @param testPlan
+ * the test plan, must not be {@code null}
+ * @return the test reverse test plan, never {@code null}
+ */
+ static TestPlan createReverseTestPlan(final TestPlan testPlan) {
+ List setupSteps = testPlan.getCompoundSetupStep().getSteps();
+ Collections.reverse(setupSteps);
+ List testSteps = testPlan.getCompoundTestStep().getSteps();
+ Collections.reverse(testSteps);
+ return new TestPlan(setupSteps, testSteps);
+ }
+
+ /**
+ * Filters a {@link CompoundStep} and returns a list of {@link AbstractStep}s that are contained in the filter {@link Set}.
+ *
+ * @param filter
+ * the filter, must not be {@code null}
+ * @param compoundStep
+ * the compound step, must not be {@code null}
+ * @return the list of filtered {@link AbstractStep}s, never {@code null}
+ */
+ private static List filterCompoundStep(final Collection filter, final CompoundStep compoundStep) {
+ List steps = new ArrayList();
+ for (AbstractStep step : compoundStep.getSteps()) {
+ if (filter.contains(step)) {
+ steps.add(step);
+ }
+ }
+ return steps;
+ }
+
+ /**
+ * Creates a {@link TestPlan} that contains the undo-steps of all {@link AbstractStep}s of the given {@link TestPlan}.
+ *
+ * @param testPlan
+ * the test plan, must not be {@code null}
+ * @return the test plan containing all undo-steps, never {@code null}
+ */
+ static TestPlan createUndoStepsTestPlan(final TestPlan testPlan) {
+ List setupUndoSteps = getAllUndoSteps(testPlan.getCompoundSetupStep().getSteps());
+ List testUndoSteps = getAllUndoSteps(testPlan.getCompoundTestStep().getSteps());
+ return new TestPlan(setupUndoSteps, testUndoSteps);
+ }
+
+ /**
+ * Utility method that returns a list of all undo steps from a list of {@link AbstractStep}.
+ *
+ * @param stepsToUndo
+ * list of steps to undo, must not be {@code null}
+ * @return list of undo steps in reverse order, never {@code null}
+ */
+ private static List getAllUndoSteps(final List stepsToUndo) {
+ List undoSteps = new ArrayList();
+ for (AbstractStep step : stepsToUndo) {
+ if (!NullStep.INSTANCE.equals(step.getUndoStep())) {
+ undoSteps.add(step.getUndoStep());
+ }
+ }
+ return undoSteps;
+ }
+
+ /**
+ * Utility method that returns a list of steps steps of the previous test that need to be undone before executing the current test.
+ *
+ * @param previousSteps
+ * previous steps, must not be {@code null}
+ * @param testPlan
+ * current test plan, must not be {@code null}
+ * @return list of steps to be undone, never {@code null}
+ */
+ private static List computeStepsToUndo(final List previousSteps, final TestPlan testPlan) {
+ List stepsToUndo = new ArrayList();
+ ListIterator reverseIterator = previousSteps.listIterator(previousSteps.size());
+ while (reverseIterator.hasPrevious()) {
+ AbstractStep step = reverseIterator.previous();
+ if (testPlan.isStepToUndo(step)) {
+ stepsToUndo.add(step.getUndoStep());
+ }
+ }
+ return stepsToUndo;
+ }
+
+ /**
+ * Utility method that checks if is step to undo.
+ *
+ * @param step
+ * step, must not be {@code null}
+ * @return true, if is step to undo
+ */
+ private boolean isStepToUndo(final AbstractStep step) {
+ // Undo all steps that don't influence ITestEntities and have been executed
+ if (!(step instanceof ITestEntityActionProvider)) {
+ return true;
+ }
+
+ for (final TestEntityAction testEntityAction : ((ITestEntityActionProvider) step).getTestEntityActions()) {
+ if (!hasTestEntity(testEntityAction)) { // Undo the step, if one TestEntity does not match.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if this {@link TestPlan} contains the {@link ITestEntity} of the given {@link TestEntityAction}.
+ *
+ * @param testEntityAction
+ * the test entity action, must not be {@code null}
+ * @return whether this {@link TestPlan} has {@link ITestEntity} of the given {@link TestEntityAction}
+ */
+ private boolean hasTestEntity(final TestEntityAction testEntityAction) {
+ for (TestEntityAction action : getAllTestEntityActions()) {
+ if (action.hasSameTestEntity(testEntityAction)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a {@link TestPlan} with the steps that actually need to be executed for this test.
+ *
+ * @param testPlan
+ * current test plan, must not be {@code null}
+ * @param previousTestPlan
+ * previous test plan, can be {@code null}
+ * @return executable test plan, never {@code null}
+ */
+ static TestPlan createExecutableTestPlan(final TestPlan testPlan, final TestPlan previousTestPlan) {
+ final List setupStepsToExecute = Lists.newArrayList(Collections2.filter(testPlan.getCompoundSetupStep().getSteps(), input -> input != null
+ && isStepToExecute(input, previousTestPlan)));
+ return new TestPlan(setupStepsToExecute, testPlan.getCompoundTestStep().getSteps());
+ }
+
+ /**
+ * Utility method that checks whether the given {@link AbstractStep} should be executed.
+ *
+ * @param setupStep
+ * the setup step, must not be {@code null}
+ * @param previousTestPlan
+ * the previous test plan, must not be {@code null}
+ * @return whether the given {@link AbstractStep} should be executed
+ */
+ private static boolean isStepToExecute(final AbstractStep setupStep, final TestPlan previousTestPlan) {
+ if (setupStep instanceof ITestEntityActionProvider) {
+ // all ITestEntites must match in order not to execute the test step
+ for (final TestEntityAction action : ((ITestEntityActionProvider) setupStep).getTestEntityActions()) {
+ if (!previousTestPlan.hasTestEntity(action)) {
+ return true;
+ }
+ }
+ return false;
+ } // else: steps that don't involve ITestEntities must always be executed
+ return true;
+ }
+
+ /**
+ * Returns the compound setup step.
+ *
+ * @return the compound setup step, never {@code null}
+ */
+ CompoundStep getCompoundSetupStep() {
+ return compoundSetupStep;
+ }
+
+ /**
+ * Returns the compound test step.
+ *
+ * @return the compound test step, never {@code null}
+ */
+ CompoundStep getCompoundTestStep() {
+ return compoundTestStep;
+ }
+}