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