Skip to content

Commit 541c14e

Browse files
author
Gilles Grousset
committed
Fixed source test file lookup
1 parent 2be0222 commit 541c14e

File tree

11 files changed

+429
-199
lines changed

11 files changed

+429
-199
lines changed

commons/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@
9090
<groupId>org.slf4j</groupId>
9191
<artifactId>slf4j-api</artifactId>
9292
</dependency>
93+
<dependency>
94+
<groupId>commons-lang</groupId>
95+
<artifactId>commons-lang</artifactId>
96+
<version>2.6</version>
97+
</dependency>
9398

9499
</dependencies>
95100

commons/src/main/java/com/backelite/sonarqube/commons/TestFileFinders.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void addFinder(TestFileFinder finder) {
3939
finders.add(finder);
4040
}
4141

42-
InputFile getUnitTestResource(FileSystem fileSystem, String classname) {
42+
public InputFile getUnitTestResource(FileSystem fileSystem, String classname) {
4343
for (TestFileFinder finder : finders) {
4444
InputFile result = finder.getUnitTestResource(fileSystem, classname);
4545
if (result != null) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* commons - Enables analysis of Swift and Objective-C projects into SonarQube.
3+
* Copyright © 2015 Backelite (${email})
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package com.backelite.sonarqube.commons.surefire;
19+
20+
import org.codehaus.staxmate.SMInputFactory;
21+
import com.ctc.wstx.stax.WstxInputFactory;
22+
import java.io.File;
23+
import java.io.FileInputStream;
24+
import java.io.IOException;
25+
import javax.xml.stream.XMLInputFactory;
26+
import javax.xml.stream.XMLStreamException;
27+
import org.codehaus.staxmate.in.SMHierarchicCursor;
28+
29+
public class StaxParser {
30+
31+
private SMInputFactory inf;
32+
private SurefireStaxHandler streamHandler;
33+
34+
public StaxParser(UnitTestIndex index) {
35+
this.streamHandler = new SurefireStaxHandler(index);
36+
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
37+
if (xmlInputFactory instanceof WstxInputFactory) {
38+
WstxInputFactory wstxInputfactory = (WstxInputFactory) xmlInputFactory;
39+
wstxInputfactory.configureForLowMemUsage();
40+
wstxInputfactory.getConfig().setUndeclaredEntityResolver((String publicID, String systemID, String baseURI, String namespace) -> namespace);
41+
}
42+
this.inf = new SMInputFactory(xmlInputFactory);
43+
}
44+
45+
public void parse(File xmlFile) throws XMLStreamException {
46+
try(FileInputStream input = new FileInputStream(xmlFile)) {
47+
parse(inf.rootElementCursor(input));
48+
} catch (IOException e) {
49+
throw new XMLStreamException(e);
50+
}
51+
}
52+
53+
private void parse(SMHierarchicCursor rootCursor) throws XMLStreamException {
54+
try {
55+
streamHandler.stream(rootCursor);
56+
} finally {
57+
rootCursor.getStreamReader().closeCompletely();
58+
}
59+
}
60+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* commons - Enables analysis of Swift and Objective-C projects into SonarQube.
3+
* Copyright © 2015 Backelite (${email})
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package com.backelite.sonarqube.commons.surefire;
19+
20+
import com.backelite.sonarqube.commons.TestFileFinders;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.sonar.api.batch.fs.FileSystem;
24+
import org.sonar.api.batch.fs.InputFile;
25+
import org.sonar.api.batch.sensor.SensorContext;
26+
import org.sonar.api.component.ResourcePerspectives;
27+
import org.sonar.api.measures.CoreMetrics;
28+
import org.sonar.api.measures.Metric;
29+
import org.sonar.api.test.MutableTestPlan;
30+
import org.sonar.api.test.TestCase;
31+
import org.sonar.squidbridge.api.AnalysisException;
32+
33+
import javax.annotation.CheckForNull;
34+
import javax.xml.parsers.DocumentBuilderFactory;
35+
import javax.xml.stream.XMLStreamException;
36+
import java.io.File;
37+
import java.io.IOException;
38+
import java.io.Serializable;
39+
import java.nio.file.DirectoryStream;
40+
import java.nio.file.Files;
41+
import java.nio.file.Path;
42+
import java.util.ArrayList;
43+
import java.util.HashMap;
44+
import java.util.List;
45+
import java.util.Map;
46+
47+
public class SurefireReportParser {
48+
private static final Logger LOGGER = LoggerFactory.getLogger(SurefireReportParser.class);
49+
private static final String TESTSUITE = "testsuite";
50+
private static final String TESTCASE = "testcase";
51+
52+
protected final SensorContext context;
53+
private final DocumentBuilderFactory dbfactory;
54+
private final UnitTestIndex index;
55+
private final ResourcePerspectives perspectives;
56+
private final FileSystem fileSystem;
57+
58+
protected SurefireReportParser(FileSystem fileSystem, ResourcePerspectives perspectives, SensorContext context) {
59+
this.fileSystem = fileSystem;
60+
this.context = context;
61+
this.perspectives = perspectives;
62+
this.dbfactory = DocumentBuilderFactory.newInstance();
63+
this.index = new UnitTestIndex();
64+
}
65+
66+
public void collect(File reportsDir) {
67+
List<File> xmlFiles = new ArrayList<>();
68+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(reportsDir.toPath(), "{TEST}*.{xml}")) {
69+
for (Path p: stream) {
70+
LOGGER.info("Processing Surefire report {}", p.getFileName());
71+
xmlFiles.add(p.toFile());
72+
}
73+
} catch (IOException e) {
74+
LOGGER.error( "Error while finding test files.", e);
75+
}
76+
77+
if (!xmlFiles.isEmpty()) {
78+
parseFiles(xmlFiles);
79+
}
80+
}
81+
82+
private void parseFiles(List<File> reports) {
83+
UnitTestIndex index = new UnitTestIndex();
84+
parseFiles(reports, index);
85+
save(index, context);
86+
}
87+
88+
private static void parseFiles(List<File> reports, UnitTestIndex index) {
89+
StaxParser parser = new StaxParser(index);
90+
for (File report : reports) {
91+
try {
92+
parser.parse(report);
93+
} catch (XMLStreamException e) {
94+
throw new AnalysisException("Fail to parse the Surefire report: " + report, e);
95+
}
96+
}
97+
}
98+
99+
private void save(UnitTestIndex index, SensorContext context) {
100+
long negativeTimeTestNumber = 0;
101+
Map<InputFile, UnitTestClassReport> indexByInputFile = mapToInputFile(index.getIndexByClassname());
102+
for (Map.Entry<InputFile, UnitTestClassReport> entry : indexByInputFile.entrySet()) {
103+
UnitTestClassReport report = entry.getValue();
104+
if (report.getTests() > 0) {
105+
negativeTimeTestNumber += report.getNegativeTimeTestNumber();
106+
save(report, entry.getKey(), context);
107+
}
108+
}
109+
if (negativeTimeTestNumber > 0) {
110+
LOGGER.warn("There is {} test(s) reported with negative time by surefire, total duration may not be accurate.", negativeTimeTestNumber);
111+
}
112+
}
113+
114+
private Map<InputFile, UnitTestClassReport> mapToInputFile(Map<String, UnitTestClassReport> indexByClassname) {
115+
Map<InputFile, UnitTestClassReport> result = new HashMap<>();
116+
indexByClassname.forEach((className, index) -> {
117+
InputFile resource = getUnitTestResource(className, index);
118+
if (resource != null) {
119+
UnitTestClassReport report = result.computeIfAbsent(resource, r -> new UnitTestClassReport());
120+
// in case of repeated/parameterized tests (JUnit 5.x) we may end up with tests having the same name
121+
index.getResults().forEach(report::add);
122+
} else {
123+
LOGGER.debug("Resource not found: {}", className);
124+
}
125+
});
126+
return result;
127+
}
128+
129+
private void save(UnitTestClassReport report, InputFile inputFile, SensorContext context) {
130+
int testsCount = report.getTests() - report.getSkipped();
131+
saveMeasure(context, inputFile, CoreMetrics.SKIPPED_TESTS, report.getSkipped());
132+
saveMeasure(context, inputFile, CoreMetrics.TESTS, testsCount);
133+
saveMeasure(context, inputFile, CoreMetrics.TEST_ERRORS, report.getErrors());
134+
saveMeasure(context, inputFile, CoreMetrics.TEST_FAILURES, report.getFailures());
135+
saveMeasure(context, inputFile, CoreMetrics.TEST_EXECUTION_TIME, report.getDurationMilliseconds());
136+
saveResults(inputFile, report);
137+
}
138+
139+
protected void saveResults(InputFile testFile, UnitTestClassReport report) {
140+
for (UnitTestResult unitTestResult : report.getResults()) {
141+
MutableTestPlan testPlan = perspectives.as(MutableTestPlan.class, testFile);
142+
if (testPlan != null) {
143+
testPlan.addTestCase(unitTestResult.getName())
144+
.setDurationInMs(Math.max(unitTestResult.getDurationMilliseconds(), 0))
145+
.setStatus(TestCase.Status.of(unitTestResult.getStatus()))
146+
.setMessage(unitTestResult.getMessage())
147+
.setStackTrace(unitTestResult.getStackTrace());
148+
}
149+
}
150+
}
151+
152+
@CheckForNull
153+
private InputFile getUnitTestResource(String className, UnitTestClassReport unitTestClassReport) {
154+
return TestFileFinders.getInstance().getUnitTestResource(fileSystem, className);
155+
}
156+
157+
private static <T extends Serializable> void saveMeasure(SensorContext context, InputFile inputFile, Metric<T> metric, T value) {
158+
context.<T>newMeasure().forMetric(metric).on(inputFile).withValue(value).save();
159+
}
160+
161+
}
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* backelite-sonar-swift-plugin - Enables analysis of Swift and Objective-C projects into SonarQube.
2+
* commons - Enables analysis of Swift and Objective-C projects into SonarQube.
33
* Copyright © 2015 Backelite (${email})
44
*
55
* This program is free software: you can redistribute it and/or modify
@@ -15,31 +15,35 @@
1515
* You should have received a copy of the GNU Lesser General Public License
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
18-
package com.backelite.sonarqube.swift.surefire;
18+
package com.backelite.sonarqube.commons.surefire;
1919

2020
import com.backelite.sonarqube.commons.Constants;
2121
import org.slf4j.Logger;
2222
import org.slf4j.LoggerFactory;
23-
import org.sonar.api.batch.fs.InputFile;
23+
import org.sonar.api.batch.fs.FileSystem;
2424
import org.sonar.api.batch.sensor.Sensor;
2525
import org.sonar.api.batch.sensor.SensorContext;
2626
import org.sonar.api.batch.sensor.SensorDescriptor;
27+
import org.sonar.api.component.ResourcePerspectives;
2728

2829
import java.io.File;
2930
import java.io.IOException;
3031
import java.nio.file.DirectoryStream;
3132
import java.nio.file.Files;
3233
import java.nio.file.Path;
33-
import java.nio.file.Paths;
3434

3535
public class SurefireSensor implements Sensor {
3636
private static final Logger LOGGER = LoggerFactory.getLogger(SurefireSensor.class);
3737
public static final String REPORT_PATH_KEY = Constants.PROPERTY_PREFIX + ".surefire.junit.reportsPath";
3838
public static final String DEFAULT_REPORT_PATH = "sonar-reports/";
3939

4040
private final SensorContext context;
41+
private final ResourcePerspectives perspectives;
42+
private final FileSystem fileSystem;
4143

42-
public SurefireSensor(SensorContext context) {
44+
public SurefireSensor(FileSystem fileSystem, ResourcePerspectives perspectives, SensorContext context) {
45+
this.fileSystem = fileSystem;
46+
this.perspectives = perspectives;
4347
this.context = context;
4448
}
4549

@@ -58,24 +62,18 @@ public void describe(SensorDescriptor descriptor) {
5862

5963
@Override
6064
public void execute(SensorContext context) {
61-
SurefireReportParser surefireParser = new SurefireReportParser(context);
65+
SurefireReportParser surefireParser = new SurefireReportParser(fileSystem, perspectives, context);
6266
String reportFileName = context.fileSystem().baseDir().getAbsolutePath() + "/"+ reportPath();
6367
File reportsDir = new File(reportFileName);
6468

6569
if (!reportsDir.isDirectory()) {
6670
LOGGER.warn("JUnit report directory not found at {}", reportsDir);
6771
return;
72+
} else {
73+
surefireParser.collect(reportsDir);
6874
}
6975

70-
try (DirectoryStream<Path> stream = Files.newDirectoryStream(reportsDir.toPath(), "{TEST}*.{xml}")) {
71-
for (Path p: stream) {
72-
LOGGER.info("Processing Surefire report {}", p.getFileName());
73-
surefireParser.parseReport(p.toFile());
74-
}
75-
} catch (IOException e) {
76-
LOGGER.error( "Error while finding test files.", e);
77-
}
7876

79-
surefireParser.save();
8077
}
78+
8179
}

0 commit comments

Comments
 (0)