JUnit4StoryReporter.java

  1. package org.jbehave.core.junit;

  2. import java.util.ArrayList;
  3. import java.util.Arrays;
  4. import java.util.Collection;
  5. import java.util.Collections;
  6. import java.util.Deque;
  7. import java.util.HashSet;
  8. import java.util.Iterator;
  9. import java.util.LinkedList;
  10. import java.util.List;
  11. import java.util.Map;
  12. import java.util.Set;
  13. import java.util.concurrent.atomic.AtomicInteger;
  14. import java.util.function.Predicate;

  15. import org.apache.commons.lang3.tuple.Pair;
  16. import org.jbehave.core.configuration.Keywords;
  17. import org.jbehave.core.failures.FailingUponPendingStep;
  18. import org.jbehave.core.failures.PassingUponPendingStep;
  19. import org.jbehave.core.failures.PendingStepStrategy;
  20. import org.jbehave.core.failures.UUIDExceptionWrapper;
  21. import org.jbehave.core.junit.JUnit4DescriptionGenerator.JUnit4Test;
  22. import org.jbehave.core.model.Lifecycle;
  23. import org.jbehave.core.model.Scenario;
  24. import org.jbehave.core.model.Step;
  25. import org.jbehave.core.model.Story;
  26. import org.jbehave.core.reporters.NullStoryReporter;
  27. import org.jbehave.core.steps.StepCollector;
  28. import org.jbehave.core.steps.StepCollector.Stage;
  29. import org.jbehave.core.steps.StepCreator.PendingStep;
  30. import org.jbehave.core.steps.StepCreator.StepExecutionType;
  31. import org.jbehave.core.steps.Timing;
  32. import org.junit.runner.Description;
  33. import org.junit.runner.Result;
  34. import org.junit.runner.notification.Failure;
  35. import org.junit.runner.notification.RunNotifier;

  36. public class JUnit4StoryReporter extends NullStoryReporter {
  37.     private final RunNotifier notifier;

  38.     private final Description rootDescription;
  39.     private final Keywords keywords;
  40.     private PendingStepStrategy pendingStepStrategy = new PassingUponPendingStep();

  41.     private final ThreadLocal<TestState> testState = ThreadLocal.withInitial(TestState::new);

  42.     private final AtomicInteger testCounter = new AtomicInteger();

  43.     public JUnit4StoryReporter(RunNotifier notifier, Description rootDescription, Keywords keywords) {
  44.         this.rootDescription = rootDescription;
  45.         this.notifier = notifier;
  46.         this.keywords = keywords;
  47.     }

  48.     @Override
  49.     public void beforeStoriesSteps(StepCollector.Stage stage) {
  50.         String name = null;
  51.         if (stage == StepCollector.Stage.BEFORE) {
  52.             notifier.fireTestRunStarted(rootDescription);
  53.             name = "BeforeStories";
  54.         } else if (stage == StepCollector.Stage.AFTER) {
  55.             name = "AfterStories";
  56.         }

  57.         Description storyDescription = findStoryDescription(name);
  58.         TestState testState = this.testState.get();
  59.         testState.currentStoryDescription = storyDescription;
  60.         testState.currentStep = storyDescription;
  61.         notifier.fireTestStarted(storyDescription);

  62.     }

  63.     @Override
  64.     public void afterStoriesSteps(StepCollector.Stage stage) {
  65.         TestState testState = this.testState.get();
  66.         notifier.fireTestFinished(testState.currentStoryDescription);
  67.         testState.currentStepStatus = StepStatus.FINISHED;
  68.         if (stage == StepCollector.Stage.AFTER) {
  69.             Result result = new Result();
  70.             notifier.fireTestRunFinished(result);
  71.         }
  72.     }

  73.     @Override
  74.     public void beforeStory(Story story, boolean isGivenStory) {
  75.         TestState testState = this.testState.get();
  76.         if (isGivenStory) {
  77.             if (testState.currentStep != null) {
  78.                 notifier.fireTestStarted(testState.currentStep);
  79.             }
  80.             testState.givenStoryLevel++;
  81.         } else {
  82.             Description storyDescription = findStoryDescription(story.getName());
  83.             testState.currentStoryDescription = storyDescription;
  84.             notifier.fireTestStarted(storyDescription);

  85.             if (storyDescription.isSuite()) {
  86.                 testState.scenarioDescriptions = filter(storyDescription.getChildren(), Arrays.asList(
  87.                     Pair.of(ElementAction.DROP, isTest()),
  88.                     Pair.of(ElementAction.TAKE, isSuite())
  89.                 )).iterator();

  90.                 testState.moveToNextScenario();
  91.             }

  92.             testState.currentStep = testState.currentStoryDescription;
  93.         }
  94.     }

  95.     @Override
  96.     public void beforeStorySteps(Stage stage, Lifecycle.ExecutionType type) {
  97.         TestState testState = this.testState.get();
  98.         if (!testState.isGivenStoryRunning()) {

  99.             if (stage == Stage.BEFORE && type == Lifecycle.ExecutionType.SYSTEM) {
  100.                 loadStepDescriptions(filter(testState.getStoryChildren(), Collections.singletonList(
  101.                     Pair.of(ElementAction.TAKE, isTest())
  102.                 )));
  103.             }

  104.             if (stage == Stage.AFTER && type == Lifecycle.ExecutionType.USER) {
  105.                 loadStepDescriptions(filter(testState.getStoryChildren(), Arrays.asList(
  106.                     Pair.of(ElementAction.DROP, isTest()),
  107.                     Pair.of(ElementAction.DROP, isSuite()),
  108.                     Pair.of(ElementAction.TAKE, isTest())
  109.                 )));
  110.             }
  111.         }
  112.     }

  113.     private void loadStepDescriptions(List<Description> stepDescriptions) {
  114.         TestState testState = this.testState.get();
  115.         testState.loadStepDescriptions(stepDescriptions);

  116.         if (testState.stepDescriptions.hasNext()) {
  117.             testState.moveToNextStep();
  118.         }
  119.     }

  120.     private Predicate<Description> isTest() {
  121.         return description -> description.getAnnotation(JUnit4Test.class) != null;
  122.     }

  123.     private Predicate<Description> isSuite() {
  124.         return isTest().negate();
  125.     }

  126.     private List<Description> filter(List<Description> descriptions,
  127.             Collection<Pair<ElementAction, Predicate<Description>>> filters) {
  128.         Iterator<Pair<ElementAction, Predicate<Description>>> filtersIterator = filters.iterator();
  129.         Pair<ElementAction, Predicate<Description>> currentFilter = filtersIterator.next();

  130.         List<Description> resultDescriptions = new ArrayList<>();
  131.         for (Description description : descriptions) {
  132.             while (!currentFilter.getValue().test(description)) {
  133.                 if (!filtersIterator.hasNext()) {
  134.                     return resultDescriptions;
  135.                 }
  136.                 currentFilter = filtersIterator.next();
  137.             }

  138.             currentFilter.getKey().compute(resultDescriptions, description);
  139.         }

  140.         return resultDescriptions;
  141.     }

  142.     private Description findStoryDescription(String storyName) {
  143.         String escapedStoryName = TextManipulator.escape(storyName);
  144.         for (Description storyDescription : rootDescription.getChildren()) {
  145.             if (storyDescription.getDisplayName().equals(escapedStoryName)) {
  146.                 return storyDescription;
  147.             } else
  148.                 // Related to issue #28: When a story does not contain any scenarios, isTest returns true,
  149.                 // but getMethodName still returns null, because it cannot be parsed by JUnit as a method name.
  150.                 if (storyDescription.isTest() && storyDescription.getMethodName() != null && storyDescription
  151.                         .getMethodName().equals(storyName)) {
  152.                     // Story BeforeStories or AfterStories
  153.                     return storyDescription;
  154.                 }
  155.         }
  156.         throw new IllegalStateException("No JUnit description found for story with name: " + storyName);
  157.     }

  158.     @Override
  159.     public void afterStory(boolean isGivenStory) {
  160.         TestState testState = this.testState.get();
  161.         if (isGivenStory) {
  162.             testState.givenStoryLevel--;
  163.             if (testState.currentStep != null) {
  164.                 notifier.fireTestFinished(testState.currentStep);
  165.             }
  166.             prepareNextStep();
  167.         } else {
  168.             if (!testState.failedSteps.contains(testState.currentStoryDescription)) {
  169.                 notifier.fireTestFinished(testState.currentStoryDescription);
  170.                 if (testState.currentStoryDescription.isTest()) {
  171.                     testCounter.incrementAndGet();
  172.                 }
  173.             }
  174.             this.testState.remove();
  175.         }
  176.     }

  177.     @Override
  178.     public void beforeScenario(Scenario scenario) {
  179.         TestState testState = this.testState.get();
  180.         if (!testState.isGivenStoryRunning()) {
  181.             notifier.fireTestStarted(testState.currentScenario);

  182.             List<Description> children = testState.currentScenario.getChildren();
  183.             List<Description> examples = filterExamples(children);
  184.             if (!examples.isEmpty()) {
  185.                 testState.exampleDescriptions = examples.iterator();
  186.                 testState.currentExample = null;
  187.             }
  188.             if (children.size() > examples.size()) {
  189.                 // in case of given stories, these steps are actually stories,
  190.                 // for which events will be fired in beforeStory(..., true)
  191.                 List<Description> steps = new ArrayList<>(testState.currentScenario.getChildren());
  192.                 steps.removeAll(examples);
  193.                 testState.loadStepDescriptions(steps);
  194.                 testState.moveToNextStep();
  195.             }
  196.         }
  197.     }

  198.     private List<Description> filterExamples(List<Description> children) {
  199.         for (int i = 0; i < children.size(); i++) {
  200.             Description child = children.get(i);
  201.             boolean isExample = child.getDisplayName().startsWith(keywords.examplesTableRow() + " ");
  202.             if (isExample) {
  203.                 return children.subList(i, children.size());
  204.             }
  205.         }
  206.         return Collections.emptyList();
  207.     }

  208.     @Override
  209.     public void afterScenario(Timing timing) {
  210.         TestState testState = this.testState.get();
  211.         if (!testState.isGivenStoryRunning()) {
  212.             notifier.fireTestFinished(testState.currentScenario);
  213.             testState.moveToNextScenario();
  214.         }
  215.     }

  216.     @Override
  217.     public void example(Map<String, String> tableRow, int exampleIndex) {
  218.         TestState testState = this.testState.get();
  219.         if (!testState.isGivenStoryRunning()) {
  220.             testState.moveToNextExample();
  221.             testState.loadStepDescriptions(testState.currentExample.getChildren());
  222.             testState.moveToNextStep();
  223.         }
  224.     }

  225.     @Override
  226.     public void beforeStep(Step step) {
  227.         if (step.getExecutionType() != StepExecutionType.EXECUTABLE) {
  228.             return;
  229.         }
  230.         TestState testState = this.testState.get();
  231.         if (!testState.isGivenStoryRunning() && testState.currentStep != null) {
  232.             // Lifecycle Before story steps
  233.             if (testState.currentStep == testState.currentStoryDescription) {
  234.                 testState.currentStep = testState.currentScenario;
  235.             }
  236.             if (testState.currentStepStatus == StepStatus.STARTED) {
  237.                 testState.parentSteps.push(testState.currentStep);
  238.                 testState.moveToNextStep();
  239.             }
  240.             notifier.fireTestStarted(testState.currentStep);
  241.             testState.currentStepStatus = StepStatus.STARTED;
  242.         }
  243.     }

  244.     @Override
  245.     public void failed(String step, Throwable e) {
  246.         TestState testState = this.testState.get();
  247.         if (!testState.isGivenStoryRunning()) {
  248.             Throwable thrownException = e instanceof UUIDExceptionWrapper ? e.getCause() : e;
  249.             notifier.fireTestFailure(new Failure(testState.currentStep, thrownException));
  250.             testState.failedSteps.add(testState.currentStep);
  251.             finishStep(testState);
  252.         }
  253.     }

  254.     @Override
  255.     public void successful(String step) {
  256.         TestState testState = this.testState.get();
  257.         if (!testState.isGivenStoryRunning()) {
  258.             if (testState.currentStep != null) {
  259.                 finishStep(testState);
  260.             } else {
  261.                 prepareNextStep();
  262.             }
  263.         }
  264.     }

  265.     private void prepareNextStep() {
  266.         TestState testState = this.testState.get();
  267.         if (testState.currentStep != null && testState.currentStep.isTest()) {
  268.             testCounter.incrementAndGet();
  269.         }
  270.         if (testState.stepDescriptions != null && testState.stepDescriptions.hasNext()) {
  271.             testState.moveToNextStep();
  272.         }
  273.     }

  274.     private void finishStep(TestState testState) {
  275.         if (testState.currentStepStatus == StepStatus.FINISHED && !testState.parentSteps.isEmpty()) {
  276.             notifier.fireTestFinished(testState.parentSteps.poll());
  277.         } else {
  278.             notifier.fireTestFinished(testState.currentStep);
  279.             testState.currentStepStatus = StepStatus.FINISHED;
  280.             prepareNextStep();
  281.         }
  282.     }

  283.     @Override
  284.     public void pending(PendingStep step) {
  285.         TestState testState = this.testState.get();
  286.         if (!testState.isGivenStoryRunning()) {
  287.             if (pendingStepStrategy instanceof FailingUponPendingStep) {
  288.                 notifier.fireTestStarted(testState.currentStep);
  289.                 notifier.fireTestFailure(new Failure(testState.currentStep, new RuntimeException("Step is pending!")));
  290.                 // Pending step strategy says to fail so treat this step as
  291.                 // having failed.
  292.                 testState.failedSteps.add(testState.currentStep);
  293.                 finishStep(testState);
  294.             } else {
  295.                 notifier.fireTestIgnored(testState.currentStep);
  296.                 prepareNextStep();
  297.             }
  298.         }
  299.     }

  300.     @Override
  301.     public void ignorable(String step) {
  302.         TestState testState = this.testState.get();
  303.         if (!testState.isGivenStoryRunning()) {
  304.             notifier.fireTestIgnored(testState.currentStep);
  305.             testState.currentStepStatus = StepStatus.FINISHED;
  306.             prepareNextStep();
  307.         }
  308.     }

  309.     @Override
  310.     public void notPerformed(String step) {
  311.         ignorable(step);
  312.     }

  313.     /**
  314.      * Notify the IDE that the current step and scenario is not being executed.
  315.      * Reason is a JBehave meta tag is filtering out this scenario.
  316.      *
  317.      * @param scenario Scenario
  318.      * @param filter   Filter
  319.      */
  320.     @Override
  321.     public void scenarioExcluded(Scenario scenario, String filter) {
  322.         TestState testState = this.testState.get();
  323.         notifier.fireTestIgnored(testState.currentStep);
  324.         notifier.fireTestIgnored(testState.currentScenario);
  325.     }

  326.     public void usePendingStepStrategy(PendingStepStrategy pendingStepStrategy) {
  327.         this.pendingStepStrategy = pendingStepStrategy;
  328.     }

  329.     private class TestState {
  330.         private Description currentStep;
  331.         private StepStatus currentStepStatus;
  332.         private final Deque<Description> parentSteps = new LinkedList<>();
  333.         private Iterator<Description> stepDescriptions;

  334.         private Description currentScenario;
  335.         private Iterator<Description> scenarioDescriptions;

  336.         private Description currentExample;
  337.         private Iterator<Description> exampleDescriptions;

  338.         private Description currentStoryDescription;
  339.         private int givenStoryLevel;

  340.         private final Set<Description> failedSteps = new HashSet<>();

  341.         private void moveToNextScenario() {
  342.             currentScenario = getNextOrNull(scenarioDescriptions);
  343.             currentStep = currentScenario;
  344.             stepDescriptions = null;
  345.         }

  346.         private void moveToNextExample() {
  347.             currentExample = getNextOrNull(exampleDescriptions);
  348.         }

  349.         private void moveToNextStep() {
  350.             currentStep = getNextOrNull(stepDescriptions);
  351.         }

  352.         private boolean isGivenStoryRunning() {
  353.             return givenStoryLevel != 0;
  354.         }

  355.         private void loadStepDescriptions(List<Description> steps) {
  356.             stepDescriptions = getAllDescendants(steps).iterator();
  357.         }

  358.         private <T> T getNextOrNull(Iterator<T> iterator) {
  359.             return iterator.hasNext() ? iterator.next() : null;
  360.         }

  361.         private Collection<Description> getAllDescendants(List<Description> steps) {
  362.             List<Description> descendants = new ArrayList<>();
  363.             for (Description child : steps) {
  364.                 descendants.add(child);
  365.                 descendants.addAll(getAllDescendants(child.getChildren()));
  366.             }
  367.             return descendants;
  368.         }

  369.         private List<Description> getStoryChildren() {
  370.             return currentStoryDescription.getChildren();
  371.         }
  372.     }

  373.     private enum StepStatus {
  374.         STARTED, FINISHED
  375.     }

  376.     private enum ElementAction {
  377.         DROP {
  378.             @Override
  379.             public <T> void compute(Collection<T> collection, T element) {
  380.             }
  381.         },
  382.         TAKE {
  383.             @Override
  384.             public <T> void compute(Collection<T> collection, T element) {
  385.                 collection.add(element);
  386.             }
  387.         };

  388.         public abstract <T> void compute(Collection<T> collection, T element);
  389.     }
  390. }