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

tw.teddysoft.ezspec.keyword.Scenario Maven / Gradle / Ivy

The newest version!
package tw.teddysoft.ezspec.keyword;

import org.junit.jupiter.api.DynamicNode;
import tw.teddysoft.ezspec.exception.EzSpecError;
import tw.teddysoft.ezspec.extension.junit5.*;
import tw.teddysoft.ezspec.keyword.table.Table;
import tw.teddysoft.ezspec.keyword.visitor.SpecificationElement;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Consumer;

import static java.lang.String.format;
import static tw.teddysoft.ezspec.extension.SpecUtils.getReplacedUnderscores;

/**
 * {@code Scenario} is a class for representing Gherkin scenario.
 *
 * @author Teddy Chen
 * @since 1.0
 */
public abstract class Scenario implements SpecificationElement {

    public static final String KEYWORD = "Scenario";
    protected final String name;
    protected final List steps;
    protected Table lookupTable;
    protected ScenarioEnvironment runtime;
    protected int index;

    protected Rule rule;

    public static final String ARG = "$ARGUMENT";


    /**
     * Gets scenario environment of this scenario.
     *
     * @return the scenario environment
     */
    protected ScenarioEnvironment getEnvironment(){
        return runtime;
    }

    /**
     * Instantiates a new Scenario.
     *
     * @param name the name
     */
    public Scenario(String name, Rule rule){
        this(name, new Table(), rule);
    }

    /**
     * Instantiates a new Scenario.
     *
     * @param name       the name of this scenario
     * @param background the background of this scenario
     */
    public Scenario(String name, Background background, Rule rule){
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(background, "background");

        this.name = name;
        steps = new ArrayList<>();
        this.runtime = ScenarioEnvironment.clone(background.getEnvironment());
        this.lookupTable = background.activeTable();
        this.rule = rule;
    }

    /**
     * Instantiates a new Scenario.
     *
     * @param name  the name of this scenario
     * @param table the table of this scenario
     */
    public Scenario(String name, Table table, Rule rule){
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(table, "table");
        Objects.requireNonNull(rule, "rule");

        this.name = name;
        steps = new ArrayList<>();
        this.runtime = ScenarioEnvironment.create();
        this.runtime.setInput(table);
        lookupTable = table;
        this.rule = rule;
    }

    public Scenario withRule(String ruleName){
        Objects.requireNonNull(ruleName);

        var withRule = getFeature().getRule(ruleName);
        if (withRule.isEmpty())
            throw new IllegalArgumentException("Rule not found: " + ruleName);

        getFeature().applyRule(ruleName, this);
        this.runtime = ScenarioEnvironment.clone(withRule.get().getBackground().getEnvironment());
        this.rule = withRule.get();
        return this;
    }

    public Scenario withRule(Rule rule){
        Objects.requireNonNull(rule);

        return withRule(rule.getName());
    }

    private Feature getFeature(){
        return rule.getFeature();
    }

    /**
     * Gets active table of this scenario.
     *
     * @return the table
     */
    public Table activeTable(){
        return lookupTable;
    }

    public String getName() {
        return name;
    }

    /**
     * The method for representing Gherkin Given step of this scenario.
     *
     * @param description the description of Given step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Given step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Given step
     * @return this scenario
     */
//region :Keywords
    public abstract Scenario Given(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Given step of this scenario.
     *
     * @param description the description of Given step
     * @param callback    a lambda expression to represent the implementation
     *                    of this Given step
     * @return this scenario
     */
    public abstract Scenario Given(String description, Consumer callback);

    /**
     * The method for representing Gherkin When step of this scenario.
     *
     * @param description the description of When step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this When step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this When step
     * @return this scenario
     */
    public abstract Scenario When(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin When step of this scenario.
     *
     * @param description the description of When step
     * @param callback    a lambda expression to represent the implementation
     *                    of this When step
     * @return this scenario
     */
    public abstract Scenario When(String description, Consumer callback);

    /**
     * The method for representing Gherkin Then step of this scenario.
     *
     * @param description the description of Then step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Then step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario Then(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Then step of this scenario.
     *
     * @param description the description of Then step
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario Then(String description, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution succeeded before
     * asserting following Then steps.
     *
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Then step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenSuccess(boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution succeeded before
     * asserting following Then steps.
     *
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenSuccess(Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution succeeded before
     * asserting following Then steps.
     *
     * @param description the description of ThenSuccess step
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenSuccess(String description, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution succeeded before
     * asserting following Then steps.
     *
     * @param description the description of ThenSuccess step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Then step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenSuccess(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution failed before
     * asserting following Then steps.
     *
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenFailure(Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution failed before
     * asserting following Then steps.
     *
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Then step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenFailure(boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution failed before
     * asserting following Then steps.
     *
     * @param description the description of ThenFailure step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this Then step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenFailure(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin Scenario execution failed before
     * asserting following Then steps.
     *
     * @param description the description of ThenFailure step
     * @param callback    a lambda expression to represent the implementation
     *                    of this Then step
     * @return this scenario
     */
    public abstract Scenario ThenFailure(String description, Consumer callback);

    /**
     * The method for representing Gherkin And step.
     *
     * @param description the description of And step
     * @param callback    a lambda expression to represent the implementation
     *                    of this And step
     * @return this scenario
     */
    public abstract Scenario And(String description, Consumer callback);

    /**
     * The method for representing Gherkin And step.
     *
     * @param description the description of And step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this And step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this And step
     * @return this scenario
     */
    public abstract Scenario And(String description, boolean continuous, Consumer callback);

    /**
     * The method for representing Gherkin But step.
     *
     * @param description the description of But step
     * @param callback    a lambda expression to represent the implementation
     *                    of this But step
     * @return this scenario
     */
    public abstract Scenario But(String description, Consumer callback);

    /**
     * The method for representing Gherkin But step.
     *
     * @param description the description of But step
     * @param continuous  the parameter for deciding to continue executing
     *                    the next step after this But step failed
     * @param callback    a lambda expression to represent the implementation
     *                    of this But step
     * @return this scenario
     */
    public abstract Scenario But(String description, boolean continuous, Consumer callback);
    //endregion :Keywords

    /**
     * A static method for formatting the name.
     *
     * @param originName the origin name
     * @return the string
     */
    protected static String replaceName(String originName){
        return originName.replace("_", " ")
                .replace("$dot$", ".")
                .replace("$parenthesis$", "()")
                .replace("$comma$", ",");

    }

    public String getDisplayName(){
        return replaceName(getName());
    }

    /**
     * Gets test case enclosing name.
     *
     * @return the test case enclosing name
     */
    public static String getEnclosingMethodName() {
        StackWalker walker = StackWalker.getInstance();
        Optional methodName = walker.walk(frames -> frames
                .skip(2)
                .findFirst()
                .map(StackWalker.StackFrame::getMethodName));

        var cookedMethodName = methodName.get();
        if (cookedMethodName.startsWith("lambda$")){
            cookedMethodName = cookedMethodName.replace("lambda$", "");
            cookedMethodName = cookedMethodName.substring(0, cookedMethodName.lastIndexOf("$"));
        }
        return cookedMethodName;
    }

    public static MethodInfo getEnclosingMethodInfo() {
        StackWalker walker = StackWalker.getInstance();
        Optional methodName = walker.walk(frames -> frames
                .skip(2)
                .findFirst()
                .map(StackWalker.StackFrame::getMethodName));

        var className = walker.walk(frames -> frames
                .skip(2)
                .findFirst()
                .map(StackWalker.StackFrame::getClassName));

        var cookedMethodName = methodName.get();
        if (cookedMethodName.startsWith("lambda$")){
            cookedMethodName = cookedMethodName.replace("lambda$", "");
            cookedMethodName = cookedMethodName.substring(0, cookedMethodName.lastIndexOf("$"));
        }
        MethodInfo methodInfo = new MethodInfo(methodName.get(), cookedMethodName, getRuleName(className.get(), methodName.get()));
        return methodInfo;
    }

    private static String getRuleName(String classNAme, String methodName) {
        Method method;
        Class cls;
        try{
            cls = Class.forName(classNAme);
            method = cls.getMethod(methodName);
            method.setAccessible(true);
        }
        catch (NoSuchMethodException | ClassNotFoundException e){
            throw new RuntimeException(e.getMessage(), e);
        }

        Annotation[] methodAnnotations = method.getDeclaredAnnotations();
        for (Annotation annotation : methodAnnotations) {
            switch (annotation){
                case EzScenario e when !e.rule().isEmpty() -> {return e.rule();}
                case EzScenarioOutline e when !e.rule().isEmpty() -> {return e.rule();}
                case EzDynamicScenario e when !e.rule().isEmpty() -> {return e.rule();}
                case EzDynamicScenarioOutline e when !e.rule().isEmpty() -> {return e.rule();}
                default -> {}
            }
        }

        Annotation[] classAnnotations = cls.getDeclaredAnnotations();
        for (Annotation annotation : classAnnotations) {
            if (annotation instanceof EzRule ezRule) {
                return ezRule.value();
            }
        }
        return "";
    }

    /**
     * Gets steps in this scenario.
     *
     * @return the steps
     */
    public List steps(){
        return Collections.unmodifiableList(steps);
    }

    /**
     * Gets steps in this scenario.
     *
     * @return the steps
     */
    public List getSteps() {
        return steps;
    }

    /**
     * Gets the scenario name with underscores replaced by space.
     *
     * @return the replaced name
     */
    public String getReplacedUnderscoresName(){
        return getReplacedUnderscores(getName());
    }

    /**
     * Execute the Given/When/Then step and
     * its following And/But steps concurrently.
     */
    public abstract void ExecuteConcurrently();

    /**
     * Execute scenario with Junit{@code DynamicNode}.
     *
     * @return the dynamic node
     */
    public abstract DynamicNode DynamicExecute();

    /**
     * Execute steps in Junit{@code DynamicNode}.
     *
     * @param step the step
     * @throws Throwable the throwable when step failed
     */
    protected void dynamicExecuteStep(Step step) throws Throwable {
        if (step.getResult().isSuccess()) return;

        switch (step.getResult().getExecutionOutcome()) {
            case Pending : return;
            case Success : return;
            case Failure : throw step.getResult().getException();
            case Skipped : return;
            default : {}
        }
    }

    /**
     * Execute this scenario.
     */
    public abstract void Execute();

    /**
     * Builds spec errors as an {@code EzSpecError}.
     *
     * @param steps the steps
     * @return the {@code EzSpecError}
     */
    protected Optional buildSpecError(List steps){
        List throwables = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        int errorCount = 0;
        for(var step : steps){
            if (step.getResult().isFailure()) {
                errorCount = errorCount + 1;
                throwables.add(step.getResult().getException());
                var failureMessage = step.getResult().getFailureMessage();
                sb.append(format("\n[%d] %s", errorCount, failureMessage));
            }
        }

        if (throwables.isEmpty()){
            return Optional.empty();
        }

        return Optional.of(new EzSpecError(sb.toString()));
    }

    public record MethodInfo(
            String methodName,
            String cookedMethodName,
            String ruleName
    ){}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy