package org.jbehave.core.embedder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jbehave.core.annotations.ScenarioType;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.failures.FailureStrategy;
import org.jbehave.core.failures.PendingStepFound;
import org.jbehave.core.failures.PendingStepStrategy;
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.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.StoryReporter;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.PendingStepMethodGenerator;
import org.jbehave.core.steps.ProvidedStepsFactory;
import org.jbehave.core.steps.Step;
import org.jbehave.core.steps.StepCollector.Stage;
import org.jbehave.core.steps.StepCreator.ParametrisedStep;
import org.jbehave.core.steps.StepCreator.PendingStep;
import org.jbehave.core.steps.StepResult;
import static org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter;
/**
* Runs a {@link Story}, given a {@link Configuration} and a list of
* {@link CandidateSteps}, describing the results to the {@link StoryReporter}.
*
* @author Elizabeth Keogh
* @author Mauro Talevi
* @author Paul Hammant
* @deprecated Replaced by {@link PerformableTree}. Kept only to facilitate the comparison with 3.x.
*/
public class StoryRunner {
private ThreadLocal<FailureStrategy> currentStrategy = new ThreadLocal<FailureStrategy>();
private ThreadLocal<FailureStrategy> failureStrategy = new ThreadLocal<FailureStrategy>();
private ThreadLocal<PendingStepStrategy> pendingStepStrategy = new ThreadLocal<PendingStepStrategy>();
private ThreadLocal<UUIDExceptionWrapper> storyFailure = new ThreadLocal<UUIDExceptionWrapper>();
private ThreadLocal<StoryReporter> reporter = new ThreadLocal<StoryReporter>();
private ThreadLocal<String> reporterStoryPath = new ThreadLocal<String>();
private ThreadLocal<State> storiesState = new ThreadLocal<State>();
// should this be volatile?
private Map<Story, StoryDuration> cancelledStories = new HashMap<Story, StoryDuration>();
/**
* Run steps before or after a collection of stories. Steps are execute only
* <b>once</b> per collection of stories.
*
* @param configuration the Configuration used to find the steps to run
* @param candidateSteps the List of CandidateSteps containing the candidate
* steps methods
* @param stage the Stage
* @return The State after running the steps
*/
public State runBeforeOrAfterStories(Configuration configuration, List<CandidateSteps> candidateSteps, Stage stage) {
String storyPath = capitalizeFirstLetter(stage.name().toLowerCase()) + "Stories";
reporter.set(configuration.storyReporter(storyPath));
reporter.get().beforeStory(new Story(storyPath), false);
RunContext context = new RunContext(configuration, candidateSteps, storyPath, MetaFilter.EMPTY);
if (stage == Stage.BEFORE) {
resetStoryFailure(context);
}
if (stage == Stage.AFTER && storiesState.get() != null) {
context.stateIs(storiesState.get());
}
try {
runStepsWhileKeepingState(context,
configuration.stepCollector().collectBeforeOrAfterStoriesSteps(context.candidateSteps(), stage));
} catch (InterruptedException e) {
throw new UUIDExceptionWrapper(e);
}
reporter.get().afterStory(false);
storiesState.set(context.state());
// if we are running with multiple threads, call delayed
// methods, otherwise we will forget to close files on BeforeStories
if (stage == Stage.BEFORE) {
if (reporter.get() instanceof ConcurrentStoryReporter) {
((ConcurrentStoryReporter) reporter.get()).invokeDelayed();
}
}
// handle any after stories failure according to strategy
if (stage == Stage.AFTER) {
try {
handleStoryFailureByStrategy();
} catch (Throwable e) {
return new SomethingHappened(storyFailure.get());
} finally {
if (reporter.get() instanceof ConcurrentStoryReporter) {
((ConcurrentStoryReporter) reporter.get()).invokeDelayed();
}
}
}
return context.state();
}
/**
* Runs a Story with the given configuration and steps.
*
* @param configuration the Configuration used to run story
* @param candidateSteps the List of CandidateSteps containing the candidate
* steps methods
* @param story the Story to run
* @throws Throwable if failures occurred and FailureStrategy dictates it to
* be re-thrown.
*/
public void run(Configuration configuration, List<CandidateSteps> candidateSteps, Story story) throws Throwable {
run(configuration, candidateSteps, story, MetaFilter.EMPTY);
}
/**
* Runs a Story with the given configuration and steps, applying the given
* meta filter.
*
* @param configuration the Configuration used to run story
* @param candidateSteps the List of CandidateSteps containing the candidate
* steps methods
* @param story the Story to run
* @param filter the Filter to apply to the story Meta
* @throws Throwable if failures occurred and FailureStrategy dictates it to
* be re-thrown.
*/
public void run(Configuration configuration, List<CandidateSteps> candidateSteps, Story story, MetaFilter filter)
throws Throwable {
run(configuration, candidateSteps, story, filter, null);
}
/**
* Runs a Story with the given configuration and steps, applying the given
* meta filter, and staring from given state.
*
* @param configuration the Configuration used to run story
* @param candidateSteps the List of CandidateSteps containing the candidate
* steps methods
* @param story the Story to run
* @param filter the Filter to apply to the story Meta
* @param beforeStories the State before running any of the stories, if not
* <code>null</code>
* @throws Throwable if failures occurred and FailureStrategy dictates it to
* be re-thrown.
*/
public void run(Configuration configuration, List<CandidateSteps> candidateSteps, Story story, MetaFilter filter,
State beforeStories) throws Throwable {
run(configuration, new ProvidedStepsFactory(candidateSteps), story, filter, beforeStories);
}
/**
* Runs a Story with the given steps factory, applying the given meta
* filter, and staring from given state.
*
* @param configuration the Configuration used to run story
* @param stepsFactory the InjectableStepsFactory used to created the
* candidate steps methods
* @param story the Story to run
* @param filter the Filter to apply to the story Meta
* @param beforeStories the State before running any of the stories, if not
* <code>null</code>
*
* @throws Throwable if failures occurred and FailureStrategy dictates it to
* be re-thrown.
*/
public void run(Configuration configuration, InjectableStepsFactory stepsFactory, Story story, MetaFilter filter,
State beforeStories) throws Throwable {
RunContext context = new RunContext(configuration, stepsFactory, story.getPath(), filter);
if (beforeStories != null) {
context.stateIs(beforeStories);
}
Map<String, String> storyParameters = new HashMap<String, String>();
run(context, story, storyParameters);
}
/**
* 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);
}
/**
* Cancels story execution following a timeout
*
* @param story the Story that was timed out
* @param storyDuration the StoryDuration
*/
public void cancelStory(Story story, StoryDuration storyDuration) {
cancelledStories.put(story, storyDuration);
}
/**
* Determines if the cause of a story failure is {@link RestartingStoryFailure}
* @param cause the {@link Throwable} containing the {@link RestartingStoryFailure} in its stack trace
* @return true if found, false otherwise
*/
private boolean restartStory(Throwable cause) {
while (cause != null) {
if (cause instanceof RestartingStoryFailure) {
return true;
}
cause = cause.getCause();
}
return false;
}
private void run(RunContext context, Story story, Map<String, String> storyParameters) throws Throwable {
boolean restartingStory = false;
try {
runCancellable(context, story, storyParameters);
} catch (Throwable e) {
if (cancelledStories.containsKey(story)) {
reporter.get().storyCancelled(story, cancelledStories.get(story));
reporter.get().afterScenario();
reporter.get().afterStory(context.givenStory);
}
// Restart entire story if needed
if (restartStory(e)) {
//this is not getting logged when running in multi-threaded mode
reporter.get().restartedStory(story, e);
restartingStory = true;
run(context, story, storyParameters);
} else {
throw e;
}
}finally {
if (!context.givenStory() && reporter.get() instanceof ConcurrentStoryReporter && !restartingStory) {
((ConcurrentStoryReporter) reporter.get()).invokeDelayed();
}
}
}
private void runCancellable(RunContext context, Story story, Map<String, String> storyParameters) throws Throwable {
if (!context.givenStory()) {
reporter.set(reporterFor(context, story));
}
pendingStepStrategy.set(context.configuration().pendingStepStrategy());
failureStrategy.set(context.configuration().failureStrategy());
resetStoryFailure(context);
if (context.dryRun()) {
reporter.get().dryRun();
}
if (context.configuration().storyControls().resetStateBeforeStory()) {
context.resetState();
}
// run before story steps, if any
reporter.get().beforeStory(story, context.givenStory());
boolean storyAllowed = true;
FilteredStory filterContext = context.filter(story);
Meta storyMeta = story.getMeta();
if (!filterContext.allowed()) {
reporter.get().storyNotAllowed(story, context.metaFilterAsString());
storyAllowed = false;
}
if (storyAllowed) {
reporter.get().narrative(story.getNarrative());
runBeforeOrAfterStorySteps(context, story, Stage.BEFORE);
addMetaParameters(storyParameters, storyMeta);
runGivenStories(story.getGivenStories(), storyParameters, context);
// determine if before and after scenario steps should be run
boolean runBeforeAndAfterScenarioSteps = shouldRunBeforeOrAfterScenarioSteps(context);
reporter.get().lifecyle(story.getLifecycle());
for (Scenario scenario : story.getScenarios()) {
// scenario also inherits meta from story
boolean scenarioAllowed = true;
if (failureOccurred(context) && context.configuration().storyControls().skipScenariosAfterFailure()) {
continue;
}
reporter.get().beforeScenario(scenario.getTitle());
reporter.get().scenarioMeta(scenario.getMeta());
if (!filterContext.allowed(scenario)) {
reporter.get().scenarioNotAllowed(scenario, context.metaFilterAsString());
scenarioAllowed = false;
}
if (scenarioAllowed) {
if (context.configuration().storyControls().resetStateBeforeScenario()) {
context.resetState();
}
Meta storyAndScenarioMeta = scenario.getMeta().inheritFrom(storyMeta);
// run before scenario steps, if allowed
if (runBeforeAndAfterScenarioSteps) {
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.BEFORE,
ScenarioType.NORMAL);
}
if (isParameterisedByExamples(scenario)) { // run parametrised scenarios by examples
runScenariosParametrisedByExamples(context, scenario, story.getLifecycle(), storyAndScenarioMeta);
} else { // run as plain old scenario
runStepsWithLifecycle(context, story.getLifecycle(), storyParameters, scenario, storyAndScenarioMeta);
}
// run after scenario steps, if allowed
if (runBeforeAndAfterScenarioSteps) {
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.AFTER,
ScenarioType.NORMAL);
}
}
reporter.get().afterScenario();
}
// run after story steps, if any
runBeforeOrAfterStorySteps(context, story, Stage.AFTER);
}
reporter.get().afterStory(context.givenStory());
// handle any failure according to strategy
if (!context.givenStory()) {
handleStoryFailureByStrategy();
}
}
private void addMetaParameters(Map<String, String> storyParameters, Meta meta) {
for (String name : meta.getPropertyNames()) {
storyParameters.put(name, meta.getProperty(name));
}
}
private boolean shouldRunBeforeOrAfterScenarioSteps(RunContext context) {
Configuration configuration = context.configuration();
if (!configuration.storyControls().skipBeforeAndAfterScenarioStepsIfGivenStory()) {
return true;
}
return !context.givenStory();
}
private boolean failureOccurred(RunContext context) {
return context.failureOccurred();
}
private StoryReporter reporterFor(RunContext context, Story story) {
Configuration configuration = context.configuration();
if (context.givenStory()) {
return configuration.storyReporter(reporterStoryPath.get());
} else {
// store parent story path for reporting
reporterStoryPath.set(story.getPath());
return configuration.storyReporter(reporterStoryPath.get());
}
}
private void handleStoryFailureByStrategy() throws Throwable {
Throwable throwable = storyFailure.get();
if (throwable != null) {
currentStrategy.get().handleFailure(throwable);
}
}
private void resetStoryFailure(RunContext context) {
if (context.givenStory()) {
// do not reset failure for given stories
return;
}
currentStrategy.set(context.configuration().failureStrategy());
storyFailure.set(null);
}
private void runGivenStories(GivenStories givenStories, Map<String, String> parameters, RunContext context) throws Throwable {
if (givenStories.getPaths().size() > 0) {
reporter.get().givenStories(givenStories);
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());
run(childContext, story, parameters);
}
}
}
private Story storyWithMatchingScenarios(Story story, Map<String,String> parameters) {
if ( parameters.isEmpty() ) return story;
List<Scenario> scenarios = new ArrayList<Scenario>();
for ( Scenario scenario : story.getScenarios() ){
if ( matchesParameters(scenario, parameters) ){
scenarios.add(scenario);
}
}
return new Story(story.getPath(), story.getDescription(), story.getMeta(), story.getNarrative(), 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;
}
private boolean isParameterisedByExamples(Scenario scenario) {
return scenario.getExamplesTable().getRowCount() > 0 && !scenario.getGivenStories().requireParameters();
}
private void runScenariosParametrisedByExamples(RunContext context, Scenario scenario, Lifecycle lifecycle, Meta storyAndScenarioMeta)
throws Throwable {
ExamplesTable table = scenario.getExamplesTable();
reporter.get().beforeExamples(scenario.getSteps(), table);
Keywords keywords = context.configuration().keywords();
for (Map<String, String> scenarioParameters : table.getRows()) {
Meta parameterMeta = parameterMeta(keywords, scenarioParameters);
if ( !parameterMeta.isEmpty() && !context.filter.allow(parameterMeta) ){
continue;
}
reporter.get().example(scenarioParameters);
if (context.configuration().storyControls().resetStateBeforeScenario()) {
context.resetState();
}
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.BEFORE, ScenarioType.EXAMPLE);
runStepsWithLifecycle(context, lifecycle, scenarioParameters, scenario, storyAndScenarioMeta);
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.AFTER, ScenarioType.EXAMPLE);
}
reporter.get().afterExamples();
}
private void runStepsWithLifecycle(RunContext context, Lifecycle lifecycle, Map<String, String> parameters, Scenario scenario, Meta storyAndScenarioMeta) throws Throwable {
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.BEFORE, ScenarioType.ANY);
runLifecycleSteps(context, lifecycle, Stage.BEFORE, storyAndScenarioMeta);
addMetaParameters(parameters, storyAndScenarioMeta);
runGivenStories(scenario.getGivenStories(), parameters, context);
runScenarioSteps(context, scenario, parameters);
runLifecycleSteps(context, lifecycle, Stage.AFTER, storyAndScenarioMeta);
runBeforeOrAfterScenarioSteps(context, scenario, storyAndScenarioMeta, Stage.AFTER, ScenarioType.ANY);
}
private Meta parameterMeta(Keywords keywords,
Map<String, String> scenarioParameters) {
String meta = keywords.meta();
if (scenarioParameters.containsKey(meta)) {
return Meta.createMeta(scenarioParameters.get(meta), keywords);
}
return Meta.EMPTY;
}
private void runBeforeOrAfterStorySteps(RunContext context, Story story, Stage stage) throws InterruptedException {
runStepsWhileKeepingState(context, context.collectBeforeOrAfterStorySteps(story, stage));
}
private void runBeforeOrAfterScenarioSteps(RunContext context, Scenario scenario, Meta storyAndScenarioMeta,
Stage stage, ScenarioType type) throws InterruptedException {
runStepsWhileKeepingState(context, context.collectBeforeOrAfterScenarioSteps(storyAndScenarioMeta, stage, type));
}
private void runLifecycleSteps(RunContext context, Lifecycle lifecycle, Stage stage, Meta storyAndScenarioMeta) throws InterruptedException {
runStepsWhileKeepingState(context, context.collectLifecycleSteps(lifecycle, storyAndScenarioMeta, stage));
}
private void runScenarioSteps(RunContext context, Scenario scenario, Map<String, String> scenarioParameters)
throws InterruptedException {
boolean restart = true;
while (restart) {
restart = false;
List<Step> steps = context.collectScenarioSteps(scenario, scenarioParameters);
try {
runStepsWhileKeepingState(context, steps);
} catch (RestartingScenarioFailure e) {
restart = true;
continue;
}
generatePendingStepMethods(context, steps);
}
}
private void generatePendingStepMethods(RunContext context, List<Step> steps) {
List<PendingStep> pendingSteps = new ArrayList<PendingStep>();
for (Step step : steps) {
if (step instanceof PendingStep) {
pendingSteps.add((PendingStep) step);
}
}
if (!pendingSteps.isEmpty()) {
PendingStepMethodGenerator generator = new PendingStepMethodGenerator(context.configuration().keywords());
List<String> methods = new ArrayList<String>();
for (PendingStep pendingStep : pendingSteps) {
if (!pendingStep.annotated()) {
methods.add(generator.generateMethod(pendingStep));
}
}
reporter.get().pendingMethods(methods);
}
}
private void runStepsWhileKeepingState(RunContext context, List<Step> steps) throws InterruptedException {
if (steps == null || steps.size() == 0) {
return;
}
State state = context.state();
for (Step step : steps) {
try {
context.interruptIfCancelled();
state = state.run(step);
} catch (RestartingScenarioFailure e) {
reporter.get().restarted(step.toString(), e);
throw e;
}
}
context.stateIs(state);
}
public interface State {
State run(Step step);
}
private final class FineSoFar implements State {
public State run(Step step) {
if ( step instanceof ParametrisedStep ){
((ParametrisedStep)step).describeTo(reporter.get());
}
UUIDExceptionWrapper storyFailureIfItHappened = storyFailure.get();
StepResult result = step.perform(storyFailureIfItHappened);
result.describeTo(reporter.get());
UUIDExceptionWrapper stepFailure = result.getFailure();
if (stepFailure == null) {
return this;
}
storyFailure.set(mostImportantOf(storyFailureIfItHappened, stepFailure));
currentStrategy.set(strategyFor(storyFailure.get()));
return new SomethingHappened(stepFailure);
}
private UUIDExceptionWrapper mostImportantOf(UUIDExceptionWrapper failure1, UUIDExceptionWrapper failure2) {
return failure1 == null ? failure2
: failure1.getCause() instanceof PendingStepFound ? (failure2 == null ? failure1 : failure2)
: failure1;
}
private FailureStrategy strategyFor(Throwable failure) {
if (failure instanceof PendingStepFound) {
return pendingStepStrategy.get();
} else {
return failureStrategy.get();
}
}
}
private final class SomethingHappened implements State {
UUIDExceptionWrapper failure;
public SomethingHappened(UUIDExceptionWrapper failure) {
this.failure = failure;
}
public State run(Step step) {
StepResult result = step.doNotPerform(failure);
result.describeTo(reporter.get());
return this;
}
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
/**
* The context for running a story.
*/
private class RunContext {
private final Configuration configuration;
private final List<CandidateSteps> candidateSteps;
private final String path;
private final MetaFilter filter;
private final boolean givenStory;
private State state;
private RunContext parentContext;
public RunContext(Configuration configuration, InjectableStepsFactory stepsFactory, String path,
MetaFilter filter) {
this(configuration, stepsFactory.createCandidateSteps(), path, filter);
}
public RunContext(Configuration configuration, List<CandidateSteps> steps, String path, MetaFilter filter) {
this(configuration, steps, path, filter, false, null);
}
private RunContext(Configuration configuration, List<CandidateSteps> steps, String path, MetaFilter filter,
boolean givenStory, RunContext parentContext) {
this.configuration = configuration;
this.candidateSteps = steps;
this.path = path;
this.filter = filter;
this.givenStory = givenStory;
this.parentContext = parentContext;
resetState();
}
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 List<CandidateSteps> candidateSteps() {
return candidateSteps;
}
public boolean givenStory() {
return givenStory;
}
public String path() {
return path;
}
public FilteredStory filter(Story story) {
return new FilteredStory(filter, story, configuration.storyControls(), givenStory);
}
public String metaFilterAsString() {
return filter.asString();
}
public List<Step> collectBeforeOrAfterStorySteps(Story story, Stage stage) {
return configuration.stepCollector().collectBeforeOrAfterStorySteps(candidateSteps, story, stage,
givenStory);
}
public List<Step> collectBeforeOrAfterScenarioSteps(Meta storyAndScenarioMeta, Stage stage, ScenarioType type) {
return configuration.stepCollector().collectBeforeOrAfterScenarioSteps(candidateSteps,
storyAndScenarioMeta, stage, type);
}
public List<Step> collectLifecycleSteps(Lifecycle lifecycle, Meta storyAndScenarioMeta, Stage stage) {
return configuration.stepCollector().collectLifecycleSteps(candidateSteps, lifecycle, storyAndScenarioMeta, stage);
}
public List<Step> collectScenarioSteps(Scenario scenario, Map<String, String> parameters) {
return configuration.stepCollector().collectScenarioSteps(candidateSteps, scenario, parameters);
}
public RunContext childContextFor(GivenStory givenStory) {
String actualPath = configuration.pathCalculator().calculate(path, givenStory.getPath());
return new RunContext(configuration, candidateSteps, actualPath, filter, true, this);
}
public State state() {
return state;
}
public void stateIs(State state) {
this.state = state;
if ( parentContext != null ){
parentContext.stateIs(state);
}
}
public boolean failureOccurred() {
return failed(state);
}
public void resetState() {
this.state = new FineSoFar();
}
}
public boolean failed(State state) {
return !state.getClass().equals(FineSoFar.class);
}
public Throwable failure(State state) {
if (failed(state)) {
return ((SomethingHappened) state).failure.getCause();
}
return null;
}
}