SurefireReporter.java

  1. package org.jbehave.core.reporters;

  2. import static java.util.Arrays.asList;
  3. import static org.apache.commons.lang3.StringUtils.EMPTY;

  4. import java.io.File;
  5. import java.io.FileWriter;
  6. import java.io.IOException;
  7. import java.util.ArrayList;
  8. import java.util.HashMap;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.Properties;
  12. import javax.xml.XMLConstants;
  13. import javax.xml.transform.stream.StreamSource;
  14. import javax.xml.validation.Schema;
  15. import javax.xml.validation.SchemaFactory;
  16. import javax.xml.validation.Validator;

  17. import org.apache.commons.lang3.StringUtils;
  18. import org.apache.commons.lang3.builder.ToStringBuilder;
  19. import org.apache.commons.lang3.builder.ToStringStyle;
  20. import org.jbehave.core.embedder.PerformableTree.PerformableRoot;
  21. import org.jbehave.core.embedder.PerformableTree.PerformableScenario;
  22. import org.jbehave.core.embedder.PerformableTree.PerformableStory;
  23. import org.jbehave.core.embedder.PerformableTree.Status;
  24. import org.jbehave.core.model.Scenario;
  25. import org.jbehave.core.model.Story;
  26. import org.xml.sax.SAXException;

  27. public class SurefireReporter {

  28.     private static final String SUREFIRE_FTL = "ftl/surefire-xml-report.ftl";
  29.     private static final String SUREFIRE_XSD = "xsd/surefire-test-report.xsd";
  30.     private static final String XML = ".xml";
  31.     private static final String DOT = ".";
  32.     private static final String HYPHEN = "-";
  33.     private static final String SLASH = "/";

  34.     private final Class<?> embeddableClass;
  35.     private final TestCaseNamingStrategy namingStrategy;
  36.     private final boolean includeProperties;
  37.     private final String reportName;
  38.     private final boolean reportByStory;

  39.     private TemplateProcessor processor = new FreemarkerProcessor();

  40.     public static class Options {
  41.         public static final String DEFAULT_REPORT_NAME = "jbehave-surefire";
  42.         public static final TestCaseNamingStrategy DEFAULT_NAMING_STRATEGY = new SimpleNamingStrategy();
  43.         public static final boolean DEFAULT_INCLUDE_PROPERTIES = true;
  44.         public static final boolean DEFAULT_REPORT_BY_STORY = false;

  45.         private String reportName;
  46.         private TestCaseNamingStrategy namingStrategy;
  47.         private boolean includeProperties;
  48.         private boolean reportByStory;

  49.         public Options() {
  50.             this(DEFAULT_REPORT_NAME, DEFAULT_NAMING_STRATEGY, DEFAULT_REPORT_BY_STORY, DEFAULT_INCLUDE_PROPERTIES);
  51.         }

  52.         public Options(String reportName, TestCaseNamingStrategy namingStrategy, boolean reportByStory,
  53.                 boolean includeProperties) {
  54.             this.reportName = reportName;
  55.             this.namingStrategy = namingStrategy;
  56.             this.includeProperties = includeProperties;
  57.             this.reportByStory = reportByStory;
  58.         }

  59.         public Options useReportName(String reportName) {
  60.             this.reportName = reportName;
  61.             return this;
  62.         }

  63.         public Options withNamingStrategy(TestCaseNamingStrategy strategy) {
  64.             this.namingStrategy = strategy;
  65.             return this;
  66.         }

  67.         public Options doReportByStory(boolean reportByStory) {
  68.             this.reportByStory = reportByStory;
  69.             return this;
  70.         }

  71.         public Options doIncludeProperties(boolean includeProperties) {
  72.             this.includeProperties = includeProperties;
  73.             return this;
  74.         }

  75.     }

  76.     public SurefireReporter(Class<?> embeddableClass) {
  77.         this(embeddableClass, new Options());
  78.     }

  79.     public SurefireReporter(Class<?> embeddableClass, Options options) {
  80.         this.embeddableClass = embeddableClass;
  81.         this.namingStrategy = options.namingStrategy;
  82.         this.includeProperties = options.includeProperties;
  83.         this.reportName = options.reportName;
  84.         this.reportByStory = options.reportByStory;
  85.     }

  86.     public synchronized void generate(PerformableRoot root,
  87.                                       File outputDirectory) {
  88.         List<PerformableStory> stories = root.getStories();
  89.         if (reportByStory) {
  90.             for (PerformableStory story : stories) {
  91.                 String name = reportName(story.getStory().getPath());
  92.                 File file = outputFile(outputDirectory, name);
  93.                 generateReport(asList(story), file);
  94.             }
  95.         } else {
  96.             File file = outputFile(outputDirectory, reportName);
  97.             generateReport(stories, file);
  98.         }
  99.     }

  100.     private String reportName(String path) {
  101.         return reportName + HYPHEN + StringUtils.replaceAll(StringUtils.substringBefore(path, DOT), SLASH, DOT);
  102.     }

  103.     private void generateReport(List<PerformableStory> stories, File file) {
  104.         try {
  105.             Map<String, Object> dataModel = new HashMap<>();
  106.             dataModel.put("testsuite", new TestSuite(embeddableClass, namingStrategy, stories, includeProperties));
  107.             processor.process(SUREFIRE_FTL, dataModel, new FileWriter(file));
  108.             validateOutput(file, SUREFIRE_XSD);
  109.         } catch (IOException | SAXException e) {
  110.             throw new RuntimeException("Failed to generate surefire report", e);
  111.         }
  112.     }

  113.     private File outputFile(File outputDirectory, String name) {
  114.         File outputDir = new File(outputDirectory, "view");
  115.         outputDir.mkdirs();
  116.         if (!name.endsWith(XML)) {
  117.             name = name + XML;
  118.         }
  119.         return new File(outputDir, name);
  120.     }

  121.     private void validateOutput(File file, String surefireXsd) throws SAXException, IOException {
  122.         SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
  123.         Schema schema = schemaFactory.newSchema(
  124.                 new StreamSource(this.getClass().getClassLoader().getResourceAsStream(surefireXsd)));
  125.         Validator validator = schema.newValidator();
  126.         validator.validate(new StreamSource(file));
  127.     }

  128.     public static class TestSuite {

  129.         private final Class<?> embeddableClass;
  130.         private final TestCaseNamingStrategy namingStrategy;
  131.         private final TestCounts testCounts;
  132.         private final List<TestCase> testCases;
  133.         private final boolean includeProperties;

  134.         public TestSuite(Class<?> embeddableClass, TestCaseNamingStrategy namingStrategy,
  135.                 List<PerformableStory> stories, boolean includeProperties) {
  136.             this.embeddableClass = embeddableClass;
  137.             this.namingStrategy = namingStrategy;
  138.             this.testCounts = collectTestCounts(stories);
  139.             this.testCases = collectTestCases(stories);
  140.             this.includeProperties = includeProperties;
  141.         }

  142.         private TestCounts collectTestCounts(List<PerformableStory> stories) {
  143.             TestCounts counts = new TestCounts();
  144.             for (PerformableStory story : stories) {
  145.                 for (PerformableScenario scenario : story.getScenarios()) {
  146.                     Status status = scenario.getStatus();
  147.                     if (status == null) {
  148.                         counts.addSkipped();
  149.                         continue;
  150.                     }
  151.                     switch (status) {
  152.                         case FAILED:
  153.                             counts.addFailure();
  154.                             break;
  155.                         case PENDING:
  156.                         case EXCLUDED:
  157.                         case NOT_PERFORMED:
  158.                             counts.addSkipped();
  159.                             break;
  160.                         case SUCCESSFUL:
  161.                             counts.addSuccessful();
  162.                             break;
  163.                         default:
  164.                             throw new IllegalArgumentException("Unsupported status: " + status);
  165.                     }
  166.                 }
  167.             }
  168.             return counts;
  169.         }

  170.         private long totalTime(List<TestCase> testCases) {
  171.             long total = 0;
  172.             for (TestCase tc : testCases) {
  173.                 total += tc.getTime();
  174.             }
  175.             return total;
  176.         }

  177.         private List<TestCase> collectTestCases(List<PerformableStory> stories) {
  178.             List<TestCase> testCases = new ArrayList<>();
  179.             for (PerformableStory story : stories) {
  180.                 for (PerformableScenario scenario : story.getScenarios()) {
  181.                     String name = namingStrategy.resolveName(story.getStory(), scenario.getScenario());
  182.                     long time = scenario.getTiming().getDurationInMillis();
  183.                     TestCase tc = new TestCase(embeddableClass, name, time);
  184.                     if (scenario.getStatus() == Status.FAILED) {
  185.                         tc.setFailure(new TestFailure(scenario.getFailure()));
  186.                     }
  187.                     testCases.add(tc);
  188.                 }
  189.             }
  190.             return testCases;
  191.         }

  192.         public String getName() {
  193.             return embeddableClass.getName();
  194.         }

  195.         public long getTime() {
  196.             return totalTime(testCases);
  197.         }

  198.         public int getTests() {
  199.             return testCounts.getTests();
  200.         }

  201.         public int getSkipped() {
  202.             return testCounts.getSkipped();
  203.         }

  204.         public int getErrors() {
  205.             return testCounts.getErrors();
  206.         }

  207.         public int getFailures() {
  208.             return testCounts.getFailures();
  209.         }

  210.         public Properties getProperties() {
  211.             return includeProperties ? System.getProperties() : new Properties();
  212.         }

  213.         public List<TestCase> getTestCases() {
  214.             return testCases;
  215.         }

  216.         @Override
  217.         public String toString() {
  218.             return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
  219.         }
  220.     }

  221.     public static class TestCase {
  222.         private final Class<?> embeddableClass;
  223.         private final String name;
  224.         private long time;
  225.         private TestFailure failure;

  226.         public TestCase(Class<?> embeddableClass, String name, long time) {
  227.             this.embeddableClass = embeddableClass;
  228.             this.name = name;
  229.             this.time = time;
  230.         }

  231.         public String getName() {
  232.             return name;
  233.         }

  234.         public String getClassname() {
  235.             return embeddableClass.getName();
  236.         }

  237.         public long getTime() {
  238.             return time;
  239.         }

  240.         public boolean hasFailure() {
  241.             return failure != null;
  242.         }

  243.         public TestFailure getFailure() {
  244.             return failure;
  245.         }

  246.         public void setFailure(TestFailure failure) {
  247.             this.failure = failure;
  248.         }

  249.         @Override
  250.         public String toString() {
  251.             return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
  252.         }

  253.     }

  254.     public interface TestCaseNamingStrategy {

  255.         String resolveName(Story story, Scenario scenario);

  256.     }

  257.     /**
  258.      * A simple naming strategy:  [story name].[scenario title]
  259.      */
  260.     public static class SimpleNamingStrategy implements TestCaseNamingStrategy {

  261.         @Override
  262.         public String resolveName(Story story, Scenario scenario) {
  263.             String path = story.getPath();
  264.             File file = new File(path);
  265.             String name = StringUtils.substringBefore(file.getName(), DOT);
  266.             return name + DOT + scenario.getTitle();
  267.         }

  268.     }

  269.     /**
  270.      * A breadcrumb-based naming strategy:  [story path with breadcrumbs].[story name].[scenario title]
  271.      */
  272.     public static class BreadcrumbNamingStrategy implements TestCaseNamingStrategy {

  273.         private static final String DEFAULT_BREADCRUMB = " > ";

  274.         private final String breadcrumb;

  275.         public BreadcrumbNamingStrategy() {
  276.             this(DEFAULT_BREADCRUMB);
  277.         }

  278.         public BreadcrumbNamingStrategy(String breadcrumb) {
  279.             this.breadcrumb = breadcrumb;
  280.         }

  281.         @Override
  282.         public String resolveName(Story story, Scenario scenario) {
  283.             String path = story.getPath();
  284.             File file = new File(path);
  285.             List<String> parentNames = new ArrayList<>();
  286.             collectParentNames(file, parentNames);
  287.             String parentPath = StringUtils.join(parentNames, breadcrumb);
  288.             String name = StringUtils.substringBefore(file.getName(), DOT);
  289.             return parentPath + breadcrumb + name + DOT + scenario.getTitle();
  290.         }

  291.         private void collectParentNames(File file, List<String> parents) {
  292.             if (file.getParent() != null) {
  293.                 String name = file.getParentFile().getName();
  294.                 if (!StringUtils.isBlank(name)) {
  295.                     parents.add(0, name);
  296.                 }
  297.                 collectParentNames(file.getParentFile(), parents);
  298.             }
  299.         }


  300.     }

  301.     public static class TestFailure {

  302.         private final Throwable failure;

  303.         public TestFailure(Throwable failure) {
  304.             this.failure = failure;
  305.         }

  306.         public boolean hasFailure() {
  307.             return failure != null;
  308.         }

  309.         public String getMessage() {
  310.             if (hasFailure()) {
  311.                 return EscapeMode.XML.escapeString(failure.getMessage());
  312.             }
  313.             return EMPTY;
  314.         }

  315.         public String getType() {
  316.             if (hasFailure()) {
  317.                 return failure.getClass().getName();
  318.             }
  319.             return EMPTY;
  320.         }

  321.         public String getStackTrace() {
  322.             if (hasFailure()) {
  323.                 String stackTrace = new StackTraceFormatter(true).stackTrace(failure);
  324.                 return EscapeMode.XML.escapeString(stackTrace);
  325.             }
  326.             return EMPTY;
  327.         }
  328.     }

  329.     public static class TestCounts {

  330.         private int tests = 0;
  331.         private int skipped = 0;
  332.         private int errors = 0;
  333.         private int failures = 0;

  334.         public int getTests() {
  335.             return tests;
  336.         }

  337.         public int getSkipped() {
  338.             return skipped;
  339.         }

  340.         public int getErrors() {
  341.             return errors;
  342.         }

  343.         public int getFailures() {
  344.             return failures;
  345.         }

  346.         public void addFailure() {
  347.             failures++;
  348.             tests++;
  349.         }

  350.         public void addSkipped() {
  351.             skipped++;
  352.             tests++;
  353.         }

  354.         public void addSuccessful() {
  355.             tests++;
  356.         }
  357.     }
  358. }