All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ocadotechnology.scenario.UnorderedSteps Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2017-2024 Ocado (Ocava)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ocadotechnology.scenario;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.ParametersAreNonnullByDefault;

import org.apache.commons.lang3.ArrayUtils;
import org.junit.jupiter.api.Assertions;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.ocadotechnology.simulation.Simulation;

/**
 * Collection of steps used for manipulating previously defined unordered/never/sequenced steps.
 */
@ParametersAreNonnullByDefault
public final class UnorderedSteps {
    private final StepCache stepCache;
    private final StepManager stepManager;
    private final NamedStepExecutionType namedStepExecutionType;

    private UnorderedSteps(StepManager stepManager, NamedStepExecutionType namedStepExecutionType) {
        this.stepManager = stepManager;
        this.stepCache = stepManager.getStepsCache();
        this.namedStepExecutionType = namedStepExecutionType;
    }

    public UnorderedSteps(StepManager stepManager) {
        this(stepManager, NamedStepExecutionType.ordered());
    }

    /**
     * @return an instance of UnorderedSteps where the steps it creates will use the supplied NamedStepExecutionType
     *          object. Used in composite steps which contain an {@link UnorderedSteps} instance.
     */
    public UnorderedSteps modify(NamedStepExecutionType executionType) {
        return new UnorderedSteps<>(stepManager, executionType);
    }

    /**
     * @return an instance of UnorderedSteps where the steps it creates will use a NamedStepExecutionType object created
     *          from the supplied CheckStepExecutionType object. Used in composite steps which contain an
     *          {@link UnorderedSteps} instance.
     */
    public UnorderedSteps modify(CheckStepExecutionType executionType) {
        return modify(executionType.getNamedStepExecutionType());
    }

    /**
     * @return an instance of the UnorderedSteps where the steps it creates has the
     * {@code NamedStepExecutionType.isFailingStep} flag set to true. The failingStep flag is checked after the scenario
     * test has completed successfully or exceptionally and should be used in conjunction with {@link FixRequired}
     *
     * @throws IllegalStateException if called after a previous invocation of this method
     */
    public UnorderedSteps failingStep() {
        return new UnorderedSteps<>(stepManager, NamedStepExecutionType.failingStep().merge(namedStepExecutionType));
    }

    /**
     * @return an instance of the UnorderedSteps where the steps it creates are linked to
     * create an ordered sub-sequence with other steps of the same name.
     *
     * @throws IllegalStateException if called after a previous invocation of this method
     * @throws NullPointerException if the name is null
     */
    public UnorderedSteps sequenced(String name) {
        return new UnorderedSteps<>(stepManager, NamedStepExecutionType.sequenced(name).merge(namedStepExecutionType));
    }

    private void addSimpleExecuteStep(Runnable runnable) {
        addExecuteStep(new SimpleExecuteStep(runnable));
    }

    private void addExecuteStep(ExecuteStep step) {
        stepManager.add(step, namedStepExecutionType);
    }

    /**
     * Used to remove an unordered step by name. Useful for removing a "never" step that no longer applies.
     *
     * @param stepName the name of step to remove
     */
    public void removesUnorderedSteps(String stepName) {
        removeSteps(stepName);
    }

    /**
     * Used to remove more than one unordered step by name. Useful for removing a "never" step that no longer applies.
     *
     * @param stepName the name of step to remove
     * @param otherStepNames the names of additional steps to remove
     */
    public void removesUnorderedSteps(String stepName, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, stepName);

        removeSteps(names);
    }

    private void removeSteps(String... names) {
        Preconditions.checkState(names.length > 0, "names length must be greater than zero");
        addSimpleExecuteStep(() -> Stream.of(names).forEach(stepCache::removeAndCancel));
    }

    /**
     * Like {@link #removesUnorderedSteps}, except continues even if the step was never added.
     *
     * @param stepName the name of step to try remove
     */
    public void removesUnorderedStepsIfPresent(String stepName) {
        removeStepsIfPresent(stepName);
    }

    /**
     * Like {@link #removesUnorderedSteps}, except continues even if the step was never added.
     *
     * @param stepName the name of step to try remove
     * @param otherStepNames the names of additional steps to remove
     */
    public void removesUnorderedStepsIfPresent(String stepName, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, stepName);

        removeStepsIfPresent(names);
    }

    private void removeStepsIfPresent(String... names) {
        Preconditions.checkState(names.length > 0, "names length must be greater than zero");
        addSimpleExecuteStep(() -> Stream.of(names).forEach(stepCache::removeAndCancelIfPresent));
    }

    /**
     * Used to wait for a single unordered step specified by name, to fix the group specified by the name to have
     * happened before subsequent steps. This is not required if there are no subsequent steps (the scenario test will
     * wait for the step anyway).
     *
     * @param stepName the name of step to wait for
     */
    public void waitForSteps(String stepName) {
        waitForAll(stepName);
    }

    /**
     * Used to wait for unordered steps specified by name, to fix the group specified by the names to have to have
     * happened before subsequent steps. This is not required if there are no subsequent steps (the scenario test will
     * wait for the steps anyway).
     *
     * @param stepName the name of step to wait for
     * @param otherStepNames the names of the additional steps to wait for
     */
    public void waitForSteps(String stepName, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, stepName);

        waitForAll(names);
    }

    /**
     * Used to wait for unordered steps specified by name, to fix the group specified by the names to have to have
     * happened before subsequent steps. This is not required if there are no subsequent steps (the scenario test will
     * wait for the steps anyway).
     *
     * @param stepNames the names of steps to wait for, must not be empty
     */
    public void waitForSteps(Set stepNames) {
        waitForAll(stepNames.toArray(new String[stepNames.size()]));
    }

    private void waitForAll(String... names) {
        Preconditions.checkState(names.length > 0, "names length must be greater than zero");

        addExecuteStep(new ExecuteStep() {
            private final List waitSteps = new LinkedList<>(Arrays.asList(names));

            @Override
            protected void executeStep() {
                Assertions.assertTrue(waitSteps.stream().allMatch(stepCache::hasAddedStepWithName),
                        "Not all steps that we are waiting for have been previously added. Waiting for: "
                                + waitSteps + ", previously added: " + stepCache.getAllUnorderedStepNames());
                waitSteps.removeIf(stepCache::isUnorderedStepFinished);
            }

            @Override
            public boolean isFinished() {
                return waitSteps.isEmpty();
            }

            @Override protected String info() {
                return waitSteps.toString();
            }
        });
    }

    /**
     * Like {@link #waitForSteps}, except continues even if the step was never added.
     *
     * @param stepName the name of step to try to wait for
     */
    public void waitForStepsIfPresent(String stepName) {
        waitForAllIfPresent(stepName);
    }

    /**
     * Like {@link #waitForSteps}, except continues even if the step was never added.
     *
     * @param stepName the name of step to try to wait for
     * @param otherStepNames the names of additional steps to try to wait for
     */
    public void waitForStepsIfPresent(String stepName, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, stepName);

        waitForAllIfPresent(names);
    }

    private void waitForAllIfPresent(String... names) {
        Preconditions.checkState(names.length > 0, "names length must be greater than zero");

        addExecuteStep(new ExecuteStep() {
            private final List waitSteps = new LinkedList<>(Arrays.asList(names));

            @Override
            protected void executeStep() {
                waitSteps.removeIf(stepCache::isUnorderedStepFinished);
            }

            @Override
            public boolean isFinished() {
                return waitSteps.isEmpty();
            }

            @Override protected String info() {
                return waitSteps.toString();
            }
        });
    }

    /**
     * Used to wait for any of a list of unordered steps to have happened before subsequent steps. This allows an OR of
     * unordered steps to be done e.g. for scenarios with multiple valid sequences of events.
     *
     * @param a name of a step
     * @param b name of another step
     *
     * @return the list of steps that have finished and therefore caused this wait step to finish
     */
    public StepFuture> waitForAnyOfSteps(String a, String b) {
        return waitForAny(a, b);
    }

    /**
     * Used to wait for any of a list of unordered steps to have happened before subsequent steps. This allows an OR of
     * unordered steps to be done e.g. for scenarios with multiple valid sequences of events.
     *
     * @param a name of a step
     * @param b name of another step
     * @param otherStepNames the names of any additional optional steps to include, optional
     *
     * @return the list of steps that have finished and therefore caused this wait step to finish
     */
    public StepFuture> waitForAnyOfSteps(String a, String b, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, a, b);

        return waitForAny(names);
    }

    private StepFuture> waitForAny(String... names) {
        Preconditions.checkState(names.length >= 2, "names length should be at least two");

        MutableStepFuture> finishedStepsFuture = new MutableStepFuture<>();

        addExecuteStep(new ExecuteStep() {
            private List waitSteps = ImmutableList.copyOf(names);
            private boolean isComplete = false;

            @Override
            protected void executeStep() {
                Assertions.assertTrue(waitSteps.stream().allMatch(stepCache::hasAddedStepWithName),
                        "Not all steps that we are waiting for have been previously added. Waiting for: "
                                + waitSteps + ", previously added: " + stepCache.getAllUnorderedStepNames());

                List finishedSteps = waitSteps.stream()
                        .filter(stepCache::isUnorderedStepFinished)
                        .collect(Collectors.toList());
                if (!finishedSteps.isEmpty()) {
                    finishedStepsFuture.populate(finishedSteps);
                    waitSteps.forEach(stepCache::removeAndCancelIfPresent);
                    isComplete = true;
                }
            }

            @Override
            public boolean isFinished() {
                return isComplete;
            }

            @Override protected String info() {
                return "Waiting for any of steps " + waitSteps + " to finish; have any finished = " + isComplete;
            }
        });

        return finishedStepsFuture;
    }

    /**
     * Asserts that the steps specified by name are finished by the time this step executes.
     *
     * @param stepName the name of step to check
     */
    public void allStepsAreAlreadyFinished(String stepName) {
        allStepsFinished(stepName);
    }

    /**
     * Asserts that the steps specified by name are finished by the time this step executes.
     *
     * @param stepName the name of step to check
     * @param otherStepNames the names of other steps to check
     */
    public void allStepsAreAlreadyFinished(String stepName, String... otherStepNames) {
        String[] names = ArrayUtils.insert(0, otherStepNames, stepName);

        allStepsFinished(names);
    }

    private void allStepsFinished(String... names) {
        Preconditions.checkState(names.length > 0, "names length must be greater than zero");

        addSimpleExecuteStep(() -> {
            ImmutableList unfinished = Stream.of(names).filter(name -> !stepCache.isUnorderedStepFinished(name)).collect(ImmutableList.toImmutableList());
            Assertions.assertTrue(unfinished.isEmpty(), "Steps " + unfinished + " are not finished");
        });
    }

    /**
     * Asserts that step a is finished before step b, unless they are both finished before this step executes, in which
     * case, there is currently no way to tell. FIXME: "correct" this behaviour?
     *
     * @param a the name of the step which should finish first
     * @param b the name of the step which should finish second
     */
    public void stepAIsFinishedBeforeStepB(String a, String b) {
        ExecuteStep step = new ExecuteStep() {
            private boolean finished = false;

            @Override
            protected void executeStep() {
                boolean isAFinished = stepCache.isUnorderedStepFinished(a);
                boolean isBFinished = stepCache.isUnorderedStepFinished(b);
                if (isAFinished && isBFinished) {
                    finished = true;
                } else if (isAFinished || isBFinished) {
                    Assertions.assertFalse(isBFinished, "Step " + b + " has been finished before step " + a);
                }
            }

            @Override
            public boolean isFinished() {
                return finished;
            }
        };
        addExecuteStep(step);
    }
}