/* * Sonar C# Plugin :: Gallio * Copyright (C) 2010 Jose Chillan, Alexandre Victoor and SonarSource * dev@sonar.codehaus.org * * 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 02 */ package org.sonar.plugins.csharp.gallio; import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.DependsUpon; import org.sonar.api.batch.SensorContext; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.measures.Metric; import org.sonar.api.resources.Project; import org.sonar.api.utils.ParsingUtils; import org.sonar.plugins.csharp.api.CSharpConfiguration; import org.sonar.plugins.csharp.api.MicrosoftWindowsEnvironment; import org.sonar.plugins.csharp.api.sensor.AbstractTestCSharpSensor; import org.sonar.plugins.csharp.gallio.results.execution.GallioResultParser; import org.sonar.plugins.csharp.gallio.results.execution.model.TestCaseDetail; import org.sonar.plugins.csharp.gallio.results.execution.model.TestStatus; import org.sonar.plugins.csharp.gallio.results.execution.model.UnitTestReport; /** * Gets the execution test report from Gallio and pushes data from it into sonar. */ @DependsUpon(GallioConstants.BARRIER_GALLIO_EXECUTED) public class TestReportSensor extends AbstractTestCSharpSensor { private static final Logger LOG = LoggerFactory.getLogger(TestReportSensor.class); private CSharpConfiguration configuration; private String executionMode; /** * Constructs a {@link TestReportSensor}. * * @param fileSystem * @param configuration * @param microsoftWindowsEnvironment */ public TestReportSensor(CSharpConfiguration configuration, MicrosoftWindowsEnvironment microsoftWindowsEnvironment) { super(microsoftWindowsEnvironment); this.configuration = configuration; this.executionMode = configuration.getString(GallioConstants.MODE, ""); } /** * {@inheritDoc} */ public boolean shouldExecuteOnProject(Project project) { boolean skipMode = GallioConstants.MODE_SKIP.equalsIgnoreCase(executionMode); if (skipMode) { LOG.info("Test report analysis won't execute as it is set to 'skip' mode."); } return super.shouldExecuteOnProject(project) && !skipMode; } @Override public void analyse(Project project, SensorContext context) { String reportPath = null; if (GallioConstants.MODE_REUSE_REPORT.equals(executionMode)) { reportPath = configuration.getString(GallioConstants.REPORTS_PATH_KEY, ""); LOG.info("Reusing Gallio report: " + reportPath); } else { if ( !getMicrosoftWindowsEnvironment().isTestExecutionDone()) { // This means that we are not in REUSE or SKIP mode, but for some reasons execution has not been done => skip the analysis LOG.info("Test report analysis won't execute as Gallio was not executed."); return; } reportPath = getMicrosoftWindowsEnvironment().getWorkingDirectory() + "/" + GallioConstants.GALLIO_REPORT_XML; } File reportFile = new File(getMicrosoftWindowsEnvironment().getCurrentSolution().getSolutionDir(), reportPath); if ( !reportFile.isFile()) { LOG.warn("No Gallio report file found for: " + reportFile.getAbsolutePath()); context.saveMeasure(CoreMetrics.TESTS, 0.0); return; } collect(project, reportFile, context); } private void collect(Project project, File report, SensorContext context) { GallioResultParser parser = new GallioResultParser(); Collection<UnitTestReport> reports = parser.parse(report); if (LOG.isDebugEnabled()) { LOG.debug("Found " + reports.size() + " test data"); } Set<File> csFilesAlreadyTreated = new HashSet<File>(); for (UnitTestReport testReport : reports) { File sourceFile = testReport.getSourceFile(); if (sourceFile != null && sourceFile.exists() && !csFilesAlreadyTreated.contains(sourceFile)) { if (LOG.isDebugEnabled()) { LOG.debug("Collecting test data for file " + sourceFile); } csFilesAlreadyTreated.add(sourceFile); int testsCount = testReport.getTests() - testReport.getSkipped(); org.sonar.api.resources.File testFile = fromIOFile(sourceFile, project); if (testFile != null) { saveFileMeasure(testFile, context, CoreMetrics.SKIPPED_TESTS, testReport.getSkipped()); saveFileMeasure(testFile, context, CoreMetrics.TESTS, testsCount); saveFileMeasure(testFile, context, CoreMetrics.TEST_ERRORS, testReport.getErrors()); saveFileMeasure(testFile, context, CoreMetrics.TEST_FAILURES, testReport.getFailures()); saveFileMeasure(testFile, context, CoreMetrics.TEST_EXECUTION_TIME, testReport.getTimeMS()); saveFileMeasure(testFile, context, TestMetrics.COUNT_ASSERTS, testReport.getAsserts()); int passedTests = testsCount - testReport.getErrors() - testReport.getFailures(); if (testsCount > 0) { double percentage = (float) passedTests * 100 / (float) testsCount; saveFileMeasure(testFile, context, CoreMetrics.TEST_SUCCESS_DENSITY, ParsingUtils.scaleValue(percentage)); } saveTestsDetails(testFile, context, testReport); } } else { LOG.error("Source file not found for test report " + testReport); } } } /** * Stores the test details in XML format. * * @param testFile * @param context * @param fileReport */ private void saveTestsDetails(org.sonar.api.resources.File testFile, SensorContext context, UnitTestReport fileReport) { StringBuilder testCaseDetails = new StringBuilder(256); testCaseDetails.append("<tests-details>"); List<TestCaseDetail> details = fileReport.getDetails(); for (TestCaseDetail detail : details) { testCaseDetails.append("<testcase status=\"").append(detail.getStatus().getSonarStatus()).append("\" time=\"") .append(detail.getTimeMillis()).append("\" name=\"").append(detail.getName()).append("\""); boolean isError = (detail.getStatus() == TestStatus.ERROR); if (isError || (detail.getStatus() == TestStatus.FAILED)) { testCaseDetails.append(">").append(isError ? "<error message=\"" : "<failure message=\"").append(detail.getFormatedErrorMessage()) .append("\">").append("<![CDATA[").append(detail.getFormatedStackTrace()).append("]]>") .append(isError ? "</error>" : "</failure>").append("</testcase>"); } else { testCaseDetails.append("/>"); } } testCaseDetails.append("</tests-details>"); context.saveMeasure(testFile, new Measure(CoreMetrics.TEST_DATA, testCaseDetails.toString())); LOG.debug("test detail : {}", testCaseDetails); } /** * Saves the measure the a test file. * * @param project * @param context * @param fileReport * @param metric * @param value */ private void saveFileMeasure(org.sonar.api.resources.File testFile, SensorContext context, Metric metric, double value) { if ( !Double.isNaN(value)) { context.saveMeasure(testFile, metric, value); } } }