JUnit4StoryReporter.java
- package org.jbehave.core.junit;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.Deque;
- import java.util.HashSet;
- import java.util.Iterator;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.atomic.AtomicInteger;
- import java.util.function.Predicate;
- import org.apache.commons.lang3.tuple.Pair;
- import org.jbehave.core.configuration.Keywords;
- import org.jbehave.core.failures.FailingUponPendingStep;
- import org.jbehave.core.failures.PassingUponPendingStep;
- import org.jbehave.core.failures.PendingStepStrategy;
- import org.jbehave.core.failures.UUIDExceptionWrapper;
- import org.jbehave.core.junit.JUnit4DescriptionGenerator.JUnit4Test;
- import org.jbehave.core.model.Lifecycle;
- import org.jbehave.core.model.Scenario;
- import org.jbehave.core.model.Step;
- import org.jbehave.core.model.Story;
- import org.jbehave.core.reporters.NullStoryReporter;
- 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.Timing;
- import org.junit.runner.Description;
- import org.junit.runner.Result;
- import org.junit.runner.notification.Failure;
- import org.junit.runner.notification.RunNotifier;
- public class JUnit4StoryReporter extends NullStoryReporter {
- private final RunNotifier notifier;
- private final Description rootDescription;
- private final Keywords keywords;
- private PendingStepStrategy pendingStepStrategy = new PassingUponPendingStep();
- private final ThreadLocal<TestState> testState = ThreadLocal.withInitial(TestState::new);
- private final AtomicInteger testCounter = new AtomicInteger();
- public JUnit4StoryReporter(RunNotifier notifier, Description rootDescription, Keywords keywords) {
- this.rootDescription = rootDescription;
- this.notifier = notifier;
- this.keywords = keywords;
- }
- @Override
- public void beforeStoriesSteps(StepCollector.Stage stage) {
- String name = null;
- if (stage == StepCollector.Stage.BEFORE) {
- notifier.fireTestRunStarted(rootDescription);
- name = "BeforeStories";
- } else if (stage == StepCollector.Stage.AFTER) {
- name = "AfterStories";
- }
- Description storyDescription = findStoryDescription(name);
- TestState testState = this.testState.get();
- testState.currentStoryDescription = storyDescription;
- testState.currentStep = storyDescription;
- notifier.fireTestStarted(storyDescription);
- }
- @Override
- public void afterStoriesSteps(StepCollector.Stage stage) {
- TestState testState = this.testState.get();
- notifier.fireTestFinished(testState.currentStoryDescription);
- testState.currentStepStatus = StepStatus.FINISHED;
- if (stage == StepCollector.Stage.AFTER) {
- Result result = new Result();
- notifier.fireTestRunFinished(result);
- }
- }
- @Override
- public void beforeStory(Story story, boolean isGivenStory) {
- TestState testState = this.testState.get();
- if (isGivenStory) {
- if (testState.currentStep != null) {
- notifier.fireTestStarted(testState.currentStep);
- }
- testState.givenStoryLevel++;
- } else {
- Description storyDescription = findStoryDescription(story.getName());
- testState.currentStoryDescription = storyDescription;
- notifier.fireTestStarted(storyDescription);
- if (storyDescription.isSuite()) {
- testState.scenarioDescriptions = filter(storyDescription.getChildren(), Arrays.asList(
- Pair.of(ElementAction.DROP, isTest()),
- Pair.of(ElementAction.TAKE, isSuite())
- )).iterator();
- testState.moveToNextScenario();
- }
- testState.currentStep = testState.currentStoryDescription;
- }
- }
- @Override
- public void beforeStorySteps(Stage stage, Lifecycle.ExecutionType type) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- if (stage == Stage.BEFORE && type == Lifecycle.ExecutionType.SYSTEM) {
- loadStepDescriptions(filter(testState.getStoryChildren(), Collections.singletonList(
- Pair.of(ElementAction.TAKE, isTest())
- )));
- }
- if (stage == Stage.AFTER && type == Lifecycle.ExecutionType.USER) {
- loadStepDescriptions(filter(testState.getStoryChildren(), Arrays.asList(
- Pair.of(ElementAction.DROP, isTest()),
- Pair.of(ElementAction.DROP, isSuite()),
- Pair.of(ElementAction.TAKE, isTest())
- )));
- }
- }
- }
- private void loadStepDescriptions(List<Description> stepDescriptions) {
- TestState testState = this.testState.get();
- testState.loadStepDescriptions(stepDescriptions);
- if (testState.stepDescriptions.hasNext()) {
- testState.moveToNextStep();
- }
- }
- private Predicate<Description> isTest() {
- return description -> description.getAnnotation(JUnit4Test.class) != null;
- }
- private Predicate<Description> isSuite() {
- return isTest().negate();
- }
- private List<Description> filter(List<Description> descriptions,
- Collection<Pair<ElementAction, Predicate<Description>>> filters) {
- Iterator<Pair<ElementAction, Predicate<Description>>> filtersIterator = filters.iterator();
- Pair<ElementAction, Predicate<Description>> currentFilter = filtersIterator.next();
- List<Description> resultDescriptions = new ArrayList<>();
- for (Description description : descriptions) {
- while (!currentFilter.getValue().test(description)) {
- if (!filtersIterator.hasNext()) {
- return resultDescriptions;
- }
- currentFilter = filtersIterator.next();
- }
- currentFilter.getKey().compute(resultDescriptions, description);
- }
- return resultDescriptions;
- }
- private Description findStoryDescription(String storyName) {
- String escapedStoryName = TextManipulator.escape(storyName);
- for (Description storyDescription : rootDescription.getChildren()) {
- if (storyDescription.getDisplayName().equals(escapedStoryName)) {
- return storyDescription;
- } else
- // Related to issue #28: When a story does not contain any scenarios, isTest returns true,
- // but getMethodName still returns null, because it cannot be parsed by JUnit as a method name.
- if (storyDescription.isTest() && storyDescription.getMethodName() != null && storyDescription
- .getMethodName().equals(storyName)) {
- // Story BeforeStories or AfterStories
- return storyDescription;
- }
- }
- throw new IllegalStateException("No JUnit description found for story with name: " + storyName);
- }
- @Override
- public void afterStory(boolean isGivenStory) {
- TestState testState = this.testState.get();
- if (isGivenStory) {
- testState.givenStoryLevel--;
- if (testState.currentStep != null) {
- notifier.fireTestFinished(testState.currentStep);
- }
- prepareNextStep();
- } else {
- if (!testState.failedSteps.contains(testState.currentStoryDescription)) {
- notifier.fireTestFinished(testState.currentStoryDescription);
- if (testState.currentStoryDescription.isTest()) {
- testCounter.incrementAndGet();
- }
- }
- this.testState.remove();
- }
- }
- @Override
- public void beforeScenario(Scenario scenario) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- notifier.fireTestStarted(testState.currentScenario);
- List<Description> children = testState.currentScenario.getChildren();
- List<Description> examples = filterExamples(children);
- if (!examples.isEmpty()) {
- testState.exampleDescriptions = examples.iterator();
- testState.currentExample = null;
- }
- if (children.size() > examples.size()) {
- // in case of given stories, these steps are actually stories,
- // for which events will be fired in beforeStory(..., true)
- List<Description> steps = new ArrayList<>(testState.currentScenario.getChildren());
- steps.removeAll(examples);
- testState.loadStepDescriptions(steps);
- testState.moveToNextStep();
- }
- }
- }
- private List<Description> filterExamples(List<Description> children) {
- for (int i = 0; i < children.size(); i++) {
- Description child = children.get(i);
- boolean isExample = child.getDisplayName().startsWith(keywords.examplesTableRow() + " ");
- if (isExample) {
- return children.subList(i, children.size());
- }
- }
- return Collections.emptyList();
- }
- @Override
- public void afterScenario(Timing timing) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- notifier.fireTestFinished(testState.currentScenario);
- testState.moveToNextScenario();
- }
- }
- @Override
- public void example(Map<String, String> tableRow, int exampleIndex) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- testState.moveToNextExample();
- testState.loadStepDescriptions(testState.currentExample.getChildren());
- testState.moveToNextStep();
- }
- }
- @Override
- public void beforeStep(Step step) {
- if (step.getExecutionType() != StepExecutionType.EXECUTABLE) {
- return;
- }
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning() && testState.currentStep != null) {
- // Lifecycle Before story steps
- if (testState.currentStep == testState.currentStoryDescription) {
- testState.currentStep = testState.currentScenario;
- }
- if (testState.currentStepStatus == StepStatus.STARTED) {
- testState.parentSteps.push(testState.currentStep);
- testState.moveToNextStep();
- }
- notifier.fireTestStarted(testState.currentStep);
- testState.currentStepStatus = StepStatus.STARTED;
- }
- }
- @Override
- public void failed(String step, Throwable e) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- Throwable thrownException = e instanceof UUIDExceptionWrapper ? e.getCause() : e;
- notifier.fireTestFailure(new Failure(testState.currentStep, thrownException));
- testState.failedSteps.add(testState.currentStep);
- finishStep(testState);
- }
- }
- @Override
- public void successful(String step) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- if (testState.currentStep != null) {
- finishStep(testState);
- } else {
- prepareNextStep();
- }
- }
- }
- private void prepareNextStep() {
- TestState testState = this.testState.get();
- if (testState.currentStep != null && testState.currentStep.isTest()) {
- testCounter.incrementAndGet();
- }
- if (testState.stepDescriptions != null && testState.stepDescriptions.hasNext()) {
- testState.moveToNextStep();
- }
- }
- private void finishStep(TestState testState) {
- if (testState.currentStepStatus == StepStatus.FINISHED && !testState.parentSteps.isEmpty()) {
- notifier.fireTestFinished(testState.parentSteps.poll());
- } else {
- notifier.fireTestFinished(testState.currentStep);
- testState.currentStepStatus = StepStatus.FINISHED;
- prepareNextStep();
- }
- }
- @Override
- public void pending(PendingStep step) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- if (pendingStepStrategy instanceof FailingUponPendingStep) {
- notifier.fireTestStarted(testState.currentStep);
- notifier.fireTestFailure(new Failure(testState.currentStep, new RuntimeException("Step is pending!")));
- // Pending step strategy says to fail so treat this step as
- // having failed.
- testState.failedSteps.add(testState.currentStep);
- finishStep(testState);
- } else {
- notifier.fireTestIgnored(testState.currentStep);
- prepareNextStep();
- }
- }
- }
- @Override
- public void ignorable(String step) {
- TestState testState = this.testState.get();
- if (!testState.isGivenStoryRunning()) {
- notifier.fireTestIgnored(testState.currentStep);
- testState.currentStepStatus = StepStatus.FINISHED;
- prepareNextStep();
- }
- }
- @Override
- public void notPerformed(String step) {
- ignorable(step);
- }
- /**
- * Notify the IDE that the current step and scenario is not being executed.
- * Reason is a JBehave meta tag is filtering out this scenario.
- *
- * @param scenario Scenario
- * @param filter Filter
- */
- @Override
- public void scenarioExcluded(Scenario scenario, String filter) {
- TestState testState = this.testState.get();
- notifier.fireTestIgnored(testState.currentStep);
- notifier.fireTestIgnored(testState.currentScenario);
- }
- public void usePendingStepStrategy(PendingStepStrategy pendingStepStrategy) {
- this.pendingStepStrategy = pendingStepStrategy;
- }
- private class TestState {
- private Description currentStep;
- private StepStatus currentStepStatus;
- private final Deque<Description> parentSteps = new LinkedList<>();
- private Iterator<Description> stepDescriptions;
- private Description currentScenario;
- private Iterator<Description> scenarioDescriptions;
- private Description currentExample;
- private Iterator<Description> exampleDescriptions;
- private Description currentStoryDescription;
- private int givenStoryLevel;
- private final Set<Description> failedSteps = new HashSet<>();
- private void moveToNextScenario() {
- currentScenario = getNextOrNull(scenarioDescriptions);
- currentStep = currentScenario;
- stepDescriptions = null;
- }
- private void moveToNextExample() {
- currentExample = getNextOrNull(exampleDescriptions);
- }
- private void moveToNextStep() {
- currentStep = getNextOrNull(stepDescriptions);
- }
- private boolean isGivenStoryRunning() {
- return givenStoryLevel != 0;
- }
- private void loadStepDescriptions(List<Description> steps) {
- stepDescriptions = getAllDescendants(steps).iterator();
- }
- private <T> T getNextOrNull(Iterator<T> iterator) {
- return iterator.hasNext() ? iterator.next() : null;
- }
- private Collection<Description> getAllDescendants(List<Description> steps) {
- List<Description> descendants = new ArrayList<>();
- for (Description child : steps) {
- descendants.add(child);
- descendants.addAll(getAllDescendants(child.getChildren()));
- }
- return descendants;
- }
- private List<Description> getStoryChildren() {
- return currentStoryDescription.getChildren();
- }
- }
- private enum StepStatus {
- STARTED, FINISHED
- }
- private enum ElementAction {
- DROP {
- @Override
- public <T> void compute(Collection<T> collection, T element) {
- }
- },
- TAKE {
- @Override
- public <T> void compute(Collection<T> collection, T element) {
- collection.add(element);
- }
- };
- public abstract <T> void compute(Collection<T> collection, T element);
- }
- }