2121package org .sonar .plugins .objectivec .surefire ;
2222
2323import com .google .common .collect .ImmutableList ;
24- import org .apache .commons .lang .StringEscapeUtils ;
24+ import org .apache .commons .lang .StringUtils ;
2525import org .slf4j .Logger ;
2626import org .slf4j .LoggerFactory ;
2727import org .sonar .api .batch .SensorContext ;
2828import org .sonar .api .batch .fs .FileSystem ;
2929import org .sonar .api .batch .fs .InputFile ;
30+ import org .sonar .api .component .ResourcePerspectives ;
3031import org .sonar .api .measures .CoreMetrics ;
31- import org .sonar .api .measures .Measure ;
3232import org .sonar .api .measures .Metric ;
3333import org .sonar .api .resources .Project ;
3434import org .sonar .api .resources .Qualifiers ;
3535import org .sonar .api .resources .Resource ;
36+ import org .sonar .api .test .MutableTestPlan ;
37+ import org .sonar .api .test .TestCase ;
3638import org .sonar .api .utils .ParsingUtils ;
39+ import org .sonar .api .utils .SonarException ;
3740import org .sonar .api .utils .StaxParser ;
38- import org .sonar .api .utils .XmlParserException ;
3941import org .sonar .plugins .objectivec .ObjectiveC ;
40- import org .sonar .plugins .surefire .TestCaseDetails ;
41- import org .sonar .plugins .surefire .TestSuiteParser ;
42- import org .sonar .plugins .surefire .TestSuiteReport ;
42+ import org .sonar .plugins .objectivec .surefire .data .SurefireStaxHandler ;
43+ import org .sonar .plugins .objectivec .surefire .data .UnitTestClassReport ;
44+ import org .sonar .plugins .objectivec .surefire .data .UnitTestIndex ;
45+ import org .sonar .plugins .objectivec .surefire .data .UnitTestResult ;
4346
44- import javax .xml .transform . TransformerException ;
47+ import javax .xml .stream . XMLStreamException ;
4548import java .io .File ;
4649import java .io .FilenameFilter ;
47- import java .util .HashSet ;
4850import java .util .List ;
49- import java .util .Set ;
51+ import java .util .Map ;
5052
5153public final class SurefireParser {
5254 private static final Logger LOGGER = LoggerFactory .getLogger (SurefireParser .class );
5355
56+ private final FileSystem fileSystem ;
5457 private final Project project ;
5558 private final SensorContext context ;
56- private final FileSystem fileSystem ;
59+ private final ResourcePerspectives perspectives ;
5760
58- public SurefireParser (Project project , SensorContext context , FileSystem fileSystem ) {
61+ public SurefireParser (FileSystem fileSystem , Project project , ResourcePerspectives perspectives ,
62+ SensorContext context ) {
63+ this .fileSystem = fileSystem ;
5964 this .project = project ;
65+ this .perspectives = perspectives ;
6066 this .context = context ;
61- this .fileSystem = fileSystem ;
6267 }
6368
6469 public void collect (File reportsDir ) {
@@ -88,75 +93,78 @@ private void insertZeroWhenNoReports() {
8893 }
8994
9095 private void parseFiles (File [] reports ) {
91- Set <TestSuiteReport > analyzedReports = new HashSet <TestSuiteReport >();
92- try {
93- for (File report : reports ) {
94- TestSuiteParser parserHandler = new TestSuiteParser ();
95- StaxParser parser = new StaxParser (parserHandler , false );
96- parser .parse (report );
96+ UnitTestIndex index = new UnitTestIndex ();
97+ parseFiles (reports , index );
98+ sanitize (index );
99+ save (index );
100+ }
97101
98- for (TestSuiteReport fileReport : parserHandler .getParsedReports ()) {
99- if (!fileReport .isValid () || analyzedReports .contains (fileReport )) {
100- continue ;
101- }
102- if (fileReport .getTests () > 0 ) {
103- double testsCount = fileReport .getTests () - fileReport .getSkipped ();
104- saveClassMeasure (fileReport , CoreMetrics .SKIPPED_TESTS , fileReport .getSkipped ());
105- saveClassMeasure (fileReport , CoreMetrics .TESTS , testsCount );
106- saveClassMeasure (fileReport , CoreMetrics .TEST_ERRORS , fileReport .getErrors ());
107- saveClassMeasure (fileReport , CoreMetrics .TEST_FAILURES , fileReport .getFailures ());
108- saveClassMeasure (fileReport , CoreMetrics .TEST_EXECUTION_TIME , fileReport .getTimeMS ());
109- double passedTests = testsCount - fileReport .getErrors () - fileReport .getFailures ();
110- if (testsCount > 0 ) {
111- double percentage = passedTests * 100d / testsCount ;
112- saveClassMeasure (fileReport , CoreMetrics .TEST_SUCCESS_DENSITY , ParsingUtils .scaleValue (percentage ));
113- }
114- saveTestsDetails (fileReport );
115- analyzedReports .add (fileReport );
116- }
117- }
102+ private static void parseFiles (File [] reports , UnitTestIndex index ) {
103+ SurefireStaxHandler staxParser = new SurefireStaxHandler (index );
104+ StaxParser parser = new StaxParser (staxParser , false );
105+ for (File report : reports ) {
106+ try {
107+ parser .parse (report );
108+ } catch (XMLStreamException e ) {
109+ throw new SonarException ("Fail to parse the Surefire report: " + report , e );
118110 }
119-
120- } catch (Exception e ) {
121- throw new XmlParserException ("Can not parse surefire reports" , e );
122111 }
123112 }
124113
125- private void saveTestsDetails (TestSuiteReport fileReport ) throws TransformerException {
126- StringBuilder testCaseDetails = new StringBuilder (256 );
127- testCaseDetails .append ("<tests-details>" );
128- List <TestCaseDetails > details = fileReport .getDetails ();
129- for (TestCaseDetails detail : details ) {
130- testCaseDetails .append ("<testcase status=\" " ).append (detail .getStatus ())
131- .append ("\" time=\" " ).append (detail .getTimeMS ())
132- .append ("\" name=\" " ).append (detail .getName ()).append ("\" " );
133- boolean isError = detail .getStatus ().equals (TestCaseDetails .STATUS_ERROR );
134- if (isError || detail .getStatus ().equals (TestCaseDetails .STATUS_FAILURE )) {
135- testCaseDetails .append (">" )
136- .append (isError ? "<error message=\" " : "<failure message=\" " )
137- .append (StringEscapeUtils .escapeXml (detail .getErrorMessage ())).append ("\" >" )
138- .append ("<![CDATA[" ).append (StringEscapeUtils .escapeXml (detail .getStackTrace ())).append ("]]>" )
139- .append (isError ? "</error>" : "</failure>" ).append ("</testcase>" );
140- } else {
141- testCaseDetails .append ("/>" );
114+ private static void sanitize (UnitTestIndex index ) {
115+ for (String classname : index .getClassnames ()) {
116+ if (StringUtils .contains (classname , "$" )) {
117+ // Surefire reports classes whereas sonar supports files
118+ String parentClassName = StringUtils .substringBefore (classname , "$" );
119+ index .merge (classname , parentClassName );
142120 }
143121 }
144- testCaseDetails .append ("</tests-details>" );
145- context .saveMeasure (getUnitTestResource (fileReport .getClassKey ()), new Measure (CoreMetrics .TEST_DATA , testCaseDetails .toString ()));
146122 }
147123
148- private void saveClassMeasure (TestSuiteReport fileReport , Metric metric , double value ) {
149- if (!Double .isNaN (value )) {
150-
151- String className = fileReport .getClassKey ();
124+ private void save (UnitTestIndex index ) {
125+ long negativeTimeTestNumber = 0 ;
126+ for (Map .Entry <String , UnitTestClassReport > entry : index .getIndexByClassname ().entrySet ()) {
127+ UnitTestClassReport report = entry .getValue ();
128+ if (report .getTests () > 0 ) {
129+ negativeTimeTestNumber += report .getNegativeTimeTestNumber ();
130+ Resource resource = getUnitTestResource (entry .getKey ());
131+ if (resource != null ) {
132+ save (report , resource );
133+ } else {
134+ LOGGER .warn ("Resource not found: {}" , entry .getKey ());
135+ }
136+ }
137+ }
138+ if (negativeTimeTestNumber > 0 ) {
139+ LOGGER .warn ("There is {} test(s) reported with negative time by surefire, total duration may not be accurate." , negativeTimeTestNumber );
140+ }
141+ }
152142
153- context .saveMeasure (getUnitTestResource (className ), metric , value );
143+ private void save (UnitTestClassReport report , Resource resource ) {
144+ double testsCount = report .getTests () - report .getSkipped ();
145+ saveMeasure (resource , CoreMetrics .SKIPPED_TESTS , report .getSkipped ());
146+ saveMeasure (resource , CoreMetrics .TESTS , testsCount );
147+ saveMeasure (resource , CoreMetrics .TEST_ERRORS , report .getErrors ());
148+ saveMeasure (resource , CoreMetrics .TEST_FAILURES , report .getFailures ());
149+ saveMeasure (resource , CoreMetrics .TEST_EXECUTION_TIME , report .getDurationMilliseconds ());
150+ double passedTests = testsCount - report .getErrors () - report .getFailures ();
151+ if (testsCount > 0 ) {
152+ double percentage = passedTests * 100d / testsCount ;
153+ saveMeasure (resource , CoreMetrics .TEST_SUCCESS_DENSITY , ParsingUtils .scaleValue (percentage ));
154+ }
155+ saveResults (resource , report );
156+ }
154157
155- // Try with + in name
156- try {
157- context .saveMeasure (getUnitTestResource (className .replace ('_' , '+' )), metric , value );
158- } catch (Exception e ) {
159- // no-op - file was probably already registered successfully
158+ protected void saveResults (Resource testFile , UnitTestClassReport report ) {
159+ for (UnitTestResult unitTestResult : report .getResults ()) {
160+ MutableTestPlan testPlan = perspectives .as (MutableTestPlan .class , testFile );
161+ if (testPlan != null ) {
162+ testPlan .addTestCase (unitTestResult .getName ())
163+ .setDurationInMs (Math .max (unitTestResult .getDurationMilliseconds (), 0 ))
164+ .setStatus (TestCase .Status .of (unitTestResult .getStatus ()))
165+ .setMessage (unitTestResult .getMessage ())
166+ .setType (TestCase .TYPE_UNIT )
167+ .setStackTrace (unitTestResult .getStackTrace ());
160168 }
161169 }
162170 }
@@ -170,12 +178,11 @@ public Resource getUnitTestResource(String classname) {
170178 }
171179
172180 /*
173- * Most xcodebuild JUnit parsers don't include the path to the class in the class field, so search for if it
181+ * Most xcodebuild JUnit parsers don't include the path to the class in the class field, so search for it if it
174182 * wasn't found in the root.
175183 */
176184 if (!file .isFile () || !file .exists ()) {
177185 List <File > files = ImmutableList .copyOf (fileSystem .files (fileSystem .predicates ().and (
178- fileSystem .predicates ().hasLanguage (ObjectiveC .KEY ),
179186 fileSystem .predicates ().hasType (InputFile .Type .TEST ),
180187 fileSystem .predicates ().matchesPathPattern ("**/" + fileName ))));
181188
@@ -194,4 +201,10 @@ public Resource getUnitTestResource(String classname) {
194201 sonarFile .setQualifier (Qualifiers .UNIT_TEST_FILE );
195202 return sonarFile ;
196203 }
204+
205+ private void saveMeasure (Resource resource , Metric metric , double value ) {
206+ if (!Double .isNaN (value )) {
207+ context .saveMeasure (resource , metric , value );
208+ }
209+ }
197210}
0 commit comments