org.sonar.plugins.python.xunit.PythonXUnitSensor Maven / Gradle / Ivy
The newest version!
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plugins.python.xunit;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.measure.Metric;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.config.Configuration;
import org.sonar.api.measures.CoreMetrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.plugins.python.PythonReportSensor;
import org.sonar.plugins.python.parser.StaxParser;
import org.sonar.plugins.python.warnings.AnalysisWarningsWrapper;
public class PythonXUnitSensor extends PythonReportSensor {
private static final Logger LOG = LoggerFactory.getLogger(PythonXUnitSensor.class);
public static final String REPORT_PATH_KEY = "sonar.python.xunit.reportPath";
public static final String DEFAULT_REPORT_PATH = "xunit-reports/xunit-result-*.xml";
public static final String SKIP_DETAILS = "sonar.python.xunit.skipDetails";
private final FileSystem fileSystem;
public PythonXUnitSensor(Configuration conf, FileSystem fileSystem, AnalysisWarningsWrapper analysisWarnings) {
super(conf, analysisWarnings, "XUnit");
this.fileSystem = fileSystem;
}
@Override
protected String reportPathKey() {
return REPORT_PATH_KEY;
}
@Override
protected String defaultReportPath() {
return DEFAULT_REPORT_PATH;
}
@Override
protected void processReports(final SensorContext context, List reports) throws XMLStreamException {
if (conf.getBoolean(SKIP_DETAILS).orElse(Boolean.FALSE)) {
simpleMode(context, reports);
} else {
detailedMode(context, reports);
}
}
private static void simpleMode(final SensorContext context, List reports) throws XMLStreamException {
TestSuiteParser parserHandler = new TestSuiteParser();
StaxParser parser = new StaxParser(parserHandler);
for (File report : reports) {
parser.parse(report);
}
TestResult total = new TestResult();
parserHandler.getParsedReports().forEach(testSuite -> testSuite.getTestCases().forEach(total::addTestCase));
if (total.getTests() > 0) {
InputComponent module = context.module();
saveMeasure(context, module, CoreMetrics.TESTS, total.getExecutedTests());
saveMeasure(context, module, CoreMetrics.SKIPPED_TESTS, total.getSkipped());
saveMeasure(context, module, CoreMetrics.TEST_ERRORS, total.getErrors());
saveMeasure(context, module, CoreMetrics.TEST_FAILURES, total.getFailures());
saveMeasure(context, module, CoreMetrics.TEST_EXECUTION_TIME, total.getTime());
}
}
private void detailedMode(final SensorContext context, List reports) throws XMLStreamException {
for (File report : reports) {
TestSuiteParser parserHandler = new TestSuiteParser();
StaxParser parser = new StaxParser(parserHandler);
parser.parse(report);
LOG.info("Processing report '{}'", report);
processReportDetailed(context, parserHandler.getParsedReports());
}
}
private void processReportDetailed(SensorContext context, Collection parsedReports) {
Map locatedResources = lookupResources(parsedReports);
for (Map.Entry entry : locatedResources.entrySet()) {
InputFile inputFile = entry.getKey();
TestResult fileTestResult = entry.getValue();
LOG.debug("Saving test execution measures for '{}'", inputFile);
saveMeasure(context, inputFile, CoreMetrics.SKIPPED_TESTS, fileTestResult.getSkipped());
saveMeasure(context, inputFile, CoreMetrics.TESTS, fileTestResult.getExecutedTests());
saveMeasure(context, inputFile, CoreMetrics.TEST_ERRORS, fileTestResult.getErrors());
saveMeasure(context, inputFile, CoreMetrics.TEST_FAILURES, fileTestResult.getFailures());
saveMeasure(context, inputFile, CoreMetrics.TEST_EXECUTION_TIME, fileTestResult.getTime());
}
}
@CheckForNull
private InputFile findResource(TestCase testCase, String fileKey) {
InputFile unitTestFile = null;
if (testCase.getFile() != null) {
unitTestFile = getSonarTestFile(new File(testCase.getFile()));
}
if (unitTestFile == null) {
String testClassname = testCase.getTestClassname();
String key = testClassname != null ? testClassname : fileKey;
return findResourceUsingNoseTestsStrategy(key);
}
return unitTestFile;
}
private InputFile findResourceUsingNoseTestsStrategy(String fileKey) {
// a) check assuming the key doesnt contain the class name
String candidateKey = StringUtils.replace(fileKey, ".", "/") + ".py";
InputFile unitTestFile = getSonarTestFile(new File(candidateKey));
if (unitTestFile == null) {
// b) check assuming the key *does* contain the class name
String candidateKey2 = StringUtils.replace(StringUtils.substringBeforeLast(fileKey, "."), ".", "/") + ".py";
if ( !(candidateKey2.equals(candidateKey))) {
unitTestFile = getSonarTestFile(new File(candidateKey2));
}
}
return unitTestFile;
}
private Map lookupResources(Collection testReports) {
Map testResultsByFile = new HashMap<>();
for (TestSuite testSuite : testReports) {
testSuite.getTestCases().forEach(testCase -> {
String testClassname = testCase.getTestClassname();
LOG.debug("Trying to find a SonarQube resource for test case '{}'", testClassname);
InputFile inputFile = findResource(testCase, testSuite.getKey());
if (inputFile != null) {
LOG.debug("The resource was found '{}'", inputFile);
testResultsByFile.computeIfAbsent(inputFile, k -> new TestResult()).addTestCase(testCase);
} else {
LOG.warn("The resource for '{}' is not found, drilling down to the details of this test won't be possible", testClassname);
}
});
}
return testResultsByFile;
}
@CheckForNull
private InputFile getSonarTestFile(File file) {
LOG.debug("Using the key '{}' to lookup the resource in SonarQube", file.getPath());
var predicate = file.isAbsolute() ? fileSystem.predicates().hasAbsolutePath(file.getAbsolutePath())
: fileSystem.predicates().hasRelativePath(file.getPath());
return fileSystem.inputFile(predicate);
}
private static void saveMeasure(SensorContext context, InputComponent component, Metric metric, int value) {
context.newMeasure()
.on(component)
.forMetric(metric)
.withValue(value)
.save();
}
private static void saveMeasure(SensorContext context, InputComponent component, Metric metric, long value) {
context.newMeasure()
.on(component)
.forMetric(metric)
.withValue(value)
.save();
}
}