From 65f631cfc95dcb8d3b56f33c5dcf86eaf7d52afc Mon Sep 17 00:00:00 2001 From: U004458 Date: Fri, 27 Mar 2026 09:22:13 +0800 Subject: [PATCH 1/2] test: Migrate AbstractSystemTest to junit5 Issue: ACF-14152 --- .../META-INF/MANIFEST.MF | 4 +- .../tools/ddk/test/core/AbstractTestStep.java | 4 +- .../tools/ddk/test/core/CompoundStep.java | 2 +- .../test/core/jupiter/AbstractSystemTest.java | 325 ++++++++++++++ .../core/jupiter/AbstractTestWatchman.java | 45 ++ .../core/jupiter/MultipleTestProblems.java | 149 +++++++ .../tools/ddk/test/core/jupiter/TestPlan.java | 414 ++++++++++++++++++ 7 files changed, 939 insertions(+), 4 deletions(-) create mode 100644 com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractSystemTest.java create mode 100644 com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/AbstractTestWatchman.java create mode 100644 com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/MultipleTestProblems.java create mode 100644 com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/TestPlan.java 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..c73ef60987 --- /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(final 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; + } +} From 6525d06295f5ed65b2606f13c5bf8258b9584c7e Mon Sep 17 00:00:00 2001 From: rubenporras <43636626+rubenporras@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:26:54 +0100 Subject: [PATCH 2/2] Apply suggestion from @rubenporras --- .../tools/ddk/test/core/jupiter/AbstractTestWatchman.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c73ef60987..eb4a4945e0 100644 --- 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 @@ -31,7 +31,7 @@ public void beforeEach(final ExtensionContext context) throws Exception { } @Override - public abstract void testSuccessful(final ExtensionContext context); + public abstract void testSuccessful(ExtensionContext context); @Override public void testFailed(final ExtensionContext context, @Nullable final Throwable cause) {