SurefireReporter.java
package org.jbehave.core.reporters;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jbehave.core.embedder.PerformableTree.PerformableRoot;
import org.jbehave.core.embedder.PerformableTree.PerformableScenario;
import org.jbehave.core.embedder.PerformableTree.PerformableStory;
import org.jbehave.core.embedder.PerformableTree.Status;
import org.jbehave.core.model.Scenario;
import org.jbehave.core.model.Story;
import org.xml.sax.SAXException;
public class SurefireReporter {
private static final String SUREFIRE_FTL = "ftl/surefire-xml-report.ftl";
private static final String SUREFIRE_XSD = "xsd/surefire-test-report.xsd";
private static final String XML = ".xml";
private static final String DOT = ".";
private static final String HYPHEN = "-";
private static final String SLASH = "/";
private final Class<?> embeddableClass;
private final TestCaseNamingStrategy namingStrategy;
private final boolean includeProperties;
private final String reportName;
private final boolean reportByStory;
private TemplateProcessor processor = new FreemarkerProcessor();
public static class Options {
public static final String DEFAULT_REPORT_NAME = "jbehave-surefire";
public static final TestCaseNamingStrategy DEFAULT_NAMING_STRATEGY = new SimpleNamingStrategy();
public static final boolean DEFAULT_INCLUDE_PROPERTIES = true;
public static final boolean DEFAULT_REPORT_BY_STORY = false;
private String reportName;
private TestCaseNamingStrategy namingStrategy;
private boolean includeProperties;
private boolean reportByStory;
public Options() {
this(DEFAULT_REPORT_NAME, DEFAULT_NAMING_STRATEGY, DEFAULT_REPORT_BY_STORY, DEFAULT_INCLUDE_PROPERTIES);
}
public Options(String reportName, TestCaseNamingStrategy namingStrategy, boolean reportByStory,
boolean includeProperties) {
this.reportName = reportName;
this.namingStrategy = namingStrategy;
this.includeProperties = includeProperties;
this.reportByStory = reportByStory;
}
public Options useReportName(String reportName) {
this.reportName = reportName;
return this;
}
public Options withNamingStrategy(TestCaseNamingStrategy strategy) {
this.namingStrategy = strategy;
return this;
}
public Options doReportByStory(boolean reportByStory) {
this.reportByStory = reportByStory;
return this;
}
public Options doIncludeProperties(boolean includeProperties) {
this.includeProperties = includeProperties;
return this;
}
}
public SurefireReporter(Class<?> embeddableClass) {
this(embeddableClass, new Options());
}
public SurefireReporter(Class<?> embeddableClass, Options options) {
this.embeddableClass = embeddableClass;
this.namingStrategy = options.namingStrategy;
this.includeProperties = options.includeProperties;
this.reportName = options.reportName;
this.reportByStory = options.reportByStory;
}
public synchronized void generate(PerformableRoot root,
File outputDirectory) {
List<PerformableStory> stories = root.getStories();
if (reportByStory) {
for (PerformableStory story : stories) {
String name = reportName(story.getStory().getPath());
File file = outputFile(outputDirectory, name);
generateReport(asList(story), file);
}
} else {
File file = outputFile(outputDirectory, reportName);
generateReport(stories, file);
}
}
private String reportName(String path) {
return reportName + HYPHEN + StringUtils.replaceAll(StringUtils.substringBefore(path, DOT), SLASH, DOT);
}
private void generateReport(List<PerformableStory> stories, File file) {
try {
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("testsuite", new TestSuite(embeddableClass, namingStrategy, stories, includeProperties));
processor.process(SUREFIRE_FTL, dataModel, new FileWriter(file));
validateOutput(file, SUREFIRE_XSD);
} catch (IOException | SAXException e) {
throw new RuntimeException("Failed to generate surefire report", e);
}
}
private File outputFile(File outputDirectory, String name) {
File outputDir = new File(outputDirectory, "view");
outputDir.mkdirs();
if (!name.endsWith(XML)) {
name = name + XML;
}
return new File(outputDir, name);
}
private void validateOutput(File file, String surefireXsd) throws SAXException, IOException {
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = schemaFactory.newSchema(
new StreamSource(this.getClass().getClassLoader().getResourceAsStream(surefireXsd)));
Validator validator = schema.newValidator();
validator.validate(new StreamSource(file));
}
public static class TestSuite {
private final Class<?> embeddableClass;
private final TestCaseNamingStrategy namingStrategy;
private final TestCounts testCounts;
private final List<TestCase> testCases;
private final boolean includeProperties;
public TestSuite(Class<?> embeddableClass, TestCaseNamingStrategy namingStrategy,
List<PerformableStory> stories, boolean includeProperties) {
this.embeddableClass = embeddableClass;
this.namingStrategy = namingStrategy;
this.testCounts = collectTestCounts(stories);
this.testCases = collectTestCases(stories);
this.includeProperties = includeProperties;
}
private TestCounts collectTestCounts(List<PerformableStory> stories) {
TestCounts counts = new TestCounts();
for (PerformableStory story : stories) {
for (PerformableScenario scenario : story.getScenarios()) {
Status status = scenario.getStatus();
if (status == null) {
counts.addSkipped();
continue;
}
switch (status) {
case FAILED:
counts.addFailure();
break;
case PENDING:
case EXCLUDED:
case NOT_PERFORMED:
counts.addSkipped();
break;
case SUCCESSFUL:
counts.addSuccessful();
break;
default:
throw new IllegalArgumentException("Unsupported status: " + status);
}
}
}
return counts;
}
private long totalTime(List<TestCase> testCases) {
long total = 0;
for (TestCase tc : testCases) {
total += tc.getTime();
}
return total;
}
private List<TestCase> collectTestCases(List<PerformableStory> stories) {
List<TestCase> testCases = new ArrayList<>();
for (PerformableStory story : stories) {
for (PerformableScenario scenario : story.getScenarios()) {
String name = namingStrategy.resolveName(story.getStory(), scenario.getScenario());
long time = scenario.getTiming().getDurationInMillis();
TestCase tc = new TestCase(embeddableClass, name, time);
if (scenario.getStatus() == Status.FAILED) {
tc.setFailure(new TestFailure(scenario.getFailure()));
}
testCases.add(tc);
}
}
return testCases;
}
public String getName() {
return embeddableClass.getName();
}
public long getTime() {
return totalTime(testCases);
}
public int getTests() {
return testCounts.getTests();
}
public int getSkipped() {
return testCounts.getSkipped();
}
public int getErrors() {
return testCounts.getErrors();
}
public int getFailures() {
return testCounts.getFailures();
}
public Properties getProperties() {
return includeProperties ? System.getProperties() : new Properties();
}
public List<TestCase> getTestCases() {
return testCases;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
}
}
public static class TestCase {
private final Class<?> embeddableClass;
private final String name;
private long time;
private TestFailure failure;
public TestCase(Class<?> embeddableClass, String name, long time) {
this.embeddableClass = embeddableClass;
this.name = name;
this.time = time;
}
public String getName() {
return name;
}
public String getClassname() {
return embeddableClass.getName();
}
public long getTime() {
return time;
}
public boolean hasFailure() {
return failure != null;
}
public TestFailure getFailure() {
return failure;
}
public void setFailure(TestFailure failure) {
this.failure = failure;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
}
}
public interface TestCaseNamingStrategy {
String resolveName(Story story, Scenario scenario);
}
/**
* A simple naming strategy: [story name].[scenario title]
*/
public static class SimpleNamingStrategy implements TestCaseNamingStrategy {
@Override
public String resolveName(Story story, Scenario scenario) {
String path = story.getPath();
File file = new File(path);
String name = StringUtils.substringBefore(file.getName(), DOT);
return name + DOT + scenario.getTitle();
}
}
/**
* A breadcrumb-based naming strategy: [story path with breadcrumbs].[story name].[scenario title]
*/
public static class BreadcrumbNamingStrategy implements TestCaseNamingStrategy {
private static final String DEFAULT_BREADCRUMB = " > ";
private final String breadcrumb;
public BreadcrumbNamingStrategy() {
this(DEFAULT_BREADCRUMB);
}
public BreadcrumbNamingStrategy(String breadcrumb) {
this.breadcrumb = breadcrumb;
}
@Override
public String resolveName(Story story, Scenario scenario) {
String path = story.getPath();
File file = new File(path);
List<String> parentNames = new ArrayList<>();
collectParentNames(file, parentNames);
String parentPath = StringUtils.join(parentNames, breadcrumb);
String name = StringUtils.substringBefore(file.getName(), DOT);
return parentPath + breadcrumb + name + DOT + scenario.getTitle();
}
private void collectParentNames(File file, List<String> parents) {
if (file.getParent() != null) {
String name = file.getParentFile().getName();
if (!StringUtils.isBlank(name)) {
parents.add(0, name);
}
collectParentNames(file.getParentFile(), parents);
}
}
}
public static class TestFailure {
private final Throwable failure;
public TestFailure(Throwable failure) {
this.failure = failure;
}
public boolean hasFailure() {
return failure != null;
}
public String getMessage() {
if (hasFailure()) {
return EscapeMode.XML.escapeString(failure.getMessage());
}
return EMPTY;
}
public String getType() {
if (hasFailure()) {
return failure.getClass().getName();
}
return EMPTY;
}
public String getStackTrace() {
if (hasFailure()) {
String stackTrace = new StackTraceFormatter(true).stackTrace(failure);
return EscapeMode.XML.escapeString(stackTrace);
}
return EMPTY;
}
}
public static class TestCounts {
private int tests = 0;
private int skipped = 0;
private int errors = 0;
private int failures = 0;
public int getTests() {
return tests;
}
public int getSkipped() {
return skipped;
}
public int getErrors() {
return errors;
}
public int getFailures() {
return failures;
}
public void addFailure() {
failures++;
tests++;
}
public void addSkipped() {
skipped++;
tests++;
}
public void addSuccessful() {
tests++;
}
}
}