/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact 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.surefire; import org.apache.commons.lang.StringUtils; import org.sonar.api.batch.BatchSide; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.component.ResourcePerspectives; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric; import org.sonar.api.test.MutableTestPlan; import org.sonar.api.test.TestCase; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.plugins.java.api.JavaResourceLocator; import org.sonar.plugins.surefire.data.UnitTestClassReport; import org.sonar.plugins.surefire.data.UnitTestIndex; import org.sonar.plugins.surefire.data.UnitTestResult; import org.sonar.squidbridge.api.AnalysisException; import javax.annotation.Nullable; import javax.xml.stream.XMLStreamException; import java.io.File; import java.io.Serializable; import java.util.Map; /** * @since 2.4 */ @BatchSide public class SurefireJavaParser { private static final Logger LOGGER = Loggers.get(SurefireJavaParser.class); private final ResourcePerspectives perspectives; private final JavaResourceLocator javaResourceLocator; public SurefireJavaParser(ResourcePerspectives perspectives, JavaResourceLocator javaResourceLocator) { this.perspectives = perspectives; this.javaResourceLocator = javaResourceLocator; } public void collect(SensorContext context, File reportsDir, boolean reportDirSetByUser) { File[] xmlFiles = getReports(reportsDir, reportDirSetByUser); if (xmlFiles.length > 0) { parseFiles(context, xmlFiles); } } private static File[] getReports(@Nullable File dir, boolean reportDirSetByUser) { if (dir == null) { return new File[0]; } else if (!dir.isDirectory()) { if(reportDirSetByUser) { LOGGER.error("Reports path not found or is not a directory: " + dir.getAbsolutePath()); } return new File[0]; } File[] unitTestResultFiles = findXMLFilesStartingWith(dir, "TEST-"); if (unitTestResultFiles.length == 0) { // maybe there's only a test suite result file unitTestResultFiles = findXMLFilesStartingWith(dir, "TESTS-"); } if(unitTestResultFiles.length == 0) { LOGGER.warn("Reports path contains no files matching TEST-.*.xml : "+dir.getAbsolutePath()); } return unitTestResultFiles; } private static File[] findXMLFilesStartingWith(File dir, final String fileNameStart) { return dir.listFiles((parentDir, name) -> name.startsWith(fileNameStart) && name.endsWith(".xml")); } private void parseFiles(SensorContext context, File[] reports) { UnitTestIndex index = new UnitTestIndex(); parseFiles(reports, index); sanitize(index); save(index, context); } private static void parseFiles(File[] reports, UnitTestIndex index) { StaxParser parser = new StaxParser(index); for (File report : reports) { try { parser.parse(report); } catch (XMLStreamException e) { throw new AnalysisException("Fail to parse the Surefire report: " + report, e); } } } private static void sanitize(UnitTestIndex index) { for (String classname : index.getClassnames()) { if (StringUtils.contains(classname, "$")) { // Surefire reports classes whereas sonar supports files String parentClassName = StringUtils.substringBefore(classname, "$"); index.merge(classname, parentClassName); } } } private void save(UnitTestIndex index, SensorContext context) { long negativeTimeTestNumber = 0; for (Map.Entry<String, UnitTestClassReport> entry : index.getIndexByClassname().entrySet()) { UnitTestClassReport report = entry.getValue(); if (report.getTests() > 0) { negativeTimeTestNumber += report.getNegativeTimeTestNumber(); InputFile resource = getUnitTestResource(entry.getKey()); if (resource != null) { save(report, resource, context); } else { LOGGER.warn("Resource not found: {}", entry.getKey()); } } } if (negativeTimeTestNumber > 0) { LOGGER.warn("There is {} test(s) reported with negative time by surefire, total duration may not be accurate.", negativeTimeTestNumber); } } private void save(UnitTestClassReport report, InputFile inputFile, SensorContext context) { int testsCount = report.getTests() - report.getSkipped(); saveMeasure(context, inputFile, CoreMetrics.SKIPPED_TESTS, report.getSkipped()); saveMeasure(context, inputFile, CoreMetrics.TESTS, testsCount); saveMeasure(context, inputFile, CoreMetrics.TEST_ERRORS, report.getErrors()); saveMeasure(context, inputFile, CoreMetrics.TEST_FAILURES, report.getFailures()); saveMeasure(context, inputFile, CoreMetrics.TEST_EXECUTION_TIME, report.getDurationMilliseconds()); saveResults(inputFile, report); } protected void saveResults(InputFile testFile, UnitTestClassReport report) { for (UnitTestResult unitTestResult : report.getResults()) { MutableTestPlan testPlan = perspectives.as(MutableTestPlan.class, testFile); if (testPlan != null) { testPlan.addTestCase(unitTestResult.getName()) .setDurationInMs(Math.max(unitTestResult.getDurationMilliseconds(), 0)) .setStatus(TestCase.Status.of(unitTestResult.getStatus())) .setMessage(unitTestResult.getMessage()) .setStackTrace(unitTestResult.getStackTrace()); } } } protected InputFile getUnitTestResource(String classKey) { return javaResourceLocator.findResourceByClassName(classKey); } private static <T extends Serializable> void saveMeasure(SensorContext context, InputFile inputFile, Metric<T> metric, T value) { context.<T>newMeasure().forMetric(metric).on(inputFile).withValue(value).save(); } }