package org.apache.maven.plugins.surefire.report; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.File; import java.text.NumberFormat; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import org.apache.maven.doxia.markup.HtmlMarkup; import org.apache.maven.doxia.sink.Sink; import org.apache.maven.doxia.sink.SinkEventAttributeSet; import org.apache.maven.doxia.sink.SinkEventAttributes; import org.apache.maven.doxia.util.DoxiaUtils; import org.apache.maven.plugin.surefire.log.api.ConsoleLogger; import org.apache.maven.reporting.MavenReportException; import static org.apache.maven.doxia.sink.Sink.JUSTIFY_LEFT; /** * */ public final class SurefireReportGenerator { private static final int LEFT = JUSTIFY_LEFT; private static final Object[] TAG_TYPE_START = { HtmlMarkup.TAG_TYPE_START }; private static final Object[] TAG_TYPE_END = { HtmlMarkup.TAG_TYPE_END }; private final SurefireReportParser report; private final boolean showSuccess; private final String xrefLocation; private List<ReportTestSuite> testSuites; public SurefireReportGenerator( List<File> reportsDirectories, Locale locale, boolean showSuccess, String xrefLocation, ConsoleLogger consoleLogger ) { report = new SurefireReportParser( reportsDirectories, locale, consoleLogger ); this.showSuccess = showSuccess; this.xrefLocation = xrefLocation; } public void doGenerateReport( ResourceBundle bundle, Sink sink ) throws MavenReportException { testSuites = report.parseXMLReportFiles(); sink.head(); sink.title(); sink.text( bundle.getString( "report.surefire.header" ) ); sink.title_(); sink.head_(); sink.body(); SinkEventAttributeSet atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.TYPE, "text/javascript" ); sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts ); sink.unknown( "cdata", new Object[]{ HtmlMarkup.CDATA_TYPE, javascriptToggleDisplayCode() }, null ); sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null ); sink.section1(); sink.sectionTitle1(); sink.text( bundle.getString( "report.surefire.header" ) ); sink.sectionTitle1_(); sink.section1_(); constructSummarySection( bundle, sink ); Map<String, List<ReportTestSuite>> suitePackages = report.getSuitesGroupByPackage( testSuites ); if ( !suitePackages.isEmpty() ) { constructPackagesSection( bundle, sink, suitePackages ); } if ( !testSuites.isEmpty() ) { constructTestCasesSection( bundle, sink ); } List<ReportTestCase> failureList = report.getFailureDetails( testSuites ); if ( !failureList.isEmpty() ) { constructFailureDetails( sink, bundle, failureList ); } sink.body_(); sink.flush(); sink.close(); } private void constructSummarySection( ResourceBundle bundle, Sink sink ) { Map<String, String> summary = report.getSummary( testSuites ); sink.section1(); sink.sectionTitle1(); sink.text( bundle.getString( "report.surefire.label.summary" ) ); sink.sectionTitle1_(); sinkAnchor( sink, "Summary" ); constructHotLinks( sink, bundle ); sinkLineBreak( sink ); sink.table(); sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true ); sink.tableRow(); sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) ); sink.tableRow_(); sink.tableRow(); sinkCell( sink, summary.get( "totalTests" ) ); sinkCell( sink, summary.get( "totalErrors" ) ); sinkCell( sink, summary.get( "totalFailures" ) ); sinkCell( sink, summary.get( "totalSkipped" ) ); sinkCell( sink, summary.get( "totalPercentage" ) + "%" ); sinkCell( sink, summary.get( "totalElapsedTime" ) ); sink.tableRow_(); sink.tableRows_(); sink.table_(); sink.lineBreak(); sink.paragraph(); sink.text( bundle.getString( "report.surefire.text.note1" ) ); sink.paragraph_(); sinkLineBreak( sink ); sink.section1_(); } private void constructPackagesSection( ResourceBundle bundle, Sink sink, Map<String, List<ReportTestSuite>> suitePackages ) { NumberFormat numberFormat = report.getNumberFormat(); sink.section1(); sink.sectionTitle1(); sink.text( bundle.getString( "report.surefire.label.packagelist" ) ); sink.sectionTitle1_(); sinkAnchor( sink, "Package_List" ); constructHotLinks( sink, bundle ); sinkLineBreak( sink ); sink.table(); sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true ); sink.tableRow(); sinkHeader( sink, bundle.getString( "report.surefire.label.package" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) ); sink.tableRow_(); for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() ) { sink.tableRow(); String packageName = entry.getKey(); List<ReportTestSuite> testSuiteList = entry.getValue(); Map<String, String> packageSummary = report.getSummary( testSuiteList ); sinkCellLink( sink, packageName, "#" + packageName ); sinkCell( sink, packageSummary.get( "totalTests" ) ); sinkCell( sink, packageSummary.get( "totalErrors" ) ); sinkCell( sink, packageSummary.get( "totalFailures" ) ); sinkCell( sink, packageSummary.get( "totalSkipped" ) ); sinkCell( sink, packageSummary.get( "totalPercentage" ) + "%" ); sinkCell( sink, packageSummary.get( "totalElapsedTime" ) ); sink.tableRow_(); } sink.tableRows_(); sink.table_(); sink.lineBreak(); sink.paragraph(); sink.text( bundle.getString( "report.surefire.text.note2" ) ); sink.paragraph_(); for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() ) { String packageName = entry.getKey(); List<ReportTestSuite> testSuiteList = entry.getValue(); sink.section2(); sink.sectionTitle2(); sink.text( packageName ); sink.sectionTitle2_(); sinkAnchor( sink, packageName ); boolean showTable = false; for ( ReportTestSuite suite : testSuiteList ) { if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 ) { showTable = true; break; } } if ( showTable ) { sink.table(); sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true ); sink.tableRow(); sinkHeader( sink, "" ); sinkHeader( sink, bundle.getString( "report.surefire.label.class" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) ); sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) ); sink.tableRow_(); for ( ReportTestSuite suite : testSuiteList ) { if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 ) { constructTestSuiteSection( sink, numberFormat, suite ); } } sink.tableRows_(); sink.table_(); } sink.section2_(); } sinkLineBreak( sink ); sink.section1_(); } private void constructTestSuiteSection( Sink sink, NumberFormat numberFormat, ReportTestSuite suite ) { sink.tableRow(); sink.tableCell(); sink.link( "#" + suite.getPackageName() + suite.getName() ); if ( suite.getNumberOfErrors() > 0 ) { sinkIcon( "error", sink ); } else if ( suite.getNumberOfFailures() > 0 ) { sinkIcon( "junit.framework", sink ); } else if ( suite.getNumberOfSkipped() > 0 ) { sinkIcon( "skipped", sink ); } else { sinkIcon( "success", sink ); } sink.link_(); sink.tableCell_(); sinkCellLink( sink, suite.getName(), "#" + suite.getPackageName() + suite.getName() ); sinkCell( sink, Integer.toString( suite.getNumberOfTests() ) ); sinkCell( sink, Integer.toString( suite.getNumberOfErrors() ) ); sinkCell( sink, Integer.toString( suite.getNumberOfFailures() ) ); sinkCell( sink, Integer.toString( suite.getNumberOfSkipped() ) ); String percentage = report.computePercentage( suite.getNumberOfTests(), suite.getNumberOfErrors(), suite.getNumberOfFailures(), suite.getNumberOfSkipped() ); sinkCell( sink, percentage + "%" ); sinkCell( sink, numberFormat.format( suite.getTimeElapsed() ) ); sink.tableRow_(); } private void constructTestCasesSection( ResourceBundle bundle, Sink sink ) { NumberFormat numberFormat = report.getNumberFormat(); sink.section1(); sink.sectionTitle1(); sink.text( bundle.getString( "report.surefire.label.testcases" ) ); sink.sectionTitle1_(); sinkAnchor( sink, "Test_Cases" ); constructHotLinks( sink, bundle ); for ( ReportTestSuite suite : testSuites ) { List<ReportTestCase> testCases = suite.getTestCases(); if ( testCases != null && !testCases.isEmpty() ) { sink.section2(); sink.sectionTitle2(); sink.text( suite.getName() ); sink.sectionTitle2_(); sinkAnchor( sink, suite.getPackageName() + suite.getName() ); boolean showTable = false; for ( ReportTestCase testCase : testCases ) { if ( testCase.hasFailure() || showSuccess ) { showTable = true; break; } } if ( showTable ) { sink.table(); sink.tableRows( new int[]{ LEFT, LEFT, LEFT }, true ); for ( ReportTestCase testCase : testCases ) { if ( testCase.hasFailure() || showSuccess ) { constructTestCaseSection( sink, numberFormat, testCase ); } } sink.tableRows_(); sink.table_(); } sink.section2_(); } } sinkLineBreak( sink ); sink.section1_(); } private void constructTestCaseSection( Sink sink, NumberFormat numberFormat, ReportTestCase testCase ) { sink.tableRow(); sink.tableCell(); if ( testCase.getFailureType() != null ) { sink.link( "#" + toHtmlId( testCase.getFullName() ) ); sinkIcon( testCase.getFailureType(), sink ); sink.link_(); } else { sinkIcon( "success", sink ); } sink.tableCell_(); if ( testCase.hasFailure() ) { sink.tableCell(); sinkAnchor( sink, "TC_" + toHtmlId( testCase.getFullName() ) ); sinkLink( sink, testCase.getName(), "#" + toHtmlId( testCase.getFullName() ) ); SinkEventAttributeSet atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.CLASS, "detailToggle" ); atts.addAttribute( SinkEventAttributes.STYLE, "display:inline" ); sink.unknown( "div", TAG_TYPE_START, atts ); sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" ); atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.STYLE, "display:inline;" ); atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "off" ); sink.unknown( "span", TAG_TYPE_START, atts ); sink.text( " + " ); sink.unknown( "span", TAG_TYPE_END, null ); atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" ); atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "on" ); sink.unknown( "span", TAG_TYPE_START, atts ); sink.text( " - " ); sink.unknown( "span", TAG_TYPE_END, null ); sink.text( "[ Detail ]" ); sink.link_(); sink.unknown( "div", TAG_TYPE_END, null ); sink.tableCell_(); } else { sinkCellAnchor( sink, testCase.getName(), "TC_" + toHtmlId( testCase.getFullName() ) ); } sinkCell( sink, numberFormat.format( testCase.getTime() ) ); sink.tableRow_(); if ( testCase.hasFailure() ) { sink.tableRow(); sinkCell( sink, "" ); sinkCell( sink, testCase.getFailureMessage() ); sinkCell( sink, "" ); sink.tableRow_(); String detail = testCase.getFailureDetail(); if ( detail != null ) { sink.tableRow(); sinkCell( sink, "" ); sink.tableCell(); SinkEventAttributeSet atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "error" ); atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" ); sink.unknown( "div", TAG_TYPE_START, atts ); sink.verbatim( null ); sink.text( detail ); sink.verbatim_(); sink.unknown( "div", TAG_TYPE_END, null ); sink.tableCell_(); sinkCell( sink, "" ); sink.tableRow_(); } } } private String toHtmlId( String id ) { return DoxiaUtils.isValidId( id ) ? id : DoxiaUtils.encodeId( id, true ); } private void constructFailureDetails( Sink sink, ResourceBundle bundle, List<ReportTestCase> failures ) { sink.section1(); sink.sectionTitle1(); sink.text( bundle.getString( "report.surefire.label.failuredetails" ) ); sink.sectionTitle1_(); sinkAnchor( sink, "Failure_Details" ); constructHotLinks( sink, bundle ); sinkLineBreak( sink ); sink.table(); sink.tableRows( new int[]{ LEFT, LEFT }, true ); for ( ReportTestCase tCase : failures ) { sink.tableRow(); sink.tableCell(); String type = tCase.getFailureType(); sinkIcon( type, sink ); sink.tableCell_(); sinkCellAnchor( sink, tCase.getName(), toHtmlId( tCase.getFullName() ) ); sink.tableRow_(); String message = tCase.getFailureMessage(); sink.tableRow(); sinkCell( sink, "" ); sinkCell( sink, message == null ? type : type + ": " + message ); sink.tableRow_(); String detail = tCase.getFailureDetail(); if ( detail != null ) { sink.tableRow(); sinkCell( sink, "" ); sink.tableCell(); SinkEventAttributeSet atts = new SinkEventAttributeSet(); atts.addAttribute( SinkEventAttributes.ID, tCase.getName() + "error" ); sink.unknown( "div", TAG_TYPE_START, atts ); String fullClassName = tCase.getFullClassName(); String errorLineNumber = tCase.getFailureErrorLine(); if ( xrefLocation != null ) { String path = fullClassName.replace( '.', '/' ); sink.link( xrefLocation + "/" + path + ".html#" + errorLineNumber ); } sink.text( fullClassName + ":" + errorLineNumber ); if ( xrefLocation != null ) { sink.link_(); } sink.unknown( "div", TAG_TYPE_END, null ); sink.tableCell_(); sink.tableRow_(); } } sink.tableRows_(); sink.table_(); sinkLineBreak( sink ); sink.section1_(); } private void constructHotLinks( Sink sink, ResourceBundle bundle ) { if ( !testSuites.isEmpty() ) { sink.paragraph(); sink.text( "[" ); sinkLink( sink, bundle.getString( "report.surefire.label.summary" ), "#Summary" ); sink.text( "]" ); sink.text( " [" ); sinkLink( sink, bundle.getString( "report.surefire.label.packagelist" ), "#Package_List" ); sink.text( "]" ); sink.text( " [" ); sinkLink( sink, bundle.getString( "report.surefire.label.testcases" ), "#Test_Cases" ); sink.text( "]" ); sink.paragraph_(); } } private void sinkLineBreak( Sink sink ) { sink.lineBreak(); } private void sinkIcon( String type, Sink sink ) { sink.figure(); if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) ) { sink.figureGraphics( "images/icon_warning_sml.gif" ); } else if ( type.startsWith( "success" ) ) { sink.figureGraphics( "images/icon_success_sml.gif" ); } else { sink.figureGraphics( "images/icon_error_sml.gif" ); } sink.figure_(); } private void sinkHeader( Sink sink, String header ) { sink.tableHeaderCell(); sink.text( header ); sink.tableHeaderCell_(); } private void sinkCell( Sink sink, String text ) { sink.tableCell(); sink.text( text ); sink.tableCell_(); } private void sinkLink( Sink sink, String text, String link ) { sink.link( link ); sink.text( text ); sink.link_(); } private void sinkCellLink( Sink sink, String text, String link ) { sink.tableCell(); sinkLink( sink, text, link ); sink.tableCell_(); } private void sinkCellAnchor( Sink sink, String text, String anchor ) { sink.tableCell(); sinkAnchor( sink, anchor ); sink.text( text ); sink.tableCell_(); } private void sinkAnchor( Sink sink, String anchor ) { sink.anchor( anchor ); sink.anchor_(); } private static String javascriptToggleDisplayCode() { // the javascript code is emitted within a commented CDATA section // so we have to start with a newline and comment the CDATA closing in the end return "\n" + "function toggleDisplay(elementId) {\n" + " var elm = document.getElementById(elementId + 'error');\n" + " if (elm && typeof elm.style != \"undefined\") {\n" + " if (elm.style.display == \"none\") {\n" + " elm.style.display = \"\";\n" + " document.getElementById(elementId + 'off').style.display = \"none\";\n" + " document.getElementById(elementId + 'on').style.display = \"inline\";\n" + " }" + " else if (elm.style.display == \"\") {" + " elm.style.display = \"none\";\n" + " document.getElementById(elementId + 'off').style.display = \"inline\";\n" + " document.getElementById(elementId + 'on').style.display = \"none\";\n" + " } \n" + " } \n" + " }\n" + "//"; } }