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);
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);
}
}