StoryManager.java

  1. package org.jbehave.core.embedder;

  2. import java.io.File;
  3. import java.io.FileWriter;
  4. import java.io.IOException;
  5. import java.io.Writer;
  6. import java.util.ArrayList;
  7. import java.util.Collection;
  8. import java.util.HashMap;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.Properties;
  12. import java.util.concurrent.Callable;
  13. import java.util.concurrent.ExecutionException;
  14. import java.util.concurrent.ExecutorService;
  15. import java.util.concurrent.Future;

  16. import org.jbehave.core.configuration.Configuration;
  17. import org.jbehave.core.embedder.PerformableTree.PerformableRoot;
  18. import org.jbehave.core.embedder.PerformableTree.RunContext;
  19. import org.jbehave.core.embedder.StoryTimeouts.TimeoutParser;
  20. import org.jbehave.core.failures.BatchFailures;
  21. import org.jbehave.core.model.Story;
  22. import org.jbehave.core.model.StoryDuration;
  23. import org.jbehave.core.steps.InjectableStepsFactory;
  24. import org.jbehave.core.steps.StepCollector.Stage;

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

  31.     private final Configuration configuration;
  32.     private final EmbedderControls embedderControls;
  33.     private final EmbedderMonitor embedderMonitor;
  34.     private final ExecutorService executorService;
  35.     private final InjectableStepsFactory stepsFactory;
  36.     private final PerformableTree performableTree;
  37.     private final Map<String, RunningStory> runningStories = new HashMap<>();
  38.     private final Map<MetaFilter, List<Story>> excludedStories = new HashMap<>();
  39.     private RunContext context;
  40.     private StoryTimeouts timeouts;
  41.    
  42.     public StoryManager(Configuration configuration,
  43.             InjectableStepsFactory stepsFactory,
  44.             EmbedderControls embedderControls, EmbedderMonitor embedderMonitor,
  45.             ExecutorService executorService, PerformableTree performableTree, TimeoutParser... parsers) {
  46.         this.configuration = configuration;
  47.         this.embedderControls = embedderControls;
  48.         this.embedderMonitor = embedderMonitor;
  49.         this.executorService = executorService;
  50.         this.stepsFactory = stepsFactory;
  51.         this.performableTree = performableTree;
  52.         this.timeouts = new StoryTimeouts(embedderControls, embedderMonitor);
  53.         this.timeouts.withParsers(parsers);
  54.     }

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

  58.     public List<Story> storiesOfPaths(List<String> storyPaths) {
  59.         List<Story> stories = new ArrayList<>(storyPaths.size());
  60.         for (String storyPath : storyPaths) {
  61.             stories.add(storyOfPath(storyPath));
  62.         }
  63.         if (configuration.isParallelStoryExamplesEnabled()) {
  64.             StorySplitter storySplitter = new StorySplitter(configuration.storyControls().storyIndexFormat());
  65.             return storySplitter.splitStories(stories);
  66.         }
  67.         return stories;
  68.     }

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

  72.     public void clear() {
  73.         runningStories.clear();
  74.     }

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

  78.     public List<StoryOutcome> outcomes() {
  79.         List<StoryOutcome> outcomes = new ArrayList<>();
  80.         for (RunningStory story : runningStories.values()) {
  81.             outcomes.add(new StoryOutcome(story));
  82.         }
  83.         return outcomes;
  84.     }

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

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

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

  97.         // perform stories
  98.         performStories(context, performableTree, stories);

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

  101.     }

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

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

  111.         // run stories
  112.         runStories(context, stories);
  113.         waitUntilAllDoneOrFailed(context);
  114.         MetaFilter filter = context.filter();
  115.         List<Story> excluded = excludedBy(filter);
  116.         if (!excluded.isEmpty()) {
  117.             embedderMonitor.storiesExcluded(excluded, filter,
  118.                     embedderControls.verboseFiltering());
  119.         }

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

  123.     private void filterRunning(RunContext context, Story story) {
  124.         if (context.filter(story).excluded()) {
  125.             excludedBy(context.getFilter()).add(story);
  126.         } else {
  127.             runningStories.put(story.getPath(), runningStory(story));
  128.         }
  129.     }

  130.     public List<Story> excludedBy(MetaFilter filter) {
  131.         List<Story> stories = excludedStories.get(filter);
  132.         if (stories == null) {
  133.             stories = new ArrayList<>();
  134.             excludedStories.put(filter, stories);
  135.         }
  136.         return stories;
  137.     }

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

  142.     public void waitUntilAllDoneOrFailed(RunContext context) {
  143.         if (runningStories.values().isEmpty()) {
  144.             return;
  145.         }
  146.         boolean allDone = false;
  147.         boolean started = false;
  148.         while (!allDone || !started) {
  149.             allDone = true;
  150.             for (RunningStory runningStory : runningStories.values()) {
  151.                 if (runningStory.isStarted()) {
  152.                     started = true;
  153.                     Story story = runningStory.getStory();
  154.                     Future<ThrowableStory> future = runningStory.getFuture();
  155.                     if (!future.isDone()) {
  156.                         allDone = false;
  157.                         StoryDuration duration = runningStory.getDuration();
  158.                         runningStory.updateDuration();
  159.                         if (context.isCancelled(story)) {
  160.                             if (duration.cancelTimedOut()) {
  161.                                 future.cancel(true);
  162.                             }
  163.                             continue;
  164.                         }
  165.                         if (duration.timedOut()) {
  166.                             embedderMonitor.storyTimeout(story, duration);
  167.                             context.cancelStory(story, duration);
  168.                             if (embedderControls.failOnStoryTimeout()) {
  169.                                 throw new StoryExecutionFailed(story.getPath(),
  170.                                         new StoryTimedOut(duration));
  171.                             }
  172.                             continue;
  173.                         }
  174.                     } else {
  175.                         try {
  176.                             ThrowableStory throwableStory = future.get();
  177.                             Throwable throwable = throwableStory.getThrowable();
  178.                             if (throwable != null) {
  179.                                 context.addFailure(story, throwable);
  180.                                 if (!embedderControls.ignoreFailureInStories()) {
  181.                                     continue;
  182.                                 }
  183.                             }
  184.                         } catch (Throwable e) {
  185.                             context.addFailure(story, e);
  186.                             if (!embedderControls.ignoreFailureInStories()) {
  187.                                 continue;
  188.                             }
  189.                         }
  190.                     }
  191.                 } else {
  192.                     started = false;
  193.                     allDone = false;
  194.                 }
  195.             }
  196.             tickTock();
  197.         }
  198.         writeStoryDurations(runningStories.values());
  199.     }

  200.     protected void writeStoryDurations(Collection<RunningStory> runningStories) {
  201.         // collect story durations and cancel any outstanding execution which is
  202.         // not done before returning
  203.         Properties storyDurations = new Properties();
  204.         long total = 0;
  205.         for (RunningStory runningStory : runningStories) {
  206.             long durationInMillis = runningStory.getDurationInMillis();
  207.             total += durationInMillis;
  208.             storyDurations.setProperty(runningStory.getStory().getPath(),
  209.                     Long.toString(durationInMillis));
  210.             Future<ThrowableStory> future = runningStory.getFuture();
  211.             if (!future.isDone()) {
  212.                 future.cancel(true);
  213.             }
  214.         }
  215.         int threads = embedderControls.threads();
  216.         long threadAverage = total / threads;
  217.         storyDurations.setProperty("total", Long.toString(total));
  218.         storyDurations.setProperty("threads", Long.toString(threads));
  219.         storyDurations.setProperty("threadAverage",
  220.                 Long.toString(threadAverage));
  221.         write(storyDurations, "storyDurations.props");
  222.     }

  223.     private void write(Properties p, String name) {
  224.         File outputDirectory = configuration.storyReporterBuilder()
  225.                 .outputDirectory();
  226.         try {
  227.             outputDirectory.mkdirs();
  228.             Writer output = new FileWriter(new File(outputDirectory, name));
  229.             p.store(output, this.getClass().getName());
  230.             output.close();
  231.         } catch (IOException e) {
  232.             e.printStackTrace();
  233.         }
  234.     }

  235.     private void tickTock() {
  236.         try {
  237.             Thread.sleep(100);
  238.         } catch (InterruptedException e) {
  239.             // swallow exception quietly
  240.         }
  241.     }

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

  245.     static class EnqueuedStory implements Callable<ThrowableStory> {

  246.         private final PerformableTree performableTree;
  247.         private final RunContext context;
  248.         private final EmbedderControls embedderControls;
  249.         private final EmbedderMonitor embedderMonitor;
  250.         private final Story story;
  251.         private final StoryTimeouts timeouts;
  252.         private long startedAtMillis;

  253.         public EnqueuedStory(PerformableTree performableTree,
  254.                 RunContext context, EmbedderControls embedderControls,
  255.                 EmbedderMonitor embedderMonitor, Story story, StoryTimeouts timeouts) {
  256.             this.performableTree = performableTree;
  257.             this.context = context;
  258.             this.embedderControls = embedderControls;
  259.             this.embedderMonitor = embedderMonitor;
  260.             this.story = story;
  261.             this.timeouts = timeouts;
  262.         }

  263.         @Override
  264.         public ThrowableStory call() {
  265.             startedAtMillis = System.currentTimeMillis();
  266.             String storyPath = story.getPath();
  267.             try {
  268.                 embedderMonitor.runningStory(storyPath);
  269.                 performableTree.perform(context, story);
  270.             } catch (Throwable e) {
  271.                 if (embedderControls.ignoreFailureInStories()) {
  272.                     embedderMonitor.storyFailed(storyPath, e);
  273.                 } else {
  274.                     return new ThrowableStory(story, new StoryExecutionFailed(
  275.                             storyPath, e));
  276.                 }
  277.             }
  278.             return new ThrowableStory(story, null);
  279.         }

  280.         public Story getStory() {
  281.             return story;
  282.         }

  283.         public long getStartedAtMillis() {
  284.             return startedAtMillis;
  285.         }

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

  289.     }

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

  292.         public StoryExecutionFailed(String storyPath, Throwable failure) {
  293.             super(storyPath, failure);
  294.         }

  295.     }

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

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

  302.     }

  303.     public static class ThrowableStory {
  304.         private Story story;
  305.         private Throwable throwable;

  306.         public ThrowableStory(Story story, Throwable throwable) {
  307.             this.story = story;
  308.             this.throwable = throwable;
  309.         }

  310.         public Story getStory() {
  311.             return story;
  312.         }

  313.         public Throwable getThrowable() {
  314.             return throwable;
  315.         }
  316.     }

  317.     public static class RunningStory {
  318.         private EnqueuedStory enqueuedStory;
  319.         private Future<ThrowableStory> future;
  320.         private StoryDuration duration;

  321.         public RunningStory(EnqueuedStory enqueuedStory,
  322.                 Future<ThrowableStory> future) {
  323.             this.enqueuedStory = enqueuedStory;
  324.             this.future = future;
  325.         }

  326.         public Future<ThrowableStory> getFuture() {
  327.             return future;
  328.         }

  329.         public Story getStory() {
  330.             return enqueuedStory.getStory();
  331.         }

  332.         public long getDurationInMillis() {
  333.             if (duration == null) {
  334.                 return 0;
  335.             }
  336.             return duration.getDurationInSecs() * 1000;
  337.         }

  338.         public StoryDuration getDuration() {
  339.             if (duration == null) {
  340.                 duration = new StoryDuration(enqueuedStory.getStartedAtMillis(), enqueuedStory.getTimeoutInSecs());
  341.             }
  342.             return duration;
  343.         }

  344.         public void updateDuration() {
  345.             duration.update();
  346.         }

  347.         public boolean isDone() {
  348.             return future.isDone();
  349.         }

  350.         public boolean isFailed() {
  351.             if (isDone()) {
  352.                 try {
  353.                     return future.get().getThrowable() != null;
  354.                 } catch (InterruptedException | ExecutionException e) {
  355.                     // swallow exception quietly
  356.                 }
  357.             }
  358.             return false;
  359.         }
  360.        
  361.         public boolean isStarted() {
  362.             return enqueuedStory.getStartedAtMillis() != 0;
  363.         }
  364.     }

  365.     public static class StoryOutcome {
  366.         private String path;
  367.         private Boolean done;
  368.         private Boolean failed;

  369.         public StoryOutcome(RunningStory story) {
  370.             this.path = story.getStory().getPath();
  371.             this.done = story.isDone();
  372.             this.failed = story.isFailed();
  373.         }

  374.         public String getPath() {
  375.             return path;
  376.         }

  377.         public Boolean isDone() {
  378.             return done;
  379.         }

  380.         public Boolean isFailed() {
  381.             return failed;
  382.         }

  383.     }

  384. }