StoryManager.java

package org.jbehave.core.embedder;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.embedder.PerformableTree.PerformableRoot;
import org.jbehave.core.embedder.PerformableTree.RunContext;
import org.jbehave.core.embedder.StoryTimeouts.TimeoutParser;
import org.jbehave.core.failures.BatchFailures;
import org.jbehave.core.model.Story;
import org.jbehave.core.model.StoryDuration;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.StepCollector.Stage;

/**
 * Manages the execution and outcomes of running stories. While each story is
 * run by the {@link PerformableTree}, the manager is responsible for the concurrent
 * submission and monitoring of their execution via the {@link ExecutorService}.
 */
public class StoryManager {

    private final Configuration configuration;
    private final EmbedderControls embedderControls;
    private final EmbedderMonitor embedderMonitor;
    private final ExecutorService executorService;
    private final InjectableStepsFactory stepsFactory;
    private final PerformableTree performableTree;
    private final Map<String, RunningStory> runningStories = new HashMap<>();
    private final Map<MetaFilter, List<Story>> excludedStories = new HashMap<>();
    private RunContext context;
    private StoryTimeouts timeouts;
    
    public StoryManager(Configuration configuration,
            InjectableStepsFactory stepsFactory,
            EmbedderControls embedderControls, EmbedderMonitor embedderMonitor,
            ExecutorService executorService, PerformableTree performableTree, TimeoutParser... parsers) {
        this.configuration = configuration;
        this.embedderControls = embedderControls;
        this.embedderMonitor = embedderMonitor;
        this.executorService = executorService;
        this.stepsFactory = stepsFactory;
        this.performableTree = performableTree;
        this.timeouts = new StoryTimeouts(embedderControls, embedderMonitor);
        this.timeouts.withParsers(parsers);
    }

    public Story storyOfPath(String storyPath) {
        return performableTree.storyOfPath(configuration, storyPath);
    }

    public List<Story> storiesOfPaths(List<String> storyPaths) {
        List<Story> stories = new ArrayList<>(storyPaths.size());
        for (String storyPath : storyPaths) {
            stories.add(storyOfPath(storyPath));
        }
        if (configuration.isParallelStoryExamplesEnabled()) {
            StorySplitter storySplitter = new StorySplitter(configuration.storyControls().storyIndexFormat());
            return storySplitter.splitStories(stories);
        }
        return stories;
    }

    public Story storyOfText(String storyAsText, String storyId) {
        return performableTree.storyOfText(configuration, storyAsText, storyId);
    }

    public void clear() {
        runningStories.clear();
    }

    public PerformableRoot performableRoot() {
        return performableTree.getRoot();
    }

    public List<StoryOutcome> outcomes() {
        List<StoryOutcome> outcomes = new ArrayList<>();
        for (RunningStory story : runningStories.values()) {
            outcomes.add(new StoryOutcome(story));
        }
        return outcomes;
    }

    public void runStoriesAsPaths(List<String> storyPaths, MetaFilter filter,
            BatchFailures failures) {
        runStories(storiesOfPaths(storyPaths), filter, failures);
    }

    public void runStories(List<Story> stories, MetaFilter filter,
            BatchFailures failures) {
        // create new run context
        AllStepCandidates allStepCandidates = new AllStepCandidates(configuration.stepConditionMatcher(),
                stepsFactory.createCandidateSteps());
        context = performableTree.newRunContext(configuration, allStepCandidates, embedderMonitor, filter, failures);

        // add stories
        performableTree.addStories(context, stories);

        // perform stories
        performStories(context, performableTree, stories);

        // collect failures
        failures.putAll(context.getFailures());

    }

    private void runStories(RunContext context, List<Story> stories) {
        stories.stream()
                .sorted(configuration.storyExecutionComparator())
                .forEach(story -> filterRunning(context, story));
    }

    private void performStories(RunContext context,
            PerformableTree performableTree, List<Story> stories) {
        // before stories
        performableTree.performBeforeOrAfterStories(context, Stage.BEFORE);

        // run stories
        runStories(context, stories);
        waitUntilAllDoneOrFailed(context);
        MetaFilter filter = context.filter();
        List<Story> excluded = excludedBy(filter);
        if (!excluded.isEmpty()) {
            embedderMonitor.storiesExcluded(excluded, filter,
                    embedderControls.verboseFiltering());
        }

        // after stories
        performableTree.performBeforeOrAfterStories(context, Stage.AFTER);
    }

    private void filterRunning(RunContext context, Story story) {
        if (context.filter(story).excluded()) {
            excludedBy(context.getFilter()).add(story);
        } else {
            runningStories.put(story.getPath(), runningStory(story));
        }
    }

    public List<Story> excludedBy(MetaFilter filter) {
        List<Story> stories = excludedStories.get(filter);
        if (stories == null) {
            stories = new ArrayList<>();
            excludedStories.put(filter, stories);
        }
        return stories;
    }

    public RunningStory runningStory(Story story) {
        return submit(new EnqueuedStory(performableTree, context,
                embedderControls, embedderMonitor, story, timeouts));
    }

    public void waitUntilAllDoneOrFailed(RunContext context) {
        if (runningStories.values().isEmpty()) {
            return;
        }
        boolean allDone = false;
        boolean started = false;
        while (!allDone || !started) {
            allDone = true;
            for (RunningStory runningStory : runningStories.values()) {
                if (runningStory.isStarted()) {
                    started = true;
                    Story story = runningStory.getStory();
                    Future<ThrowableStory> future = runningStory.getFuture();
                    if (!future.isDone()) {
                        allDone = false;
                        StoryDuration duration = runningStory.getDuration();
                        runningStory.updateDuration();
                        if (context.isCancelled(story)) {
                            if (duration.cancelTimedOut()) {
                                future.cancel(true);
                            }
                            continue;
                        }
                        if (duration.timedOut()) {
                            embedderMonitor.storyTimeout(story, duration);
                            context.cancelStory(story, duration);
                            if (embedderControls.failOnStoryTimeout()) {
                                throw new StoryExecutionFailed(story.getPath(),
                                        new StoryTimedOut(duration));
                            }
                            continue;
                        }
                    } else {
                        try {
                            ThrowableStory throwableStory = future.get();
                            Throwable throwable = throwableStory.getThrowable();
                            if (throwable != null) {
                                context.addFailure(story, throwable);
                                if (!embedderControls.ignoreFailureInStories()) {
                                    continue;
                                }
                            }
                        } catch (Throwable e) {
                            context.addFailure(story, e);
                            if (!embedderControls.ignoreFailureInStories()) {
                                continue;
                            }
                        }
                    }
                } else {
                    started = false;
                    allDone = false;
                }
            }
            tickTock();
        }
        writeStoryDurations(runningStories.values());
    }

    protected void writeStoryDurations(Collection<RunningStory> runningStories) {
        // collect story durations and cancel any outstanding execution which is
        // not done before returning
        Properties storyDurations = new Properties();
        long total = 0;
        for (RunningStory runningStory : runningStories) {
            long durationInMillis = runningStory.getDurationInMillis();
            total += durationInMillis;
            storyDurations.setProperty(runningStory.getStory().getPath(),
                    Long.toString(durationInMillis));
            Future<ThrowableStory> future = runningStory.getFuture();
            if (!future.isDone()) {
                future.cancel(true);
            }
        }
        int threads = embedderControls.threads();
        long threadAverage = total / threads;
        storyDurations.setProperty("total", Long.toString(total));
        storyDurations.setProperty("threads", Long.toString(threads));
        storyDurations.setProperty("threadAverage",
                Long.toString(threadAverage));
        write(storyDurations, "storyDurations.props");
    }

    private void write(Properties p, String name) {
        File outputDirectory = configuration.storyReporterBuilder()
                .outputDirectory();
        try {
            outputDirectory.mkdirs();
            Writer output = new FileWriter(new File(outputDirectory, name));
            p.store(output, this.getClass().getName());
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void tickTock() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // swallow exception quietly
        }
    }

    private synchronized RunningStory submit(EnqueuedStory enqueuedStory) {
        return new RunningStory(enqueuedStory, executorService.submit(enqueuedStory));
    }

    static class EnqueuedStory implements Callable<ThrowableStory> {

        private final PerformableTree performableTree;
        private final RunContext context;
        private final EmbedderControls embedderControls;
        private final EmbedderMonitor embedderMonitor;
        private final Story story;
        private final StoryTimeouts timeouts;
        private long startedAtMillis;

        public EnqueuedStory(PerformableTree performableTree,
                RunContext context, EmbedderControls embedderControls,
                EmbedderMonitor embedderMonitor, Story story, StoryTimeouts timeouts) {
            this.performableTree = performableTree;
            this.context = context;
            this.embedderControls = embedderControls;
            this.embedderMonitor = embedderMonitor;
            this.story = story;
            this.timeouts = timeouts;
        }

        @Override
        public ThrowableStory call() {
            startedAtMillis = System.currentTimeMillis();
            String storyPath = story.getPath();
            try {
                embedderMonitor.runningStory(storyPath);
                performableTree.perform(context, story);
            } catch (Throwable e) {
                if (embedderControls.ignoreFailureInStories()) {
                    embedderMonitor.storyFailed(storyPath, e);
                } else {
                    return new ThrowableStory(story, new StoryExecutionFailed(
                            storyPath, e));
                }
            }
            return new ThrowableStory(story, null);
        }

        public Story getStory() {
            return story;
        }

        public long getStartedAtMillis() {
            return startedAtMillis;
        }

        public long getTimeoutInSecs() {
            return timeouts.getTimeoutInSecs(story);
        }

    }

    @SuppressWarnings("serial")
    public static class StoryExecutionFailed extends RuntimeException {

        public StoryExecutionFailed(String storyPath, Throwable failure) {
            super(storyPath, failure);
        }

    }

    @SuppressWarnings("serial")
    public static class StoryTimedOut extends RuntimeException {

        public StoryTimedOut(StoryDuration storyDuration) {
            super(storyDuration.getDurationInSecs() + "s > "
                    + storyDuration.getTimeoutInSecs() + "s");
        }

    }

    public static class ThrowableStory {
        private Story story;
        private Throwable throwable;

        public ThrowableStory(Story story, Throwable throwable) {
            this.story = story;
            this.throwable = throwable;
        }

        public Story getStory() {
            return story;
        }

        public Throwable getThrowable() {
            return throwable;
        }
    }

    public static class RunningStory {
        private EnqueuedStory enqueuedStory;
        private Future<ThrowableStory> future;
        private StoryDuration duration;

        public RunningStory(EnqueuedStory enqueuedStory,
                Future<ThrowableStory> future) {
            this.enqueuedStory = enqueuedStory;
            this.future = future;
        }

        public Future<ThrowableStory> getFuture() {
            return future;
        }

        public Story getStory() {
            return enqueuedStory.getStory();
        }

        public long getDurationInMillis() {
            if (duration == null) {
                return 0;
            }
            return duration.getDurationInSecs() * 1000;
        }

        public StoryDuration getDuration() {
            if (duration == null) {
                duration = new StoryDuration(enqueuedStory.getStartedAtMillis(), enqueuedStory.getTimeoutInSecs());
            }
            return duration;
        }

        public void updateDuration() {
            duration.update();
        }

        public boolean isDone() {
            return future.isDone();
        }

        public boolean isFailed() {
            if (isDone()) {
                try {
                    return future.get().getThrowable() != null;
                } catch (InterruptedException | ExecutionException e) {
                    // swallow exception quietly
                }
            }
            return false;
        }
        
        public boolean isStarted() {
            return enqueuedStory.getStartedAtMillis() != 0;
        }
    }

    public static class StoryOutcome {
        private String path;
        private Boolean done;
        private Boolean failed;

        public StoryOutcome(RunningStory story) {
            this.path = story.getStory().getPath();
            this.done = story.isDone();
            this.failed = story.isFailed();
        }

        public String getPath() {
            return path;
        }

        public Boolean isDone() {
            return done;
        }

        public Boolean isFailed() {
            return failed;
        }

    }

}