PerformableTree.java
package org.jbehave.core.embedder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.function.FailableRunnable;
import org.jbehave.core.annotations.ScenarioType;
import org.jbehave.core.annotations.Scope;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.embedder.MatchingStepMonitor.StepMatch;
import org.jbehave.core.failures.BatchFailures;
import org.jbehave.core.failures.FailingUponPendingStep;
import org.jbehave.core.failures.IgnoringStepsFailure;
import org.jbehave.core.failures.PendingStepFound;
import org.jbehave.core.failures.PendingStepsFound;
import org.jbehave.core.failures.RestartingScenarioFailure;
import org.jbehave.core.failures.RestartingStoryFailure;
import org.jbehave.core.failures.UUIDExceptionWrapper;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.GivenStories;
import org.jbehave.core.model.GivenStory;
import org.jbehave.core.model.Lifecycle;
import org.jbehave.core.model.Lifecycle.ExecutionType;
import org.jbehave.core.model.Meta;
import org.jbehave.core.model.Scenario;
import org.jbehave.core.model.Story;
import org.jbehave.core.model.StoryDuration;
import org.jbehave.core.reporters.ConcurrentStoryReporter;
import org.jbehave.core.reporters.DelegatingStoryReporter;
import org.jbehave.core.reporters.StoryReporter;
import org.jbehave.core.steps.AbstractStepResult;
import org.jbehave.core.steps.PendingStepMethodGenerator;
import org.jbehave.core.steps.Step;
import org.jbehave.core.steps.StepCollector;
import org.jbehave.core.steps.StepCollector.Stage;
import org.jbehave.core.steps.StepCreator.PendingStep;
import org.jbehave.core.steps.StepCreator.StepExecutionType;
import org.jbehave.core.steps.StepResult;
import org.jbehave.core.steps.Timer;
import org.jbehave.core.steps.Timing;
import org.jbehave.core.steps.context.StepsContext;
/**
* Creates a tree of {@link Performable} objects for a set of stories, grouping
* sets of performable steps for each story and scenario, and adding before and
* after stories steps. The process has two phases:
* <ol>
* <li>The tree is populated with groups of performable steps when the stories
* are added via the {@link #addStories(RunContext, List)} method.</li>
* <li>The performable steps are then populated with the results when the
* {@link #performBeforeOrAfterStories(RunContext, Stage)} and
* {@link #perform(RunContext, Story)} methods are executed.</li>
* </ol>
* The tree is created per {@link RunContext} for the set of stories being run
* but the individual stories can be performed concurrently.
*/
public class PerformableTree {
private static final Map<String, String> NO_PARAMETERS = new HashMap<>();
private PerformableRoot root = new PerformableRoot();
public PerformableRoot getRoot() {
return root;
}
public void addStories(RunContext context, List<Story> stories) {
root.addBeforeSteps(context.beforeStoriesSteps());
for (Story story : stories) {
root.add(performableStory(context, story, NO_PARAMETERS));
}
root.addAfterSteps(context.afterStoriesSteps());
}
private PerformableStory performableStory(RunContext context, Story story, Map<String, String> storyParameters) {
PerformableStory performableStory = new PerformableStory(story, context.configuration().keywords(),
context.givenStory());
FilteredStory filteredStory = context.filter(story);
Meta storyMeta = story.getMeta();
boolean storyExcluded = filteredStory.excluded();
performableStory.excluded(storyExcluded);
if (!storyExcluded) {
Map<Stage, PerformableSteps> lifecycleSteps = context.lifecycleSteps(story.getLifecycle(), storyMeta,
Scope.STORY, new HashMap<>()); // TODO: Rework to use ExamplesTable row here
performableStory.addBeforeSteps(ExecutionType.SYSTEM, context.beforeStorySteps(storyMeta));
performableStory.addBeforeSteps(ExecutionType.USER, lifecycleSteps.get(Stage.BEFORE));
performableStory.addAll(performableScenarios(context, story, storyParameters, filteredStory));
// Add Given stories only if story contains non-filtered scenarios
if (performableStory.hasIncludedScenarios()) {
Map<String, String> givenStoryParameters = new HashMap<>(storyParameters);
addMetaParameters(givenStoryParameters, storyMeta);
performableStory.setGivenStories(performableGivenStories(context, story.getGivenStories(),
givenStoryParameters));
}
performableStory.addAfterSteps(ExecutionType.USER, lifecycleSteps.get(Stage.AFTER));
performableStory.addAfterSteps(ExecutionType.SYSTEM, context.afterStorySteps(storyMeta));
}
return performableStory;
}
private List<PerformableScenario> performableScenarios(RunContext context, Story story,
Map<String, String> storyParameters, FilteredStory filterContext) {
List<PerformableScenario> performableScenarios = new ArrayList<PerformableScenario>();
ExamplesTable storyExamplesTable = story.getLifecycle().getExamplesTable();
List<Map<String, String>> storyExamplesTableRows;
if (storyExamplesTable.isEmpty()) {
storyExamplesTableRows = new ArrayList<Map<String, String>>();
storyExamplesTableRows.add(new HashMap<String, String>());
} else {
storyExamplesTableRows = storyExamplesTable.getRows();
}
// determine if before and after scenario steps should be run
boolean runBeforeAndAfterScenarioSteps = shouldRunBeforeOrAfterScenarioSteps(context);
for (Map<String, String> storyExamplesTableRow : storyExamplesTableRows) {
for (Map.Entry<String, String> entry : storyExamplesTableRow.entrySet()) {
entry.setValue((String)
context.configuration().parameterConverters().convert(entry.getValue(), String.class));
}
}
for (int i = 0; i < storyExamplesTableRows.size(); i++) {
Map<String, String> storyExamplesTableRow = storyExamplesTableRows.get(i);
for (Scenario scenario : story.getScenarios()) {
Map<String, String> scenarioParameters = new HashMap<String, String>(storyParameters);
PerformableScenario performableScenario = performableScenario(context, story, scenarioParameters,
filterContext, runBeforeAndAfterScenarioSteps, scenario, storyExamplesTableRow,
storyExamplesTable.isEmpty() ? -1 : i);
if (performableScenario.isPerformable()) {
performableScenarios.add(performableScenario);
}
}
}
return performableScenarios;
}
private PerformableScenario performableScenario(RunContext context, Story story,
Map<String, String> storyParameters, FilteredStory filterContext, boolean runBeforeAndAfterScenarioSteps,
Scenario originalScenario, Map<String, String> storyExamplesTableRow, int storyExamplesTableRowIndex) {
Scenario scenario = originalScenario;
if (storyExamplesTableRowIndex != -1) {
scenario = new Scenario(scenario.getTitle() + " [" + (storyExamplesTableRowIndex + 1) + "]",
scenario.getMeta(), scenario.getGivenStories(), scenario.getExamplesTable(), scenario.getSteps());
}
PerformableScenario performableScenario = new PerformableScenario(scenario, story.getPath());
if (context.failureOccurred() && context.configuration().storyControls().skipScenariosAfterFailure()) {
return performableScenario;
}
boolean scenarioExcluded = filterContext.excluded(originalScenario);
performableScenario.excluded(scenarioExcluded);
if (!scenarioExcluded) {
Meta storyAndScenarioMeta = scenario.getMeta().inheritFrom(story.getMeta());
if (isParameterisedByExamples(scenario)) {
ExamplesTable table = scenario.getExamplesTable();
List<Map<String, String>> tableRows = table.getRows();
for (int exampleIndex = 0; exampleIndex < tableRows.size(); exampleIndex++) {
Map<String, String> scenarioParameters = tableRows.get(exampleIndex);
Map<String, String> scenarioParametersCopy = new HashMap<String, String>(storyParameters);
scenarioParametersCopy.putAll(storyExamplesTableRow);
scenarioParametersCopy.putAll(scenarioParameters);
for (Map.Entry<String, String> scenarioParameterEntry : scenarioParametersCopy.entrySet()) {
String value = context.configuration().parameterControls().replaceAllDelimitedNames(
scenarioParameterEntry.getValue(), storyExamplesTableRow);
scenarioParameterEntry.setValue((String) context.configuration().parameterConverters()
.convert(value, String.class));
}
Map<String, String> parameters = new LinkedHashMap<String, String>(scenarioParametersCopy);
for (Map.Entry<String, String> storyExamplesTableRowEntry: storyExamplesTableRow.entrySet()) {
String key = storyExamplesTableRowEntry.getKey();
if (!parameters.containsKey(key)) {
parameters.put(key, storyExamplesTableRowEntry.getValue());
}
}
addExampleScenario(context, scenario, performableScenario, story, storyAndScenarioMeta,
parameters, exampleIndex);
}
} else if (!storyExamplesTableRow.isEmpty()) {
addExampleScenario(context, scenario, performableScenario, story, storyAndScenarioMeta,
new HashMap<>(storyExamplesTableRow), -1);
} else { // plain old scenario
NormalPerformableScenario normalScenario = normalScenario(context, story, scenario,
storyAndScenarioMeta, storyParameters);
// run before scenario steps, if allowed
if (runBeforeAndAfterScenarioSteps) {
normalScenario.addBeforeSteps(ExecutionType.SYSTEM,
context.beforeScenarioSteps(storyAndScenarioMeta, ScenarioType.NORMAL));
}
performableScenario.useNormalScenario(normalScenario);
// after scenario steps, if allowed
if (runBeforeAndAfterScenarioSteps) {
normalScenario.addAfterSteps(ExecutionType.SYSTEM,
context.afterScenarioSteps(storyAndScenarioMeta, ScenarioType.NORMAL));
}
}
}
return performableScenario;
}
private void addExampleScenario(RunContext context, Scenario scenario, PerformableScenario performableScenario,
Story story, Meta storyAndScenarioMeta, Map<String, String> parameters, int exampleIndex) {
Meta exampleScenarioMeta = parameterMeta(context, parameters).inheritFrom(storyAndScenarioMeta);
if (!context.filter().excluded(exampleScenarioMeta)) {
ExamplePerformableScenario exampleScenario = exampleScenario(context, story, scenario,
storyAndScenarioMeta, parameters, exampleIndex);
performableScenario.addExampleScenario(exampleScenario);
}
}
private NormalPerformableScenario normalScenario(RunContext context, Story story, Scenario scenario,
Meta storyAndScenarioMeta, Map<String, String> storyParameters) {
NormalPerformableScenario normalScenario = new NormalPerformableScenario(story);
normalScenario.setStoryAndScenarioMeta(storyAndScenarioMeta);
addStepsWithLifecycle(normalScenario, context, story.getLifecycle(), storyParameters,
scenario, storyAndScenarioMeta);
return normalScenario;
}
private ExamplePerformableScenario exampleScenario(RunContext context, Story story, Scenario scenario,
Meta storyAndScenarioMeta, Map<String, String> parameters, int exampleIndex) {
ExamplePerformableScenario exampleScenario = new ExamplePerformableScenario(story, parameters, exampleIndex);
exampleScenario.setStoryAndScenarioMeta(storyAndScenarioMeta);
exampleScenario.addBeforeSteps(ExecutionType.SYSTEM,
context.beforeScenarioSteps(storyAndScenarioMeta, ScenarioType.EXAMPLE));
addStepsWithLifecycle(exampleScenario, context, story.getLifecycle(), parameters, scenario,
storyAndScenarioMeta);
exampleScenario.addAfterSteps(ExecutionType.SYSTEM,
context.afterScenarioSteps(storyAndScenarioMeta, ScenarioType.EXAMPLE));
return exampleScenario;
}
private Meta parameterMeta(RunContext context, Map<String, String> parameters) {
Meta meta = Meta.EMPTY;
Keywords keywords = context.configuration().keywords();
String metaText = keywords.meta();
if (parameters.containsKey(metaText)) {
meta = Meta.createMeta(parameters.get(metaText), keywords);
}
return meta;
}
private void addStepsWithLifecycle(AbstractPerformableScenario performableScenario, RunContext context,
Lifecycle lifecycle, Map<String, String> parameters, Scenario scenario, Meta storyAndScenarioMeta) {
Map<Stage, PerformableSteps> lifecycleSteps = context.lifecycleSteps(lifecycle, storyAndScenarioMeta,
Scope.SCENARIO, parameters);
performableScenario.addBeforeSteps(ExecutionType.SYSTEM,
context.beforeScenarioSteps(storyAndScenarioMeta, ScenarioType.ANY));
performableScenario.addBeforeSteps(ExecutionType.USER, lifecycleSteps.get(Stage.BEFORE));
addMetaParameters(parameters, storyAndScenarioMeta);
performableScenario.setGivenStories(performableGivenStories(context, scenario.getGivenStories(), parameters));
performableScenario.addSteps(context.scenarioSteps(lifecycle, storyAndScenarioMeta, scenario, parameters));
performableScenario.addAfterSteps(ExecutionType.USER, lifecycleSteps.get(Stage.AFTER));
performableScenario.addAfterSteps(ExecutionType.SYSTEM,
context.afterScenarioSteps(storyAndScenarioMeta, ScenarioType.ANY));
}
private PerformableGivenStories performableGivenStories(RunContext context, GivenStories givenStories,
Map<String, String> parameters) {
List<PerformableStory> stories = new ArrayList<>();
if (givenStories.getPaths().size() > 0) {
for (GivenStory givenStory : givenStories.getStories()) {
RunContext childContext = context.childContextFor(givenStory);
// run given story, using any parameters provided
Story story = storyOfPath(context.configuration(), childContext.path());
if (givenStory.hasAnchorParameters()) {
story = storyWithMatchingScenarios(story, givenStory.getAnchorParameters());
}
parameters.putAll(givenStory.getParameters());
stories.add(performableStory(childContext, story, parameters));
}
}
return new PerformableGivenStories(stories, givenStories);
}
private Story storyWithMatchingScenarios(Story story, Map<String,String> parameters) {
if (parameters.isEmpty()) {
return story;
}
List<Scenario> scenarios = new ArrayList<>();
for (Scenario scenario : story.getScenarios()) {
if (matchesParameters(scenario, parameters)) {
scenarios.add(scenario);
}
}
return story.cloneWithScenarios(scenarios);
}
private boolean matchesParameters(Scenario scenario, Map<String, String> parameters) {
Meta meta = scenario.getMeta();
for (String name : parameters.keySet()) {
if (meta.hasProperty(name)) {
return meta.getProperty(name).equals(parameters.get(name));
}
}
return false;
}
/**
* Returns the parsed story from the given path
*
* @param configuration the Configuration used to run story
* @param storyPath the story path
* @return The parsed Story
*/
public Story storyOfPath(Configuration configuration, String storyPath) {
String storyAsText = configuration.storyLoader().loadStoryAsText(storyPath);
return configuration.storyParser().parseStory(storyAsText, storyPath);
}
/**
* Returns the parsed story from the given text
*
* @param configuration the Configuration used to run story
* @param storyAsText the story text
* @param storyId the story Id, which will be returned as story path
* @return The parsed Story
*/
public Story storyOfText(Configuration configuration, String storyAsText, String storyId) {
return configuration.storyParser().parseStory(storyAsText, storyId);
}
private void addMetaParameters(Map<String, String> storyParameters, Meta meta) {
for (String name : meta.getPropertyNames()) {
if (!storyParameters.containsKey(name)) {
storyParameters.put(name, meta.getProperty(name));
}
}
}
private boolean shouldRunBeforeOrAfterScenarioSteps(RunContext context) {
return !context.configuration().storyControls().skipBeforeAndAfterScenarioStepsIfGivenStory()
|| !context.givenStory();
}
private boolean isParameterisedByExamples(Scenario scenario) {
return !scenario.getExamplesTable().isEmpty() && !scenario.getGivenStories().requireParameters();
}
public interface State {
State run(Step step, List<StepResult> results, Keywords keywords, StoryReporter reporter);
RuntimeException getFailure();
}
private static final class FineSoFar implements State {
@Override
public State run(Step step, List<StepResult> results, Keywords keywords, StoryReporter reporter) {
State state;
StepResult result;
int indexOfResult;
try {
result = step.perform(reporter, getFailure());
UUIDExceptionWrapper stepFailure = result.getFailure();
state = stepFailure == null ? this : new SomethingHappened(stepFailure);
} catch (IgnoringStepsFailure e) {
result = AbstractStepResult.ignorable(step.asString(keywords));
state = new Ignoring(e);
} catch (PendingStepFound e) {
result = AbstractStepResult.pending((PendingStep) step);
state = new Pending(e);
}
indexOfResult = results.size();
results.add(result);
List<Step> composedSteps = step.getComposedSteps();
if (!composedSteps.isEmpty()) {
reporter.beforeComposedSteps();
for (Step composedStep : composedSteps) {
state = state.run(composedStep, results, keywords, reporter);
}
reporter.afterComposedSteps();
}
if (state instanceof Ignoring) {
result = AbstractStepResult.ignorable(step.asString(keywords));
results.set(indexOfResult, result);
}
result.describeTo(reporter);
return state;
}
@Override
public UUIDExceptionWrapper getFailure() {
return null;
}
}
private static final class SomethingHappened implements State {
private UUIDExceptionWrapper failure;
public SomethingHappened(UUIDExceptionWrapper failure) {
this.failure = failure;
}
@Override
public State run(Step step, List<StepResult> results, Keywords keywords, StoryReporter reporter) {
StepResult result = step.doNotPerform(reporter, getFailure());
results.add(result);
result.describeTo(reporter);
return this;
}
@Override
public UUIDExceptionWrapper getFailure() {
return failure;
}
}
private static final class Ignoring implements State {
private final IgnoringStepsFailure failure;
private Ignoring(IgnoringStepsFailure failure) {
this.failure = failure;
}
@Override
public State run(Step step, List<StepResult> results, Keywords keywords, StoryReporter reporter) {
String stepAsString = step.asString(keywords);
reporter.beforeStep(new org.jbehave.core.model.Step(StepExecutionType.IGNORABLE, stepAsString));
StepResult result = AbstractStepResult.ignorable(stepAsString);
results.add(result);
result.describeTo(reporter);
return this;
}
@Override
public IgnoringStepsFailure getFailure() {
return failure;
}
}
private static final class Pending implements State {
private final PendingStepFound failure;
private Pending(PendingStepFound failure) {
this.failure = failure;
}
@Override
public State run(Step step, List<StepResult> results, Keywords keywords, StoryReporter reporter) {
StepResult result = AbstractStepResult.pending((PendingStep) step);
results.add(result);
result.describeTo(reporter);
return this;
}
@Override
public PendingStepFound getFailure() {
return failure;
}
}
public void perform(RunContext context, Story story) {
boolean restartingStory = false;
try {
performCancellable(context, story);
if (context.restartStory()) {
context.reporter().restartedStory(story, context.failure(context.state()));
restartingStory = true;
perform(context, story);
}
} catch (InterruptedException e) {
if (context.isCancelled(story)) {
context.reporter().storyCancelled(story, context.storyDuration(story));
context.reporter().afterStory(context.givenStory);
}
throw new UUIDExceptionWrapper(e);
} finally {
if (!context.givenStory() && !restartingStory) {
invokeDelayedReporters(context.reporter());
}
}
}
private void performCancellable(RunContext context, Story story) throws InterruptedException {
if (context.configuration().storyControls().resetStateBeforeStory()) {
context.resetState();
context.resetFailures(story);
}
if (!story.getPath().equals(context.path())) {
context.currentPath(story.getPath());
}
if (context.configuration.dryRun()) {
context.reporter().dryRun();
}
root.get(story).perform(context);
if (context.failureOccurred()) {
context.addFailure(story);
}
}
public void performBeforeOrAfterStories(RunContext context, Stage stage) {
String storyPath = StringUtils.capitalize(stage.name().toLowerCase()) + "Stories";
context.currentPath(storyPath);
context.reporter().beforeStoriesSteps(stage);
try {
(stage == Stage.BEFORE ? root.beforeSteps : root.afterSteps).perform(context, false);
} catch (InterruptedException e) {
throw new UUIDExceptionWrapper(e);
} finally {
context.reporter().afterStoriesSteps(stage);
invokeDelayedReporters(context.reporter());
}
}
private void invokeDelayedReporters(StoryReporter reporter) {
if (reporter instanceof ConcurrentStoryReporter) {
((ConcurrentStoryReporter) reporter).invokeDelayed();
} else if (reporter instanceof DelegatingStoryReporter) {
for (StoryReporter delegate : ((DelegatingStoryReporter) reporter).getDelegates()) {
invokeDelayedReporters(delegate);
}
}
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
/**
* The context for running a story.
*/
public static class RunContext {
private final Configuration configuration;
private final boolean givenStory;
private final AllStepCandidates allStepCandidates;
private final EmbedderMonitor embedderMonitor;
private final MetaFilter filter;
private final BatchFailures failures;
private final StepsContext stepsContext;
private final Map<Story, StoryDuration> cancelledStories = new HashMap<>();
private final Map<String, List<PendingStep>> pendingStories = new HashMap<>();
private final ThreadLocal<StoryRunContext> storyRunContext = ThreadLocal.withInitial(StoryRunContext::new);
public RunContext(Configuration configuration, AllStepCandidates allStepCandidates,
EmbedderMonitor embedderMonitor, MetaFilter filter, BatchFailures failures) {
this(configuration, allStepCandidates, embedderMonitor, filter, failures, false);
}
private RunContext(Configuration configuration, AllStepCandidates allStepCandidates,
EmbedderMonitor embedderMonitor, MetaFilter filter, BatchFailures failures, boolean givenStory) {
this.configuration = configuration;
this.givenStory = givenStory;
this.allStepCandidates = allStepCandidates;
this.embedderMonitor = embedderMonitor;
this.filter = filter;
this.failures = failures;
this.stepsContext = configuration.stepsContext();
resetState();
}
public StepsContext stepsContext() {
return stepsContext;
}
public boolean restartScenario() {
Throwable cause = failure(state());
while (cause != null) {
if (cause instanceof RestartingScenarioFailure) {
return true;
}
cause = cause.getCause();
}
return false;
}
public boolean restartStory() {
Throwable cause = failure(state());
while (cause != null) {
if (cause instanceof RestartingStoryFailure) {
return true;
}
cause = cause.getCause();
}
return false;
}
public void currentPath(String path) {
currentRunContext().pathIs(path);
currentRunContext().reporterIs(configuration.storyReporter(path));
}
public void interruptIfCancelled() throws InterruptedException {
for (Story story : cancelledStories.keySet()) {
if (path().equals(story.getPath())) {
throw new InterruptedException(path());
}
}
}
public boolean dryRun() {
return configuration.storyControls().dryRun();
}
public Configuration configuration() {
return configuration;
}
public boolean givenStory() {
return givenStory;
}
public String path() {
return currentRunContext().path();
}
public FilteredStory filter(Story story) {
return new FilteredStory(filter, story, configuration.storyControls(), givenStory());
}
public MetaFilter filter() {
return filter;
}
public PerformableSteps beforeStoriesSteps() {
return new PerformableSteps(configuration.stepCollector().collectBeforeOrAfterStoriesSteps(
allStepCandidates.getBeforeStoriesSteps()));
}
public PerformableSteps afterStoriesSteps() {
return new PerformableSteps(configuration.stepCollector().collectBeforeOrAfterStoriesSteps(
allStepCandidates.getAfterStoriesSteps()));
}
public PerformableSteps beforeStorySteps(Meta storyMeta) {
return new PerformableSteps(configuration.stepCollector()
.collectBeforeOrAfterStorySteps(allStepCandidates.getBeforeStorySteps(givenStory), storyMeta));
}
public PerformableSteps afterStorySteps(Meta storyMeta) {
return new PerformableSteps(configuration.stepCollector()
.collectBeforeOrAfterStorySteps(allStepCandidates.getAfterStorySteps(givenStory), storyMeta));
}
public PerformableSteps beforeScenarioSteps(Meta storyAndScenarioMeta, ScenarioType type) {
return new PerformableSteps(configuration.stepCollector()
.collectBeforeScenarioSteps(allStepCandidates.getBeforeScenarioSteps(type), storyAndScenarioMeta));
}
public PerformableSteps afterScenarioSteps(Meta storyAndScenarioMeta, ScenarioType type) {
return new PerformableSteps(configuration.stepCollector()
.collectAfterScenarioSteps(allStepCandidates.getAfterScenarioSteps(type), storyAndScenarioMeta));
}
private Map<Stage, PerformableSteps> lifecycleSteps(Lifecycle lifecycle, Meta meta, Scope scope,
Map<String, String> parameters) {
MatchingStepMonitor monitor = new MatchingStepMonitor(configuration.stepMonitor());
Map<Stage, List<Step>> steps = configuration.stepCollector().collectLifecycleSteps(
allStepCandidates.getRegularSteps(), lifecycle, meta, scope, parameters, monitor);
Map<Stage, PerformableSteps> performableSteps = new EnumMap<>(Stage.class);
for (Map.Entry<Stage, List<Step>> entry : steps.entrySet()) {
performableSteps.put(entry.getKey(), new PerformableSteps(entry.getValue(), monitor.matched()));
}
return performableSteps;
}
private PerformableSteps scenarioSteps(Lifecycle lifecycle, Meta meta, Scenario scenario,
Map<String, String> parameters) {
MatchingStepMonitor monitor = new MatchingStepMonitor(configuration.stepMonitor());
StepCollector stepCollector = configuration.stepCollector();
Map<Stage, List<Step>> beforeOrAfterStepSteps = stepCollector.collectLifecycleSteps(
allStepCandidates.getRegularSteps(), lifecycle, meta, Scope.STEP, parameters, monitor);
List<Step> steps = new LinkedList<>();
for (Step step : stepCollector.collectScenarioSteps(allStepCandidates.getRegularSteps(), scenario,
parameters, monitor)) {
steps.addAll(beforeOrAfterStepSteps.get(Stage.BEFORE));
steps.add(step);
steps.addAll(beforeOrAfterStepSteps.get(Stage.AFTER));
}
return new PerformableSteps(steps, monitor.matched());
}
public RunContext childContextFor(GivenStory givenStory) {
RunContext child = new RunContext(configuration, allStepCandidates, embedderMonitor, filter,
failures, true);
child.currentRunContext().pathIs(configuration.pathCalculator().calculate(path(), givenStory.getPath()));
return child;
}
public void cancelStory(Story story, StoryDuration storyDuration) {
cancelledStories.put(story, storyDuration);
}
public boolean isCancelled(Story story) {
return cancelledStories.containsKey(story);
}
public StoryDuration storyDuration(Story story) {
return cancelledStories.get(story);
}
public State state() {
return currentRunContext().state();
}
public void stateIs(State state) {
currentRunContext().stateIs(state);
}
public boolean failureOccurred() {
return failed(state());
}
public void resetState() {
currentRunContext().resetState();
}
/**
* Reset all the existing failures.
*/
public void resetFailures() {
this.failures.clear();
}
/**
* Resets only the failures corresponding to the given story.
* @param story the story for which we want to remove the failures.
*/
public void resetFailures(Story story) {
this.failures.entrySet()
.removeIf(entry -> entry.getKey().equals(toBatchFailuresKey(story, entry.getValue())));
}
public StoryReporter reporter() {
return currentRunContext().reporter();
}
public boolean failed(State state) {
return !state.getClass().equals(FineSoFar.class);
}
public Throwable failure(State state) {
if (failed(state)) {
return state.getFailure().getCause();
}
return null;
}
public void addFailure(Story story) {
addFailure(story, failure(state()));
}
public void addFailure(Story story, Throwable cause) {
if (cause != null) {
failures.put(toBatchFailuresKey(story, cause), cause);
}
}
public void pendingSteps(List<PendingStep> pendingSteps) {
if (!pendingSteps.isEmpty()) {
pendingStories.put(path(), pendingSteps);
}
}
public boolean hasPendingSteps() {
return pendingStories.containsKey(path());
}
public boolean isStoryPending() {
return pendingStories.containsKey(path());
}
public boolean hasFailed() {
return failed(state());
}
public Status status(State initial) {
if (isStoryPending()) {
return Status.PENDING;
} else if (failed(initial)) {
return Status.NOT_PERFORMED;
} else {
return (hasFailed() ? Status.FAILED : Status.SUCCESSFUL);
}
}
public MetaFilter getFilter() {
return filter;
}
public BatchFailures getFailures() {
return failures;
}
public EmbedderMonitor embedderMonitor() {
return embedderMonitor;
}
private StoryRunContext currentRunContext() {
return storyRunContext.get();
}
/**
* Converts the given story and failure to the key to use to store the failure.
* @param story the story where the failure occurred.
* @param cause the failure that occurred.
* @return the key to use to store the failure into the {@code BatchFailures}.
*/
private String toBatchFailuresKey(Story story, Throwable cause) {
return String.format("%s@%s", story.getPath(), Integer.toHexString(cause.hashCode()));
}
}
private static class StoryRunContext {
private State state;
private String path;
private StoryReporter reporter;
private State state() {
return state;
}
private void stateIs(State state) {
this.state = state;
}
private void resetState() {
this.state = new FineSoFar();
}
private String path() {
return path;
}
private void pathIs(String path) {
this.path = path;
}
public StoryReporter reporter() {
return this.reporter;
}
private void reporterIs(StoryReporter reporter) {
this.reporter = reporter;
}
}
public static class FailureContext {
List<Throwable> failures = new ArrayList<>();
public void addFailure(Throwable failure) {
failures.add(failure);
}
public List<Throwable> getFailures() {
return failures;
}
}
public interface ReportingFailures {
void reportFailures(FailureContext context);
}
public interface Performable extends ReportingFailures {
void perform(RunContext context) throws InterruptedException;
}
public static class PerformableRoot {
private PerformableSteps beforeSteps = new PerformableSteps();
private Map<String, PerformableStory> stories = new LinkedHashMap<>();
private PerformableSteps afterSteps = new PerformableSteps();
public void addBeforeSteps(PerformableSteps beforeSteps) {
this.beforeSteps = beforeSteps;
}
public void add(PerformableStory performableStory) {
stories.put(performableStory.getStory().getPath(), performableStory);
}
public void addAfterSteps(PerformableSteps afterSteps) {
this.afterSteps = afterSteps;
}
public PerformableStory get(Story story) {
PerformableStory performableStory = stories.get(story.getPath());
if (performableStory != null) {
return performableStory;
}
throw new RuntimeException("No performable story for path " + story.getPath());
}
public List<PerformableStory> getStories() {
return new ArrayList<>(stories.values());
}
}
public static enum Status {
SUCCESSFUL, FAILED, PENDING, NOT_PERFORMED, EXCLUDED;
}
public abstract static class PerformableEntity implements Performable {
private PerformableGivenStories givenStories;
private final Map<Stage, Map<ExecutionType, PerformableSteps>> stageSteps;
private final LifecycleStepsExecutionHook beforeHook;
private final LifecycleStepsExecutionHook afterHook;
public PerformableEntity(LifecycleStepsExecutionHook beforeHook, LifecycleStepsExecutionHook afterHook) {
this.givenStories = new PerformableGivenStories(Collections.emptyList(), null);
this.stageSteps = new EnumMap<>(Stage.class);
this.beforeHook = beforeHook;
this.afterHook = afterHook;
}
public void setGivenStories(PerformableGivenStories givenStories) {
this.givenStories = givenStories;
}
public void addBeforeSteps(Lifecycle.ExecutionType type, PerformableSteps beforeSteps) {
getBeforeSteps(type).add(beforeSteps);
}
public void addAfterSteps(Lifecycle.ExecutionType type, PerformableSteps afterSteps) {
getAfterSteps(type).add(afterSteps);
}
protected void performWithStopExecutionExceptionHandling(FailableRunnable<InterruptedException> action,
FailableRunnable<InterruptedException> afterHook) throws InterruptedException {
try {
action.run();
} finally {
afterHook.run();
}
}
protected void performBeforeSteps(RunContext context) throws InterruptedException {
performHookSteps(context, Stage.BEFORE, ExecutionType.SYSTEM);
performHookSteps(context, Stage.BEFORE, ExecutionType.USER);
}
protected void performAfterSteps(RunContext context) throws InterruptedException {
performHookSteps(context, Stage.AFTER, ExecutionType.USER);
performHookSteps(context, Stage.AFTER, ExecutionType.SYSTEM);
}
protected PerformableSteps getBeforeSteps(Lifecycle.ExecutionType type) {
return getPerformableSteps(Stage.BEFORE, type);
}
protected PerformableSteps getAfterSteps(Lifecycle.ExecutionType type) {
return getPerformableSteps(Stage.AFTER, type);
}
private PerformableSteps getPerformableSteps(Stage stage, Lifecycle.ExecutionType type) {
Map<ExecutionType, PerformableSteps> steps = stageSteps.computeIfAbsent(stage,
t -> new EnumMap<>(ExecutionType.class));
return steps.computeIfAbsent(type, s -> new PerformableSteps());
}
private void performHookSteps(RunContext context, Stage stage, Lifecycle.ExecutionType type)
throws InterruptedException {
StoryReporter reporter = context.reporter();
this.beforeHook.perform(reporter, stage, type);
getPerformableSteps(stage, type).perform(context, false);
this.afterHook.perform(reporter, stage, type);
}
public PerformableGivenStories getGivenStories() {
return givenStories;
}
}
private interface LifecycleStepsExecutionHook {
void perform(StoryReporter reporter, Stage stage, Lifecycle.ExecutionType type);
}
public static class PerformableStory extends PerformableEntity {
private final Story story;
private final transient Keywords keywords;
private final boolean givenStory;
private boolean excluded;
private Status status;
private Timing timing = new Timing();
private List<PerformableScenario> scenarios = new ArrayList<>();
public PerformableStory(Story story, Keywords keywords, boolean givenStory) {
super(StoryReporter::beforeStorySteps, StoryReporter::afterStorySteps);
this.story = story;
this.keywords = keywords;
this.givenStory = givenStory;
}
public void excluded(boolean excluded) {
this.excluded = excluded;
}
public boolean isExcluded() {
return excluded;
}
public Story getStory() {
return story;
}
public Keywords getKeywords() {
return keywords;
}
public boolean givenStory() {
return givenStory;
}
public Status getStatus() {
return status;
}
public Timing getTiming() {
return timing;
}
public void add(PerformableScenario performableScenario) {
scenarios.add(performableScenario);
}
public void addAll(List<PerformableScenario> performableScenarios) {
scenarios.addAll(performableScenarios);
}
@Override
public void perform(RunContext context) throws InterruptedException {
if (isExcluded()) {
context.reporter().storyExcluded(story, context.filter.asString());
this.status = Status.EXCLUDED;
}
Timer timer = new Timer().start();
try {
context.stepsContext().resetStory();
context.reporter().beforeStory(story, givenStory);
context.reporter().narrative(story.getNarrative());
context.reporter().lifecycle(story.getLifecycle());
State state = context.state();
performBeforeSteps(context);
getGivenStories().perform(context);
performWithStopExecutionExceptionHandling(
() -> {
if (!context.failureOccurred() || !context.configuration().storyControls()
.skipStoryIfGivenStoryFailed()) {
context.reporter().beforeScenarios();
for (PerformableScenario scenario : scenarios) {
scenario.perform(context);
}
}
},
() -> {
context.reporter().afterScenarios();
performAfterSteps(context);
}
);
context.configuration().storyControls().resetCurrentStoryControls();
if (context.restartStory()) {
context.reporter().afterStory(true);
} else {
context.reporter().afterStory(givenStory);
}
this.status = context.status(state);
} finally {
timing = new Timing(timer.stop());
}
}
@Override
public void reportFailures(FailureContext context) {
for (PerformableScenario scenario : scenarios) {
scenario.reportFailures(context);
}
}
public List<PerformableScenario> getScenarios() {
return scenarios;
}
public boolean hasIncludedScenarios() {
return getScenarios().stream().anyMatch(scenario -> !scenario.isExcluded());
}
}
public static class PerformableScenario implements Performable {
private final Scenario scenario;
private final String storyPath;
private boolean excluded;
@SuppressWarnings("unused")
private Status status;
private Timing timing = new Timing();
private NormalPerformableScenario normalScenario;
private List<ExamplePerformableScenario> exampleScenarios;
public PerformableScenario(Scenario scenario, String storyPath) {
this.scenario = scenario;
this.storyPath = storyPath;
}
public void useNormalScenario(NormalPerformableScenario normalScenario) {
this.normalScenario = normalScenario;
}
public void addExampleScenario(ExamplePerformableScenario exampleScenario) {
if (exampleScenarios == null) {
exampleScenarios = new ArrayList<>();
}
exampleScenarios.add(exampleScenario);
}
public void excluded(boolean excluded) {
this.excluded = excluded;
}
public boolean isExcluded() {
return excluded;
}
public Status getStatus() {
return status;
}
public Timing getTiming() {
return timing;
}
public Scenario getScenario() {
return scenario;
}
public String getStoryPath() {
return storyPath;
}
public Throwable getFailure() {
FailureContext context = new FailureContext();
reportFailures(context);
List<Throwable> failures = context.getFailures();
if (failures.size() > 0) {
return failures.get(0);
}
return null;
}
public boolean hasNormalScenario() {
return normalScenario != null;
}
public boolean hasExamples() {
return exampleScenarios != null && exampleScenarios.size() > 0;
}
public boolean isPerformable() {
return hasNormalScenario() || hasExamples() || isExcluded();
}
public List<ExamplePerformableScenario> getExamples() {
return exampleScenarios;
}
@Override
public void perform(RunContext context) throws InterruptedException {
if (isExcluded()) {
context.embedderMonitor().scenarioExcluded(scenario, context.filter());
return;
}
Timer timer = new Timer().start();
try {
context.stepsContext().resetScenario();
context.reporter().beforeScenario(scenario);
State state = context.state();
if (hasExamples()) {
context.reporter().beforeExamples(scenario.getSteps(),
scenario.getExamplesTable());
for (ExamplePerformableScenario exampleScenario : exampleScenarios) {
exampleScenario.perform(context);
}
context.reporter().afterExamples();
} else {
context.stepsContext().resetExample();
normalScenario.perform(context);
}
this.status = context.status(state);
} finally {
timing = new Timing(timer.stop());
context.reporter().afterScenario(timing);
}
}
@Override
public void reportFailures(FailureContext context) {
if (hasExamples()) {
for (ExamplePerformableScenario exampleScenario : exampleScenarios) {
exampleScenario.reportFailures(context);
}
} else {
normalScenario.reportFailures(context);
}
}
}
public abstract static class AbstractPerformableScenario extends PerformableEntity {
private transient Story story;
protected final Map<String, String> parameters;
protected final PerformableSteps steps = new PerformableSteps();
private Meta storyAndScenarioMeta = new Meta();
protected AbstractPerformableScenario(Story story) {
this(story, new HashMap<>());
}
protected AbstractPerformableScenario(Story story, Map<String, String> parameters) {
super(StoryReporter::beforeScenarioSteps, StoryReporter::afterScenarioSteps);
this.story = story;
this.parameters = parameters;
}
public void addSteps(PerformableSteps steps) {
this.steps.add(steps);
}
public Map<String, String> getParameters() {
return parameters;
}
protected void performScenario(RunContext context) throws InterruptedException {
performBeforeSteps(context);
getGivenStories().perform(context);
performWithStopExecutionExceptionHandling(
() -> {
boolean restart = true;
while (restart) {
restart = false;
try {
context.reporter().beforeScenarioSteps(null, null);
steps.perform(context, true);
} catch (RestartingScenarioFailure e) {
restart = true;
}
}
},
() -> {
context.reporter().afterScenarioSteps(null, null);
performAfterSteps(context);
}
);
}
@Override
public void reportFailures(FailureContext context) {
getBeforeSteps(ExecutionType.SYSTEM).reportFailures(context);
getBeforeSteps(ExecutionType.USER).reportFailures(context);
steps.reportFailures(context);
getAfterSteps(ExecutionType.USER).reportFailures(context);
getAfterSteps(ExecutionType.SYSTEM).reportFailures(context);
}
protected void resetStateIfConfigured(RunContext context) {
if (context.configuration().storyControls().resetStateBeforeScenario()) {
if (context.failureOccurred()) {
context.addFailure(story);
}
context.resetState();
}
}
public Meta getStoryAndScenarioMeta() {
return storyAndScenarioMeta;
}
public void setStoryAndScenarioMeta(Meta storyAndScenarioMeta) {
this.storyAndScenarioMeta = storyAndScenarioMeta;
}
}
public static class NormalPerformableScenario extends AbstractPerformableScenario {
public NormalPerformableScenario(Story story) {
super(story);
}
@Override
public void perform(RunContext context) throws InterruptedException {
resetStateIfConfigured(context);
performScenario(context);
}
}
public static class ExamplePerformableScenario extends AbstractPerformableScenario {
private final int exampleIndex;
public ExamplePerformableScenario(Story story, Map<String, String> exampleParameters, int exampleIndex) {
super(story, exampleParameters);
this.exampleIndex = exampleIndex;
}
@Override
public void perform(RunContext context) throws InterruptedException {
Meta parameterMeta = parameterMeta(context.configuration().keywords(), parameters).inheritFrom(
getStoryAndScenarioMeta());
if (parameterMeta.isEmpty() || !context.filter().excluded(parameterMeta)) {
resetStateIfConfigured(context);
context.stepsContext().resetExample();
context.reporter().example(parameters, exampleIndex);
performScenario(context);
}
}
private Meta parameterMeta(Keywords keywords, Map<String, String> parameters) {
String meta = keywords.meta();
if (parameters.containsKey(meta)) {
return Meta.createMeta(parameters.get(meta), keywords);
}
return Meta.EMPTY;
}
}
public static class PerformableGivenStories implements Performable {
private final List<PerformableStory> performableGivenStories;
private final GivenStories givenStories;
public PerformableGivenStories(List<PerformableStory> performableGivenStories, GivenStories givenStories) {
this.performableGivenStories = performableGivenStories;
this.givenStories = givenStories;
}
@Override
public void perform(RunContext context) throws InterruptedException {
if (performableGivenStories.size() > 0) {
StoryReporter storyReporter = context.reporter();
storyReporter.beforeGivenStories();
storyReporter.givenStories(givenStories);
for (PerformableStory story : performableGivenStories) {
story.perform(context);
}
storyReporter.afterGivenStories();
}
}
@Override
public void reportFailures(FailureContext context) {
}
}
public static class PerformableSteps implements ReportingFailures {
private final transient List<Step> steps;
private final transient List<PendingStep> pendingSteps;
private List<StepMatch> matches;
private List<StepResult> results;
public PerformableSteps() {
this(null);
}
public PerformableSteps(List<Step> steps) {
this(steps, null);
}
public PerformableSteps(List<Step> steps, List<StepMatch> stepMatches) {
this.steps = steps != null ? steps : new ArrayList<Step>();
this.pendingSteps = pendingSteps();
this.matches = stepMatches;
}
public void add(PerformableSteps performableSteps) {
this.steps.addAll(performableSteps.steps);
this.pendingSteps.addAll(performableSteps.pendingSteps);
if (performableSteps.matches != null) {
if (this.matches == null) {
this.matches = new ArrayList<>();
}
this.matches.addAll(performableSteps.matches);
}
}
public void perform(RunContext context, boolean interruptIfCancelled) throws InterruptedException {
if (steps.size() == 0) {
return;
}
Keywords keywords = context.configuration().keywords();
State state = context.state();
State originalState = state;
StoryReporter reporter = context.reporter();
results = new ArrayList<>();
for (Step step : steps) {
try {
if (interruptIfCancelled) {
context.interruptIfCancelled();
}
state = state.run(step, results, keywords, reporter);
} catch (RestartingScenarioFailure e) {
reporter.restarted(step.asString(keywords), e);
throw e;
}
}
context.stateIs(state instanceof Ignoring ? originalState : state);
context.pendingSteps(pendingSteps);
generatePendingStepMethods(context, pendingSteps);
}
@Override
public void reportFailures(FailureContext context) {
// Results can be null if the steps are not executed
if (results == null) {
return;
}
for (StepResult result : results) {
if (result instanceof AbstractStepResult.Failed) {
context.addFailure(result.getFailure());
}
}
}
private List<PendingStep> pendingSteps() {
List<PendingStep> pending = new ArrayList<>();
for (Step step : steps) {
if (step instanceof PendingStep) {
pending.add((PendingStep) step);
}
}
return pending;
}
private void generatePendingStepMethods(RunContext context, List<PendingStep> pendingSteps) {
if (!pendingSteps.isEmpty()) {
PendingStepMethodGenerator generator = new PendingStepMethodGenerator(context.configuration()
.keywords());
List<String> methods = new ArrayList<>();
for (PendingStep pendingStep : pendingSteps) {
if (!pendingStep.annotated()) {
String generatedMethod = generator.generateMethod(pendingStep);
pendingStep.setPendingMethod(generatedMethod);
methods.add(generatedMethod);
}
}
context.reporter().pendingMethods(methods);
if (context.configuration().pendingStepStrategy() instanceof FailingUponPendingStep) {
throw new PendingStepsFound(pendingSteps);
}
}
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
public RunContext newRunContext(Configuration configuration, AllStepCandidates allStepCandidates,
EmbedderMonitor embedderMonitor, MetaFilter filter, BatchFailures failures) {
return new RunContext(configuration, allStepCandidates, embedderMonitor, filter, failures);
}
}