diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2016-10-19 16:50:16 +0200 |
---|---|---|
committer | Julien HENRY <henryju@yahoo.fr> | 2016-10-21 13:17:30 +0200 |
commit | 9ddf6fda885838c8a3a12b5ade1cf9a32df95763 (patch) | |
tree | ed055e86006645160a9122496ae0c7a2ee3c997a /sonar-scanner-engine | |
parent | f8703a4155668ee958bd38d59776c1415fce0f82 (diff) | |
download | sonarqube-9ddf6fda885838c8a3a12b5ade1cf9a32df95763.tar.gz sonarqube-9ddf6fda885838c8a3a12b5ade1cf9a32df95763.zip |
SONAR-8310, SONAR-8314 Make generic coverage plugin a core feature
Diffstat (limited to 'sonar-scanner-engine')
34 files changed, 1327 insertions, 106 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java index 51dc68f024c..d619a516cac 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java @@ -27,6 +27,8 @@ import org.sonar.core.component.DefaultResourceTypes; import org.sonar.core.config.CorePropertyDefinitions; import org.sonar.core.issue.tracking.Tracker; import org.sonar.scanner.cpd.CpdComponents; +import org.sonar.scanner.genericcoverage.GenericCoverageSensor; +import org.sonar.scanner.genericcoverage.GenericTestExecutionSensor; import org.sonar.scanner.issue.tracking.ServerIssueFromWs; import org.sonar.scanner.issue.tracking.TrackedIssue; import org.sonar.scanner.scan.report.ConsoleReport; @@ -71,6 +73,13 @@ public class BatchComponents { // CPD components.addAll(CpdComponents.all()); + + // Generic coverage + components.add(GenericCoverageSensor.class); + components.addAll(GenericCoverageSensor.properties()); + components.add(GenericTestExecutionSensor.class); + components.addAll(GenericTestExecutionSensor.properties()); + } else { // Issues tracking components.add(new Tracker<TrackedIssue, ServerIssueFromWs>()); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilder.java index fd1632b9172..255968e45f2 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilder.java @@ -21,8 +21,8 @@ package org.sonar.scanner.deprecated.perspectives; import javax.annotation.CheckForNull; import org.sonar.api.batch.ScannerSide; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.component.Perspective; -import org.sonar.scanner.index.BatchComponent; @ScannerSide public abstract class PerspectiveBuilder<T extends Perspective> { @@ -38,5 +38,5 @@ public abstract class PerspectiveBuilder<T extends Perspective> { } @CheckForNull - public abstract T loadPerspective(Class<T> perspectiveClass, BatchComponent component); + public abstract T loadPerspective(Class<T> perspectiveClass, InputComponent component); } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/BatchPerspectives.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/ScannerPerspectives.java index 1e89551a32f..1903ef38b12 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/BatchPerspectives.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/perspectives/ScannerPerspectives.java @@ -29,13 +29,13 @@ import org.sonar.api.resources.Resource; import org.sonar.scanner.index.BatchComponentCache; import org.sonar.scanner.index.DefaultIndex; -public class BatchPerspectives implements ResourcePerspectives { +public class ScannerPerspectives implements ResourcePerspectives { private final Map<Class<?>, PerspectiveBuilder<?>> builders = Maps.newHashMap(); private final DefaultIndex resourceIndex; private final BatchComponentCache componentCache; - public BatchPerspectives(PerspectiveBuilder[] builders, DefaultIndex resourceIndex, BatchComponentCache componentCache) { + public ScannerPerspectives(PerspectiveBuilder[] builders, DefaultIndex resourceIndex, BatchComponentCache componentCache) { this.resourceIndex = resourceIndex; this.componentCache = componentCache; for (PerspectiveBuilder builder : builders) { @@ -52,7 +52,7 @@ public class BatchPerspectives implements ResourcePerspectives { } if (indexedResource != null) { PerspectiveBuilder<P> builder = builderFor(perspectiveClass); - return builder.loadPerspective(perspectiveClass, componentCache.get(indexedResource)); + return builder.loadPerspective(perspectiveClass, componentCache.get(indexedResource).inputComponent()); } return null; } @@ -60,7 +60,7 @@ public class BatchPerspectives implements ResourcePerspectives { @Override public <P extends Perspective> P as(Class<P> perspectiveClass, InputPath inputPath) { PerspectiveBuilder<P> builder = builderFor(perspectiveClass); - return builder.loadPerspective(perspectiveClass, componentCache.get(inputPath)); + return builder.loadPerspective(perspectiveClass, inputPath); } private <T extends Perspective> PerspectiveBuilder<T> builderFor(Class<T> clazz) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestPlanBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestPlanBuilder.java index 94fbfd0ef18..580b0b10944 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestPlanBuilder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestPlanBuilder.java @@ -22,11 +22,11 @@ package org.sonar.scanner.deprecated.test; import java.util.HashMap; import java.util.Map; import javax.annotation.CheckForNull; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.test.MutableTestPlan; import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; public class TestPlanBuilder extends PerspectiveBuilder<MutableTestPlan> { @@ -38,9 +38,9 @@ public class TestPlanBuilder extends PerspectiveBuilder<MutableTestPlan> { @CheckForNull @Override - public MutableTestPlan loadPerspective(Class<MutableTestPlan> perspectiveClass, BatchComponent component) { + public MutableTestPlan loadPerspective(Class<MutableTestPlan> perspectiveClass, InputComponent component) { if (component.isFile()) { - InputFile inputFile = (InputFile) component.inputComponent(); + InputFile inputFile = (InputFile) component; if (inputFile.type() == Type.TEST) { if (!testPlanByFile.containsKey(inputFile)) { testPlanByFile.put(inputFile, new DefaultTestPlan()); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestableBuilder.java index 4c9c3de46fb..a03c3514f4f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestableBuilder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/deprecated/test/TestableBuilder.java @@ -20,12 +20,12 @@ package org.sonar.scanner.deprecated.test; import javax.annotation.CheckForNull; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.test.MutableTestable; import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; public class TestableBuilder extends PerspectiveBuilder<MutableTestable> { @@ -35,9 +35,9 @@ public class TestableBuilder extends PerspectiveBuilder<MutableTestable> { @CheckForNull @Override - public MutableTestable loadPerspective(Class<MutableTestable> perspectiveClass, BatchComponent component) { + public MutableTestable loadPerspective(Class<MutableTestable> perspectiveClass, InputComponent component) { if (component.isFile()) { - InputFile inputFile = (InputFile) component.inputComponent(); + InputFile inputFile = (InputFile) component; if (inputFile.type() == Type.MAIN) { return new DefaultTestable((DefaultInputFile) inputFile); } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParser.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParser.java new file mode 100644 index 00000000000..2ccd7a0abaa --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParser.java @@ -0,0 +1,194 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import com.google.common.base.Preconditions; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.xml.stream.XMLStreamException; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.utils.StaxParser; + +public class GenericCoverageReportParser { + + private static final String LINE_NUMBER_ATTR = "lineNumber"; + private static final String COVERED_ATTR = "covered"; + private static final String BRANCHES_TO_COVER_ATTR = "branchesToCover"; + private static final String COVERED_BRANCHES_ATTR = "coveredBranches"; + + private static final int MAX_STORED_UNKNOWN_FILE_PATHS = 5; + + private int numberOfUnknownFiles; + private final List<String> firstUnknownFiles = new ArrayList<>(); + private final Set<String> matchedFileKeys = new HashSet<>(); + + public void parse(java.io.File reportFile, SensorContext context) { + try (InputStream inputStream = new FileInputStream(reportFile)) { + parse(inputStream, context); + } catch (Exception e) { + throw new IllegalStateException("Error during parsing of coverage report " + reportFile, e); + } + } + + void parse(InputStream inputStream, SensorContext context) throws XMLStreamException { + new StaxParser(rootCursor -> { + rootCursor.advance(); + parseRootNode(rootCursor, context); + }).parse(inputStream); + } + + private void parseRootNode(SMHierarchicCursor rootCursor, SensorContext context) throws XMLStreamException { + checkElementName(rootCursor, "coverage"); + String version = rootCursor.getAttrValue("version"); + if (!"1".equals(version)) { + throw new IllegalStateException("Unknown report version: " + version + ". This parser only handles version 1."); + } + parseFiles(rootCursor.childElementCursor(), context); + } + + private void parseFiles(SMInputCursor fileCursor, SensorContext context) throws XMLStreamException { + while (fileCursor.getNext() != null) { + checkElementName(fileCursor, "file"); + String filePath = mandatoryAttribute(fileCursor, "path"); + InputFile inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath)); + if (inputFile == null) { + numberOfUnknownFiles++; + if (numberOfUnknownFiles <= MAX_STORED_UNKNOWN_FILE_PATHS) { + firstUnknownFiles.add(filePath); + } + continue; + } + Preconditions.checkState( + inputFile.language() != null, + "Line %s of report refers to a file with an unknown language: %s", + fileCursor.getCursorLocation().getLineNumber(), + filePath); + matchedFileKeys.add(inputFile.absolutePath()); + + NewCoverage newCoverage = context.newCoverage().onFile(inputFile); + SMInputCursor lineToCoverCursor = fileCursor.childElementCursor(); + while (lineToCoverCursor.getNext() != null) { + parseLineToCover(lineToCoverCursor, newCoverage); + } + newCoverage.save(); + } + } + + private static void parseLineToCover(SMInputCursor cursor, NewCoverage newCoverage) + throws XMLStreamException { + checkElementName(cursor, "lineToCover"); + String lineNumberAsString = mandatoryAttribute(cursor, LINE_NUMBER_ATTR); + int lineNumber = intValue(lineNumberAsString, cursor, LINE_NUMBER_ATTR, 1); + + boolean covered = getCoveredValue(cursor); + newCoverage.lineHits(lineNumber, covered ? 1 : 0); + + String branchesToCoverAsString = cursor.getAttrValue(BRANCHES_TO_COVER_ATTR); + if (branchesToCoverAsString != null) { + int branchesToCover = intValue(branchesToCoverAsString, cursor, BRANCHES_TO_COVER_ATTR, 0); + String coveredBranchesAsString = cursor.getAttrValue(COVERED_BRANCHES_ATTR); + int coveredBranches = 0; + if (coveredBranchesAsString != null) { + coveredBranches = intValue(coveredBranchesAsString, cursor, COVERED_BRANCHES_ATTR, 0); + if (coveredBranches > branchesToCover) { + throw new IllegalStateException("\"coveredBranches\" should not be greater than \"branchesToCover\" on line " + cursor.getCursorLocation().getLineNumber()); + } + } + newCoverage.conditions(lineNumber, branchesToCover, coveredBranches); + } + } + + private static boolean getCoveredValue(SMInputCursor cursor) throws XMLStreamException { + String coveredAsString = mandatoryAttribute(cursor, COVERED_ATTR); + if (!"true".equalsIgnoreCase(coveredAsString) && !"false".equalsIgnoreCase(coveredAsString)) { + throw new IllegalStateException(expectedMessage("boolean value", COVERED_ATTR, coveredAsString, cursor.getCursorLocation().getLineNumber())); + } + return Boolean.parseBoolean(coveredAsString); + } + + static void checkElementName(SMInputCursor cursor, String expectedName) throws XMLStreamException { + String elementName = cursor.getLocalName(); + if (!expectedName.equals(elementName)) { + throw new IllegalStateException("Unknown XML node, expected \"" + expectedName + "\" but got \"" + elementName + "\" at line " + cursor.getCursorLocation().getLineNumber()); + } + } + + static String mandatoryAttribute(SMInputCursor cursor, String attributeName) throws XMLStreamException { + String attributeValue = cursor.getAttrValue(attributeName); + if (attributeValue == null) { + throw new IllegalStateException( + "Missing attribute \"" + attributeName + "\" in element \"" + cursor.getLocalName() + "\" at line " + cursor.getCursorLocation().getLineNumber()); + } + return attributeValue; + } + + static int intValue(String stringValue, SMInputCursor cursor, String attributeName, int minimum) throws XMLStreamException { + int intValue; + try { + intValue = Integer.valueOf(stringValue); + } catch (NumberFormatException e) { + throw new IllegalStateException(expectedMessage("integer value", attributeName, stringValue, cursor.getCursorLocation().getLineNumber()), e); + } + if (intValue < minimum) { + throw new IllegalStateException("Value of attribute \"" + attributeName + "\" at line " + cursor.getCursorLocation().getLineNumber() + " is \"" + intValue + + "\" but it should be greater than or equal to " + minimum); + } + return intValue; + } + + static long longValue(String stringValue, SMInputCursor cursor, String attributeName, long minimum) throws XMLStreamException { + long longValue; + try { + longValue = Long.valueOf(stringValue); + } catch (NumberFormatException e) { + throw new IllegalStateException(expectedMessage("long value", attributeName, stringValue, cursor.getCursorLocation().getLineNumber()), e); + } + if (longValue < minimum) { + throw new IllegalStateException("Value of attribute \"" + attributeName + "\" at line " + cursor.getCursorLocation().getLineNumber() + " is \"" + longValue + + "\" but it should be greater than or equal to " + minimum); + } + return longValue; + } + + private static String expectedMessage(String expected, String attributeName, String stringValue, int line) { + return "Expected " + expected + " for attribute \"" + attributeName + "\" at line " + line + " but got \"" + stringValue + "\""; + } + + public int numberOfMatchedFiles() { + return matchedFileKeys.size(); + } + + public int numberOfUnknownFiles() { + return numberOfUnknownFiles; + } + + public List<String> firstUnknownFiles() { + return firstUnknownFiles; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageSensor.java new file mode 100644 index 00000000000..4a4c6012c4f --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericCoverageSensor.java @@ -0,0 +1,126 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.api.batch.Initializer; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static org.sonar.api.CoreProperties.CATEGORY_CODE_COVERAGE; + +public class GenericCoverageSensor extends Initializer implements Sensor { + + private static final Logger LOG = Loggers.get(GenericCoverageSensor.class); + + private static final String REPORT_PATH_PROPERTY_KEY = "sonar.coverageReportPaths"; + /** + * @deprecated since 6.2 + */ + @Deprecated + private static final String OLD_REPORT_PATH_PROPERTY_KEY = "sonar.genericcoverage.reportPath"; + /** + * @deprecated since 6.2 + */ + @Deprecated + private static final String OLD_COVERAGE_REPORT_PATHS_PROPERTY_KEY = "sonar.genericcoverage.reportPaths"; + /** + * @deprecated since 6.2 + */ + @Deprecated + private static final String OLD_IT_COVERAGE_REPORT_PATHS_PROPERTY_KEY = "sonar.genericcoverage.itReportPaths"; + /** + * @deprecated since 6.2 + */ + @Deprecated + private static final String OLD_OVERALL_COVERAGE_REPORT_PATHS_PROPERTY_KEY = "sonar.genericcoverage.overallReportPaths"; + + private final Settings settings; + + public GenericCoverageSensor(Settings settings) { + this.settings = settings; + } + + public static ImmutableList<PropertyDefinition> properties() { + return ImmutableList.of( + + PropertyDefinition.builder(REPORT_PATH_PROPERTY_KEY) + .name("Coverage report paths") + .description("List of comma-separated paths (absolute or relative) containing coverage report.") + .category(CATEGORY_CODE_COVERAGE) + .onQualifiers(Qualifiers.PROJECT) + .deprecatedKey(OLD_COVERAGE_REPORT_PATHS_PROPERTY_KEY) + .build()); + + } + + @Override + public void execute() { + Set<String> reportPaths = new LinkedHashSet<>(); + reportPaths.addAll(Arrays.asList(settings.getStringArray(REPORT_PATH_PROPERTY_KEY))); + loadDeprecated(reportPaths, OLD_REPORT_PATH_PROPERTY_KEY); + loadDeprecated(reportPaths, OLD_IT_COVERAGE_REPORT_PATHS_PROPERTY_KEY); + loadDeprecated(reportPaths, OLD_OVERALL_COVERAGE_REPORT_PATHS_PROPERTY_KEY); + if (!reportPaths.isEmpty()) { + settings.setProperty(REPORT_PATH_PROPERTY_KEY, reportPaths.stream().collect(Collectors.joining(","))); + } + } + + private void loadDeprecated(Set<String> reportPaths, String propertyKey) { + if (settings.hasKey(propertyKey)) { + LOG.warn("Property '{}' is deprecated. Please use '{}' instead.", propertyKey, REPORT_PATH_PROPERTY_KEY); + reportPaths.addAll(Arrays.asList(settings.getStringArray(propertyKey))); + } + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Generic Coverage Report") + .requireProperty(REPORT_PATH_PROPERTY_KEY); + } + + @Override + public void execute(SensorContext context) { + for (String reportPath : settings.getStringArray(REPORT_PATH_PROPERTY_KEY)) { + File reportFile = context.fileSystem().resolvePath(reportPath); + LOG.info("Parsing {}", reportFile); + GenericCoverageReportParser parser = new GenericCoverageReportParser(); + parser.parse(reportFile, context); + LOG.info("Imported coverage data for {} files", parser.numberOfMatchedFiles()); + int numberOfUnknownFiles = parser.numberOfUnknownFiles(); + if (numberOfUnknownFiles > 0) { + LOG.info("Coverage data ignored for " + numberOfUnknownFiles + " unknown files, including:\n" + parser.firstUnknownFiles().stream().collect(Collectors.joining("\n"))); + } + } + + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParser.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParser.java new file mode 100644 index 00000000000..2762ec37506 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParser.java @@ -0,0 +1,159 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import com.google.common.base.Preconditions; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.xml.stream.XMLStreamException; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.test.MutableTestCase; +import org.sonar.api.test.MutableTestPlan; +import org.sonar.api.test.TestCase; +import org.sonar.api.utils.StaxParser; +import org.sonar.scanner.deprecated.test.TestPlanBuilder; + +import static org.sonar.scanner.genericcoverage.GenericCoverageReportParser.checkElementName; +import static org.sonar.scanner.genericcoverage.GenericCoverageReportParser.longValue; +import static org.sonar.scanner.genericcoverage.GenericCoverageReportParser.mandatoryAttribute; + +public class GenericTestExecutionReportParser { + + private static final String NAME_ATTR = "name"; + private static final String DURATION_ATTR = "duration"; + private static final String MESSAGE_ATTR = "message"; + public static final String OK = "ok"; + public static final String ERROR = "error"; + public static final String FAILURE = "failure"; + public static final String SKIPPED = "skipped"; + + private static final int MAX_STORED_UNKNOWN_FILE_PATHS = 5; + + private final TestPlanBuilder testPlanBuilder; + + private int numberOfUnknownFiles; + private final List<String> firstUnknownFiles = new ArrayList<>(); + private final Set<String> matchedFileKeys = new HashSet<>(); + + public GenericTestExecutionReportParser(TestPlanBuilder testPlanBuilder) { + this.testPlanBuilder = testPlanBuilder; + } + + public void parse(java.io.File reportFile, SensorContext context) { + try (InputStream inputStream = new FileInputStream(reportFile)) { + parse(inputStream, context); + } catch (Exception e) { + throw new IllegalStateException("Error during parsing of test execution report " + reportFile, e); + } + } + + public void parse(InputStream inputStream, SensorContext context) throws XMLStreamException { + new StaxParser(rootCursor -> { + rootCursor.advance(); + parseRootNode(rootCursor, context); + }).parse(inputStream); + } + + private void parseRootNode(SMHierarchicCursor rootCursor, SensorContext context) throws XMLStreamException { + checkElementName(rootCursor, "unitTest"); + String version = rootCursor.getAttrValue("version"); + if (!"1".equals(version)) { + throw new IllegalStateException("Unknown report version: " + version + ". This parser only handles version 1."); + } + parseFiles(rootCursor.childElementCursor(), context); + } + + private void parseFiles(SMInputCursor fileCursor, SensorContext context) throws XMLStreamException { + while (fileCursor.getNext() != null) { + checkElementName(fileCursor, "file"); + String filePath = mandatoryAttribute(fileCursor, "path"); + InputFile inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath)); + if (inputFile == null) { + numberOfUnknownFiles++; + if (numberOfUnknownFiles <= MAX_STORED_UNKNOWN_FILE_PATHS) { + firstUnknownFiles.add(filePath); + } + continue; + } + Preconditions.checkState( + inputFile.language() != null, + "Line %s of report refers to a file with an unknown language: %s", + fileCursor.getCursorLocation().getLineNumber(), + filePath); + Preconditions.checkState( + inputFile.type() != InputFile.Type.MAIN, + "Line %s of report refers to a file which is not configured as a test file: %s", + fileCursor.getCursorLocation().getLineNumber(), + filePath); + matchedFileKeys.add(inputFile.absolutePath()); + + MutableTestPlan testPlan = testPlanBuilder.loadPerspective(MutableTestPlan.class, inputFile); + SMInputCursor testCaseCursor = fileCursor.childElementCursor(); + while (testCaseCursor.getNext() != null) { + parseTestCase(testCaseCursor, testPlan); + } + } + } + + private void parseTestCase(SMInputCursor cursor, MutableTestPlan testPlan) throws XMLStreamException { + checkElementName(cursor, "testCase"); + MutableTestCase testCase = testPlan.addTestCase(mandatoryAttribute(cursor, NAME_ATTR)); + TestCase.Status status = TestCase.Status.OK; + testCase.setDurationInMs(longValue(mandatoryAttribute(cursor, DURATION_ATTR), cursor, DURATION_ATTR, 0)); + + SMInputCursor child = cursor.descendantElementCursor(); + if (child.getNext() != null) { + String elementName = child.getLocalName(); + if (SKIPPED.equals(elementName)) { + status = TestCase.Status.SKIPPED; + } else if (FAILURE.equals(elementName)) { + status = TestCase.Status.FAILURE; + } else if (ERROR.equals(elementName)) { + status = TestCase.Status.ERROR; + } + testCase.setStatus(status); + if (TestCase.Status.OK != status) { + testCase.setMessage(mandatoryAttribute(child, MESSAGE_ATTR)); + testCase.setStackTrace(child.collectDescendantText()); + } + } + + } + + public int numberOfMatchedFiles() { + return matchedFileKeys.size(); + } + + public int numberOfUnknownFiles() { + return numberOfUnknownFiles; + } + + public List<String> firstUnknownFiles() { + return firstUnknownFiles; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionSensor.java new file mode 100644 index 00000000000..a4db6902285 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/GenericTestExecutionSensor.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.util.stream.Collectors; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.scanner.deprecated.test.TestPlanBuilder; + +import static org.sonar.api.CoreProperties.CATEGORY_CODE_COVERAGE; + +public class GenericTestExecutionSensor implements Sensor { + + private static final Logger LOG = Loggers.get(GenericTestExecutionSensor.class); + + private static final String REPORT_PATHS_PROPERTY_KEY = "sonar.testExecutionReportPaths"; + /** + * @deprecated since 6.2 + */ + @Deprecated + private static final String OLD_UNIT_TEST_REPORT_PATHS_PROPERTY_KEY = "sonar.genericcoverage.unitTestReportPaths"; + + private final TestPlanBuilder testPlanBuilder; + + public GenericTestExecutionSensor(TestPlanBuilder testPlanBuilder) { + this.testPlanBuilder = testPlanBuilder; + } + + public static ImmutableList<PropertyDefinition> properties() { + return ImmutableList.of( + + PropertyDefinition.builder(REPORT_PATHS_PROPERTY_KEY) + .name("Unit tests results report paths") + .description("List of comma-separated paths (absolute or relative) containing unit tests results report.") + .category(CATEGORY_CODE_COVERAGE) + .onQualifiers(Qualifiers.PROJECT) + .deprecatedKey(OLD_UNIT_TEST_REPORT_PATHS_PROPERTY_KEY) + .build()); + + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Generic Tests Excution Report") + .requireProperty(REPORT_PATHS_PROPERTY_KEY); + } + + @Override + public void execute(SensorContext context) { + for (String reportPath : context.settings().getStringArray(REPORT_PATHS_PROPERTY_KEY)) { + File reportFile = context.fileSystem().resolvePath(reportPath); + LOG.info("Parsing {}", reportFile); + GenericTestExecutionReportParser parser = new GenericTestExecutionReportParser(testPlanBuilder); + parser.parse(reportFile, context); + LOG.info("Imported coverage data for {} files", parser.numberOfMatchedFiles()); + int numberOfUnknownFiles = parser.numberOfUnknownFiles(); + if (numberOfUnknownFiles > 0) { + LOG.info("Coverage data ignored for " + numberOfUnknownFiles + " unknown files, including:\n" + parser.firstUnknownFiles().stream().collect(Collectors.joining("\n"))); + } + } + + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/package-info.java new file mode 100644 index 00000000000..46f0286f222 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/genericcoverage/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-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. + */ +@ParametersAreNonnullByDefault +package org.sonar.scanner.genericcoverage; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssuable.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssuable.java index 2a449596484..28c2502430a 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssuable.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssuable.java @@ -21,21 +21,21 @@ package org.sonar.scanner.issue; import java.util.Collections; import java.util.List; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; import org.sonar.api.issue.Issuable; import org.sonar.api.issue.Issue; -import org.sonar.scanner.index.BatchComponent; /** * @since 3.6 */ public class DefaultIssuable implements Issuable { - private final BatchComponent component; + private final InputComponent component; private final SensorContext sensorContext; - DefaultIssuable(BatchComponent component, SensorContext sensorContext) { + DefaultIssuable(InputComponent component, SensorContext sensorContext) { this.component = component; this.sensorContext = sensorContext; } @@ -43,7 +43,7 @@ public class DefaultIssuable implements Issuable { @Override public IssueBuilder newIssueBuilder() { DefaultIssue newIssue = (DefaultIssue) sensorContext.newIssue(); - return new DeprecatedIssueBuilderWrapper(component.inputComponent(), newIssue); + return new DeprecatedIssueBuilderWrapper(component, newIssue); } @Override diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuableFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuableFactory.java index 85177696c53..47f2ea3bcd3 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuableFactory.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuableFactory.java @@ -19,10 +19,10 @@ */ package org.sonar.scanner.issue; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.issue.Issuable; import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; import org.sonar.scanner.sensor.DefaultSensorContext; /** @@ -39,7 +39,7 @@ public class IssuableFactory extends PerspectiveBuilder<Issuable> { } @Override - public Issuable loadPerspective(Class<Issuable> perspectiveClass, BatchComponent component) { + public Issuable loadPerspective(Class<Issuable> perspectiveClass, InputComponent component) { return new DefaultIssuable(component, sensorContext); } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MeasuresPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MeasuresPublisher.java index 8eb6fe00251..4686aaea47a 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MeasuresPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MeasuresPublisher.java @@ -20,14 +20,21 @@ package org.sonar.scanner.report; import com.google.common.base.Function; +import com.google.common.collect.Iterables; import java.io.Serializable; import java.util.Collections; import java.util.Map; +import java.util.stream.StreamSupport; import javax.annotation.Nonnull; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.batch.measure.Metric; import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.test.MutableTestPlan; +import org.sonar.api.test.TestCase.Status; import org.sonar.api.utils.KeyValueFormat; +import org.sonar.scanner.deprecated.test.TestPlanBuilder; import org.sonar.scanner.index.BatchComponent; import org.sonar.scanner.index.BatchComponentCache; import org.sonar.scanner.protocol.output.ScannerReport; @@ -44,6 +51,16 @@ import static org.sonar.api.measures.CoreMetrics.CONDITIONS_TO_COVER; import static org.sonar.api.measures.CoreMetrics.CONDITIONS_TO_COVER_KEY; import static org.sonar.api.measures.CoreMetrics.LINES_TO_COVER; import static org.sonar.api.measures.CoreMetrics.LINES_TO_COVER_KEY; +import static org.sonar.api.measures.CoreMetrics.SKIPPED_TESTS; +import static org.sonar.api.measures.CoreMetrics.SKIPPED_TESTS_KEY; +import static org.sonar.api.measures.CoreMetrics.TESTS; +import static org.sonar.api.measures.CoreMetrics.TESTS_KEY; +import static org.sonar.api.measures.CoreMetrics.TEST_ERRORS; +import static org.sonar.api.measures.CoreMetrics.TEST_ERRORS_KEY; +import static org.sonar.api.measures.CoreMetrics.TEST_EXECUTION_TIME; +import static org.sonar.api.measures.CoreMetrics.TEST_EXECUTION_TIME_KEY; +import static org.sonar.api.measures.CoreMetrics.TEST_FAILURES; +import static org.sonar.api.measures.CoreMetrics.TEST_FAILURES_KEY; import static org.sonar.api.measures.CoreMetrics.UNCOVERED_CONDITIONS; import static org.sonar.api.measures.CoreMetrics.UNCOVERED_CONDITIONS_KEY; import static org.sonar.api.measures.CoreMetrics.UNCOVERED_LINES; @@ -100,47 +117,76 @@ public class MeasuresPublisher implements ReportPublisherStep { private final BatchComponentCache componentCache; private final MeasureCache measureCache; + private final TestPlanBuilder testPlanBuilder; - public MeasuresPublisher(BatchComponentCache resourceCache, MeasureCache measureCache) { + public MeasuresPublisher(BatchComponentCache resourceCache, MeasureCache measureCache, TestPlanBuilder testPlanBuilder) { this.componentCache = resourceCache; this.measureCache = measureCache; + this.testPlanBuilder = testPlanBuilder; } @Override public void publish(ScannerReportWriter writer) { for (final BatchComponent component : componentCache.all()) { // Recompute all coverage measures from line data to take into account the possible merge of several reports - DefaultMeasure<String> lineHitsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY); - if (lineHitsMeasure != null) { - Map<Integer, Integer> lineHits = KeyValueFormat.parseIntInt(lineHitsMeasure.value()); - measureCache.put(component.key(), LINES_TO_COVER_KEY, new DefaultMeasure<Integer>().forMetric(LINES_TO_COVER).withValue(lineHits.keySet().size())); - measureCache.put(component.key(), UNCOVERED_LINES_KEY, - new DefaultMeasure<Integer>().forMetric(UNCOVERED_LINES).withValue((int) lineHits.values() - .stream() - .filter(hit -> hit == 0) - .count())); - } - DefaultMeasure<String> conditionsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.CONDITIONS_BY_LINE_KEY); - DefaultMeasure<String> coveredConditionsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY); - if (conditionsMeasure != null) { - Map<Integer, Integer> conditions = KeyValueFormat.parseIntInt(conditionsMeasure.value()); - Map<Integer, Integer> coveredConditions = coveredConditionsMeasure != null ? KeyValueFormat.parseIntInt(coveredConditionsMeasure.value()) : Collections.emptyMap(); - measureCache.put(component.key(), CONDITIONS_TO_COVER_KEY, new DefaultMeasure<Integer>().forMetric(CONDITIONS_TO_COVER).withValue(conditions - .values() - .stream() - .mapToInt(Integer::intValue) - .sum())); - measureCache.put(component.key(), UNCOVERED_CONDITIONS_KEY, - new DefaultMeasure<Integer>().forMetric(UNCOVERED_CONDITIONS) - .withValue((int) conditions.keySet() - .stream() - .mapToInt(line -> conditions.get(line) - coveredConditions.get(line)) - .sum())); - } + updateCoverageFromLineData(component); + // Recompute test execution measures from MutableTestPlan to take into account the possible merge of several reports + updateTestExecutionFromTestPlan(component); + Iterable<DefaultMeasure<?>> scannerMeasures = measureCache.byComponentKey(component.key()); Iterable<ScannerReport.Measure> reportMeasures = transform(scannerMeasures, new MeasureToReportMeasure(component)); writer.writeComponentMeasures(component.batchId(), reportMeasures); } } + private void updateTestExecutionFromTestPlan(final BatchComponent component) { + final MutableTestPlan testPlan = testPlanBuilder.loadPerspective(MutableTestPlan.class, component.inputComponent()); + if (testPlan == null || Iterables.isEmpty(testPlan.testCases())) { + return; + } + long nonSkippedTests = StreamSupport.stream(testPlan.testCases().spliterator(), false).filter(t -> t.status() != Status.SKIPPED).count(); + measureCache.put(component.key(), TESTS_KEY, new DefaultMeasure<Integer>().forMetric(TESTS).withValue((int) nonSkippedTests)); + long executionTime = StreamSupport.stream(testPlan.testCases().spliterator(), false).mapToLong(t -> t.durationInMs() != null ? t.durationInMs().longValue() : 0L).sum(); + measureCache.put(component.key(), TEST_EXECUTION_TIME_KEY, new DefaultMeasure<Long>().forMetric(TEST_EXECUTION_TIME).withValue(executionTime)); + long errorTests = StreamSupport.stream(testPlan.testCases().spliterator(), false).filter(t -> t.status() == Status.ERROR).count(); + measureCache.put(component.key(), TEST_ERRORS_KEY, new DefaultMeasure<Integer>().forMetric(TEST_ERRORS).withValue((int) errorTests)); + long skippedTests = StreamSupport.stream(testPlan.testCases().spliterator(), false).filter(t -> t.status() == Status.SKIPPED).count(); + measureCache.put(component.key(), SKIPPED_TESTS_KEY, new DefaultMeasure<Integer>().forMetric(SKIPPED_TESTS).withValue((int) skippedTests)); + long failedTests = StreamSupport.stream(testPlan.testCases().spliterator(), false).filter(t -> t.status() == Status.FAILURE).count(); + measureCache.put(component.key(), TEST_FAILURES_KEY, new DefaultMeasure<Integer>().forMetric(TEST_FAILURES).withValue((int) failedTests)); + } + + private void updateCoverageFromLineData(final BatchComponent component) { + if (!component.isFile() || ((InputFile) component.inputComponent()).type() != Type.MAIN) { + return; + } + DefaultMeasure<String> lineHitsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY); + if (lineHitsMeasure != null) { + Map<Integer, Integer> lineHits = KeyValueFormat.parseIntInt(lineHitsMeasure.value()); + measureCache.put(component.key(), LINES_TO_COVER_KEY, new DefaultMeasure<Integer>().forMetric(LINES_TO_COVER).withValue(lineHits.keySet().size())); + measureCache.put(component.key(), UNCOVERED_LINES_KEY, + new DefaultMeasure<Integer>().forMetric(UNCOVERED_LINES).withValue((int) lineHits.values() + .stream() + .filter(hit -> hit == 0) + .count())); + } + DefaultMeasure<String> conditionsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.CONDITIONS_BY_LINE_KEY); + DefaultMeasure<String> coveredConditionsMeasure = (DefaultMeasure<String>) measureCache.byMetric(component.key(), CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY); + if (conditionsMeasure != null) { + Map<Integer, Integer> conditions = KeyValueFormat.parseIntInt(conditionsMeasure.value()); + Map<Integer, Integer> coveredConditions = coveredConditionsMeasure != null ? KeyValueFormat.parseIntInt(coveredConditionsMeasure.value()) : Collections.emptyMap(); + measureCache.put(component.key(), CONDITIONS_TO_COVER_KEY, new DefaultMeasure<Integer>().forMetric(CONDITIONS_TO_COVER).withValue(conditions + .values() + .stream() + .mapToInt(Integer::intValue) + .sum())); + measureCache.put(component.key(), UNCOVERED_CONDITIONS_KEY, + new DefaultMeasure<Integer>().forMetric(UNCOVERED_CONDITIONS) + .withValue((int) conditions.keySet() + .stream() + .mapToInt(line -> conditions.get(line) - coveredConditions.get(line)) + .sum())); + } + } + } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java index cb341a96a3b..7cb5611cdba 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java @@ -60,7 +60,7 @@ public class MetadataPublisher implements ReportPublisherStep { builder.setBranch(branch); } for (QProfile qp : qProfiles.findAll()) { - builder.getMutableQprofilesPerLanguage().put(qp.getLanguage(), org.sonar.scanner.protocol.output.ScannerReport.Metadata.QProfile.newBuilder() + builder.getMutableQprofilesPerLanguage().put(qp.getLanguage(), ScannerReport.Metadata.QProfile.newBuilder() .setKey(qp.getKey()) .setLanguage(qp.getLanguage()) .setName(qp.getName()) diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisher.java index e5f7cc0cbe1..d37c9439e8b 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisher.java @@ -24,8 +24,6 @@ import com.google.common.collect.Iterables; import java.util.HashSet; import java.util.Set; import javax.annotation.Nonnull; -import org.sonar.api.batch.fs.InputFile.Type; -import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.test.CoverageBlock; import org.sonar.api.test.MutableTestCase; import org.sonar.api.test.MutableTestPlan; @@ -115,16 +113,7 @@ public class TestExecutionAndCoveragePublisher implements ReportPublisherStep { @Override public void publish(ScannerReportWriter writer) { for (final BatchComponent component : componentCache.all()) { - if (!component.isFile()) { - continue; - } - - DefaultInputFile inputFile = (DefaultInputFile) component.inputComponent(); - if (inputFile.type() != Type.TEST) { - continue; - } - - final MutableTestPlan testPlan = testPlanBuilder.loadPerspective(MutableTestPlan.class, component); + final MutableTestPlan testPlan = testPlanBuilder.loadPerspective(MutableTestPlan.class, component.inputComponent()); if (testPlan == null || Iterables.isEmpty(testPlan.testCases())) { continue; } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java index 72f95401c53..5ab42b57c2e 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java @@ -35,7 +35,7 @@ import org.sonar.scanner.bootstrap.BatchExtensionDictionnary; import org.sonar.scanner.bootstrap.ExtensionInstaller; import org.sonar.scanner.bootstrap.ExtensionUtils; import org.sonar.scanner.deprecated.DeprecatedSensorContext; -import org.sonar.scanner.deprecated.perspectives.BatchPerspectives; +import org.sonar.scanner.deprecated.perspectives.ScannerPerspectives; import org.sonar.scanner.events.EventBus; import org.sonar.scanner.index.BatchComponentCache; import org.sonar.scanner.index.DefaultIndex; @@ -162,7 +162,7 @@ public class ModuleScanContainer extends ComponentContainer { IgnoreIssuesFilter.class, // Perspectives - BatchPerspectives.class, + ScannerPerspectives.class, HighlightableBuilder.class, SymbolizableBuilder.class, diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/HighlightableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/HighlightableBuilder.java index 5f062b22946..da793d3e981 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/HighlightableBuilder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/HighlightableBuilder.java @@ -21,12 +21,12 @@ package org.sonar.scanner.source; import javax.annotation.CheckForNull; import org.sonar.api.batch.AnalysisMode; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.source.Highlightable; import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; public class HighlightableBuilder extends PerspectiveBuilder<Highlightable> { @@ -41,9 +41,9 @@ public class HighlightableBuilder extends PerspectiveBuilder<Highlightable> { @CheckForNull @Override - public Highlightable loadPerspective(Class<Highlightable> perspectiveClass, BatchComponent component) { + public Highlightable loadPerspective(Class<Highlightable> perspectiveClass, InputComponent component) { if (component.isFile()) { - InputFile path = (InputFile) component.inputComponent(); + InputFile path = (InputFile) component; return new DefaultHighlightable((DefaultInputFile) path, sensorStorage, analysisMode); } return null; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/SymbolizableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/SymbolizableBuilder.java index e805c6dbcc4..947670bf769 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/SymbolizableBuilder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/source/SymbolizableBuilder.java @@ -21,11 +21,11 @@ package org.sonar.scanner.source; import javax.annotation.CheckForNull; import org.sonar.api.batch.AnalysisMode; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.source.Symbolizable; import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; import org.sonar.scanner.sensor.DefaultSensorStorage; public class SymbolizableBuilder extends PerspectiveBuilder<Symbolizable> { @@ -41,9 +41,9 @@ public class SymbolizableBuilder extends PerspectiveBuilder<Symbolizable> { @CheckForNull @Override - public Symbolizable loadPerspective(Class<Symbolizable> perspectiveClass, BatchComponent component) { + public Symbolizable loadPerspective(Class<Symbolizable> perspectiveClass, InputComponent component) { if (component.isFile()) { - InputFile path = (InputFile) component.inputComponent(); + InputFile path = (InputFile) component; return new DefaultSymbolizable((DefaultInputFile) path, sensorStorage, analysisMode); } return null; diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilderTest.java index 791db326ae7..934141f862d 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilderTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/deprecated/perspectives/PerspectiveBuilderTest.java @@ -20,18 +20,18 @@ package org.sonar.scanner.deprecated.perspectives; import org.junit.Test; +import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.component.Perspective; -import org.sonar.scanner.deprecated.perspectives.PerspectiveBuilder; -import org.sonar.scanner.index.BatchComponent; import static org.assertj.core.api.Assertions.assertThat; public class PerspectiveBuilderTest { + @Test public void testGetPerspectiveClass() throws Exception { PerspectiveBuilder<FakePerspective> builder = new PerspectiveBuilder<FakePerspective>(FakePerspective.class) { @Override - public FakePerspective loadPerspective(Class<FakePerspective> perspectiveClass, BatchComponent component) { + public FakePerspective loadPerspective(Class<FakePerspective> perspectiveClass, InputComponent component) { return null; } }; diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParserTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParserTest.java new file mode 100644 index 00000000000..fb41e6b2ad9 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericCoverageReportParserTest.java @@ -0,0 +1,224 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import java.io.ByteArrayInputStream; +import java.io.File; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.sensor.internal.SensorContextTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GenericCoverageReportParserTest { + private DefaultInputFile fileWithBranches; + private DefaultInputFile fileWithoutBranch; + private DefaultInputFile emptyFile; + private SensorContextTester context; + + @Before + public void before() { + context = SensorContextTester.create(new File("")); + fileWithBranches = setupFile("src/main/java/com/example/ClassWithBranches.java"); + fileWithoutBranch = setupFile("src/main/java/com/example/ClassWithoutBranch.java"); + emptyFile = setupFile("src/main/java/com/example/EmptyClass.java"); + } + + @Test + public void empty_file() throws Exception { + addFileToFs(emptyFile); + GenericCoverageReportParser parser = new GenericCoverageReportParser(); + parser.parse(this.getClass().getResourceAsStream("coverage.xml"), context); + assertThat(parser.numberOfMatchedFiles()).isEqualTo(1); + assertThat(parser.numberOfUnknownFiles()).isEqualTo(3); + assertThat(parser.firstUnknownFiles()).hasSize(3); + } + + @Test + public void file_without_branch() throws Exception { + addFileToFs(fileWithoutBranch); + GenericCoverageReportParser parser = new GenericCoverageReportParser(); + parser.parse(this.getClass().getResourceAsStream("coverage.xml"), context); + assertThat(parser.numberOfMatchedFiles()).isEqualTo(1); + + assertThat(context.lineHits(fileWithoutBranch.key(), 2)).isEqualTo(0); + assertThat(context.lineHits(fileWithoutBranch.key(), 3)).isEqualTo(1); + assertThat(context.lineHits(fileWithoutBranch.key(), 4)).isNull(); + assertThat(context.lineHits(fileWithoutBranch.key(), 5)).isEqualTo(1); + assertThat(context.lineHits(fileWithoutBranch.key(), 6)).isEqualTo(0); + } + + @Test + public void file_with_branches() throws Exception { + addFileToFs(fileWithBranches); + GenericCoverageReportParser parser = new GenericCoverageReportParser(); + parser.parse(this.getClass().getResourceAsStream("coverage.xml"), context); + assertThat(parser.numberOfMatchedFiles()).isEqualTo(1); + + assertThat(context.lineHits(fileWithBranches.key(), 3)).isEqualTo(1); + assertThat(context.lineHits(fileWithBranches.key(), 4)).isEqualTo(1); + + assertThat(context.conditions(fileWithBranches.key(), 3)).isEqualTo(8); + assertThat(context.conditions(fileWithBranches.key(), 4)).isEqualTo(2); + + assertThat(context.coveredConditions(fileWithBranches.key(), 3)).isEqualTo(5); + assertThat(context.coveredConditions(fileWithBranches.key(), 4)).isEqualTo(0); + } + + @Test(expected = IllegalStateException.class) + public void coverage_invalid_root_node_name() throws Exception { + new GenericCoverageReportParser().parse(new ByteArrayInputStream("<mycoverage version=\"1\"></mycoverage>".getBytes()), context); + } + + @Test(expected = IllegalStateException.class) + public void coverage_invalid_report_version() throws Exception { + parseCoverageReport("<coverage version=\"2\"></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_no_report_version() throws Exception { + parseCoverageReport("<coverage></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_invalid_file_node_name() throws Exception { + parseCoverageReport("<coverage version=\"1\"><xx></xx></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void unitTest_invalid_file_node_name() throws Exception { + parseCoverageReport("<unitTest version=\"1\"><xx></xx></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_missing_path_attribute() throws Exception { + parseCoverageReport("<coverage version=\"1\"><file></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void unitTest_missing_path_attribute() throws Exception { + parseCoverageReport("<unitTest version=\"1\"><file></file></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_invalid_lineToCover_node_name() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><xx/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_missing_lineNumber_in_lineToCover() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><lineToCover covered=\"true\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_lineNumber_in_lineToCover_should_be_a_number() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><lineToCover lineNumber=\"x\" covered=\"true\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_lineNumber_in_lineToCover_should_be_positive() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><lineToCover lineNumber=\"0\" covered=\"true\"/></file></coverage>"); + } + + @Test + public void coverage_lineNumber_in_lineToCover_can_appear_several_times_for_same_file() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\"/>" + + "<lineToCover lineNumber=\"1\" covered=\"true\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_missing_covered_in_lineToCover() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><lineToCover lineNumber=\"3\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_covered_in_lineToCover_should_be_a_boolean() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\"><lineToCover lineNumber=\"3\" covered=\"x\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_branchesToCover_in_lineToCover_should_be_a_number() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\" branchesToCover=\"x\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_branchesToCover_in_lineToCover_should_not_be_negative() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\" branchesToCover=\"-1\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_coveredBranches_in_lineToCover_should_be_a_number() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\" branchesToCover=\"2\" coveredBranches=\"x\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_coveredBranches_in_lineToCover_should_not_be_negative() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\" branchesToCover=\"2\" coveredBranches=\"-1\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void coverage_coveredBranches_should_not_be_greater_than_branchesToCover() throws Exception { + addFileToFs(setupFile("file1")); + parseCoverageReport("<coverage version=\"1\"><file path=\"file1\">" + + "<lineToCover lineNumber=\"1\" covered=\"true\" branchesToCover=\"2\" coveredBranches=\"3\"/></file></coverage>"); + } + + @Test(expected = IllegalStateException.class) + public void testUnknownFile() throws Exception { + parseCoverageReportFile("xxx.xml"); + } + + private void addFileToFs(DefaultInputFile inputFile) { + context.fileSystem().add(inputFile); + } + + private void parseCoverageReport(String string) throws Exception { + new GenericCoverageReportParser().parse(new ByteArrayInputStream(string.getBytes()), context); + } + + private void parseCoverageReportFile(String reportLocation) throws Exception { + new GenericCoverageReportParser().parse(new File(reportLocation), context); + } + + private DefaultInputFile setupFile(String path) { + return new DefaultInputFile(context.module().key(), path) + .setLanguage("bla") + .setType(InputFile.Type.TEST) + .initMetadata("1\n2\n3\n4\n5\n6"); + } +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParserTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParserTest.java new file mode 100644 index 00000000000..3ee34b3f4f0 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/genericcoverage/GenericTestExecutionReportParserTest.java @@ -0,0 +1,167 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.genericcoverage; + +import java.io.ByteArrayInputStream; +import java.io.File; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.test.MutableTestCase; +import org.sonar.api.test.MutableTestPlan; +import org.sonar.scanner.deprecated.test.TestPlanBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class GenericTestExecutionReportParserTest { + + private TestPlanBuilder testPlanBuilder; + private DefaultInputFile fileWithBranches; + private DefaultInputFile emptyFile; + private SensorContextTester context; + private MutableTestPlan testPlan; + + @Before + public void before() { + context = SensorContextTester.create(new File("")); + fileWithBranches = setupFile("src/main/java/com/example/ClassWithBranches.java"); + emptyFile = setupFile("src/main/java/com/example/EmptyClass.java"); + testPlanBuilder = mock(TestPlanBuilder.class); + + MutableTestCase testCase = mockMutableTestCase(); + testPlan = mockMutableTestPlan(testCase); + + when(testPlanBuilder.loadPerspective(eq(MutableTestPlan.class), any(InputFile.class))).thenReturn(testPlan); + } + + @Test + public void ut_empty_file() throws Exception { + addFileToFs(emptyFile); + GenericTestExecutionReportParser parser = parseReportFile("unittest.xml"); + assertThat(parser.numberOfMatchedFiles()).isEqualTo(1); + assertThat(parser.numberOfUnknownFiles()).isEqualTo(1); + assertThat(parser.firstUnknownFiles()).hasSize(1); + } + + @Test + public void file_with_unittests() throws Exception { + addFileToFs(fileWithBranches); + GenericTestExecutionReportParser parser = parseReportFile("unittest2.xml"); + assertThat(parser.numberOfMatchedFiles()).isEqualTo(1); + + verify(testPlan).addTestCase("test1"); + verify(testPlan).addTestCase("test2"); + verify(testPlan).addTestCase("test3"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_invalid_root_node_name() throws Exception { + parseUnitTestReport("<mycoverage version=\"1\"></mycoverage>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_invalid_report_version() throws Exception { + parseUnitTestReport("<unitTest version=\"2\"></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_duration_in_testCase_should_be_a_number() throws Exception { + addFileToFs(setupFile("file1")); + parseUnitTestReport("<unitTest version=\"1\"><file path=\"file1\">" + + "<testCase name=\"test1\" duration=\"aaa\"/></file></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_failure_should_have_a_message() throws Exception { + addFileToFs(setupFile("file1")); + parseUnitTestReport("<unitTest version=\"1\"><file path=\"file1\">" + + "<testCase name=\"test1\" duration=\"2\"><failure /></testCase></file></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_error_should_have_a_message() throws Exception { + addFileToFs(setupFile("file1")); + parseUnitTestReport("<unitTest version=\"1\"><file path=\"file1\">" + + "<testCase name=\"test1\" duration=\"2\"><error /></testCase></file></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_skipped_should_have_a_message() throws Exception { + addFileToFs(setupFile("file1")); + parseUnitTestReport("<unitTest version=\"1\"><file path=\"file1\">" + + "<testCase name=\"test1\" duration=\"2\"><skipped notmessage=\"\"/></testCase></file></unitTest>"); + } + + @Test(expected = IllegalStateException.class) + public void unittest_duration_in_testCase_should_not_be_negative() throws Exception { + addFileToFs(setupFile("file1")); + parseUnitTestReport("<unitTest version=\"1\"><file path=\"file1\">" + + "<testCase name=\"test1\" duration=\"-5\"/></file></unitTest>"); + } + + private void addFileToFs(DefaultInputFile inputFile) { + context.fileSystem().add(inputFile); + } + + private GenericTestExecutionReportParser parseUnitTestReport(String string) throws Exception { + GenericTestExecutionReportParser parser = new GenericTestExecutionReportParser(testPlanBuilder); + parser.parse(new ByteArrayInputStream(string.getBytes()), context); + return parser; + } + + private GenericTestExecutionReportParser parseReportFile(String reportLocation) throws Exception { + GenericTestExecutionReportParser parser = new GenericTestExecutionReportParser(testPlanBuilder); + parser.parse(this.getClass().getResourceAsStream(reportLocation), context); + return parser; + } + + private DefaultInputFile setupFile(String path) { + return new DefaultInputFile(context.module().key(), path) + .setLanguage("bla") + .setType(InputFile.Type.TEST) + .initMetadata("1\n2\n3\n4\n5\n6"); + } + + private MutableTestPlan mockMutableTestPlan(MutableTestCase testCase) { + MutableTestPlan testPlan = mock(MutableTestPlan.class); + when(testPlan.addTestCase(anyString())).thenReturn(testCase); + return testPlan; + } + + private MutableTestCase mockMutableTestCase() { + MutableTestCase testCase = mock(MutableTestCase.class); + when(testCase.setDurationInMs(anyLong())).thenReturn(testCase); + when(testCase.setStatus(any(org.sonar.api.test.TestCase.Status.class))).thenReturn(testCase); + when(testCase.setMessage(anyString())).thenReturn(testCase); + when(testCase.setStackTrace(anyString())).thenReturn(testCase); + when(testCase.setType(anyString())).thenReturn(testCase); + return testCase; + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuableFactoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuableFactoryTest.java index 07038c5832f..996e5066856 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuableFactoryTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuableFactoryTest.java @@ -20,13 +20,10 @@ package org.sonar.scanner.issue; import org.junit.Test; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.issue.Issuable; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Project; import org.sonar.scanner.DefaultProjectTree; -import org.sonar.scanner.index.BatchComponent; -import org.sonar.scanner.issue.IssuableFactory; -import org.sonar.scanner.issue.ModuleIssues; import org.sonar.scanner.sensor.DefaultSensorContext; import static org.assertj.core.api.Assertions.assertThat; @@ -40,8 +37,7 @@ public class IssuableFactoryTest { @Test public void file_should_be_issuable() { IssuableFactory factory = new IssuableFactory(mock(DefaultSensorContext.class)); - BatchComponent component = new BatchComponent(1, File.create("foo/bar.c").setEffectiveKey("foo/bar.c"), null); - Issuable issuable = factory.loadPerspective(Issuable.class, component); + Issuable issuable = factory.loadPerspective(Issuable.class, new DefaultInputFile("foo", "src/Foo.java")); assertThat(issuable).isNotNull(); assertThat(issuable.issues()).isEmpty(); @@ -50,8 +46,7 @@ public class IssuableFactoryTest { @Test public void project_should_be_issuable() { IssuableFactory factory = new IssuableFactory(mock(DefaultSensorContext.class)); - BatchComponent component = new BatchComponent(1, new Project("Foo").setEffectiveKey("foo"), null); - Issuable issuable = factory.loadPerspective(Issuable.class, component); + Issuable issuable = factory.loadPerspective(Issuable.class, new DefaultInputModule("foo")); assertThat(issuable).isNotNull(); assertThat(issuable.issues()).isEmpty(); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumTest.java new file mode 100644 index 00000000000..f14e6c73286 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumTest.java @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-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.scanner.mediumtest.coverage; + +import java.io.File; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.scanner.mediumtest.BatchMediumTester; +import org.sonar.scanner.mediumtest.TaskResult; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class GenericCoverageMediumTest { + + public BatchMediumTester tester = BatchMediumTester.builder() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .build(); + + @Before + public void prepare() { + tester.start(); + } + + @After + public void stop() { + tester.stop(); + } + + @Test + public void singleReport() throws IOException { + + File projectDir = new File("src/test/resources/mediumtest/xoo/sample-generic-coverage"); + + TaskResult result = tester + .newScanTask(new File(projectDir, "sonar-project.properties")) + .property("sonar.coverageReportPaths", "coverage.xml") + .start(); + + InputFile noConditions = result.inputFile("xources/hello/NoConditions.xoo"); + assertThat(result.coverageFor(noConditions, 6).getHits()).isTrue(); + assertThat(result.coverageFor(noConditions, 6).getConditions()).isEqualTo(0); + assertThat(result.coverageFor(noConditions, 6).getCoveredConditions()).isEqualTo(0); + + assertThat(result.coverageFor(noConditions, 7).getHits()).isFalse(); + + assertThat(result.allMeasures().get(noConditions.key())).extracting("metricKey", "intValue.value", "stringValue.value") + .containsOnly( + tuple(CoreMetrics.LINES_KEY, 8, ""), + tuple(CoreMetrics.LINES_TO_COVER_KEY, 2, ""), + tuple(CoreMetrics.UNCOVERED_LINES_KEY, 1, ""), + tuple(CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY, 0, "6=1;7=0")); + + InputFile withConditions = result.inputFile("xources/hello/WithConditions.xoo"); + assertThat(result.coverageFor(withConditions, 3).getHits()).isTrue(); + assertThat(result.coverageFor(withConditions, 3).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(withConditions, 3).getCoveredConditions()).isEqualTo(1); + + assertThat(result.allMeasures().get(withConditions.key())).extracting("metricKey", "intValue.value", "stringValue.value") + .containsOnly( + tuple(CoreMetrics.LINES_KEY, 6, ""), + tuple(CoreMetrics.LINES_TO_COVER_KEY, 1, ""), + tuple(CoreMetrics.UNCOVERED_LINES_KEY, 0, ""), + tuple(CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY, 0, "3=1"), + tuple(CoreMetrics.CONDITIONS_TO_COVER_KEY, 2, ""), + tuple(CoreMetrics.UNCOVERED_CONDITIONS_KEY, 1, ""), + tuple(CoreMetrics.CONDITIONS_BY_LINE_KEY, 0, "3=2"), + tuple(CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY, 0, "3=1") + + ); + } + + @Test + public void twoReports() throws IOException { + + File projectDir = new File("src/test/resources/mediumtest/xoo/sample-generic-coverage"); + + TaskResult result = tester + .newScanTask(new File(projectDir, "sonar-project.properties")) + .property("sonar.coverageReportPaths", "coverage.xml,coverage2.xml") + .start(); + + InputFile noConditions = result.inputFile("xources/hello/NoConditions.xoo"); + assertThat(result.coverageFor(noConditions, 6).getHits()).isTrue(); + assertThat(result.coverageFor(noConditions, 6).getConditions()).isEqualTo(0); + assertThat(result.coverageFor(noConditions, 6).getCoveredConditions()).isEqualTo(0); + + assertThat(result.coverageFor(noConditions, 7).getHits()).isTrue(); + + assertThat(result.allMeasures().get(noConditions.key())).extracting("metricKey", "intValue.value", "stringValue.value") + .containsOnly( + tuple(CoreMetrics.LINES_KEY, 8, ""), + tuple(CoreMetrics.LINES_TO_COVER_KEY, 2, ""), + tuple(CoreMetrics.UNCOVERED_LINES_KEY, 0, ""), + tuple(CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY, 0, "6=1;7=1")); + + InputFile withConditions = result.inputFile("xources/hello/WithConditions.xoo"); + assertThat(result.coverageFor(withConditions, 3).getHits()).isTrue(); + assertThat(result.coverageFor(withConditions, 3).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(withConditions, 3).getCoveredConditions()).isEqualTo(2); + + assertThat(result.allMeasures().get(withConditions.key())).extracting("metricKey", "intValue.value", "stringValue.value") + .containsOnly( + tuple(CoreMetrics.LINES_KEY, 6, ""), + tuple(CoreMetrics.LINES_TO_COVER_KEY, 1, ""), + tuple(CoreMetrics.UNCOVERED_LINES_KEY, 0, ""), + tuple(CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY, 0, "3=2"), + tuple(CoreMetrics.CONDITIONS_TO_COVER_KEY, 2, ""), + tuple(CoreMetrics.UNCOVERED_CONDITIONS_KEY, 0, ""), + tuple(CoreMetrics.CONDITIONS_BY_LINE_KEY, 0, "3=2"), + tuple(CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY, 0, "3=2") + + ); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MeasuresPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MeasuresPublisherTest.java index 37edc1a0389..190907e001d 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MeasuresPublisherTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MeasuresPublisherTest.java @@ -28,10 +28,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.resources.Project; import org.sonar.core.util.CloseableIterator; +import org.sonar.scanner.deprecated.test.TestPlanBuilder; import org.sonar.scanner.index.BatchComponentCache; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.protocol.output.ScannerReportReader; @@ -65,11 +68,11 @@ public class MeasuresPublisherTest { Project p = new Project("foo").setAnalysisDate(new Date(1234567L)); BatchComponentCache resourceCache = new BatchComponentCache(); sampleFile = org.sonar.api.resources.File.create("src/Foo.php").setEffectiveKey(FILE_KEY); - resourceCache.add(p, null); - resourceCache.add(sampleFile, null); + resourceCache.add(p, null).setInputComponent(new DefaultInputModule("foo")); + resourceCache.add(sampleFile, null).setInputComponent(new DefaultInputFile("foo", "src/Foo.php")); measureCache = mock(MeasureCache.class); when(measureCache.byComponentKey(anyString())).thenReturn(Collections.<DefaultMeasure<?>>emptyList()); - publisher = new MeasuresPublisher(resourceCache, measureCache); + publisher = new MeasuresPublisher(resourceCache, measureCache, mock(TestPlanBuilder.class)); } @Test diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/HighlightableBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/HighlightableBuilderTest.java index fd07607430d..f68fe762e42 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/HighlightableBuilderTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/HighlightableBuilderTest.java @@ -24,13 +24,7 @@ import org.sonar.api.batch.AnalysisMode; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.sensor.internal.SensorStorage; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Resource; import org.sonar.api.source.Highlightable; -import org.sonar.scanner.index.BatchComponent; -import org.sonar.scanner.source.DefaultHighlightable; -import org.sonar.scanner.source.HighlightableBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -39,21 +33,16 @@ public class HighlightableBuilderTest { @Test public void should_load_default_perspective() { - Resource file = File.create("foo.c").setEffectiveKey("myproject:path/to/foo.c"); - BatchComponent component = new BatchComponent(1, file, null).setInputComponent(new DefaultInputFile("foo", "foo.c")); - HighlightableBuilder builder = new HighlightableBuilder(mock(SensorStorage.class), mock(AnalysisMode.class)); - Highlightable perspective = builder.loadPerspective(Highlightable.class, component); + Highlightable perspective = builder.loadPerspective(Highlightable.class, new DefaultInputFile("foo", "foo.c")); assertThat(perspective).isNotNull().isInstanceOf(DefaultHighlightable.class); } @Test public void project_should_not_be_highlightable() { - BatchComponent component = new BatchComponent(1, new Project("struts").setEffectiveKey("org.struts"), null).setInputComponent(new DefaultInputModule("struts")); - HighlightableBuilder builder = new HighlightableBuilder(mock(SensorStorage.class), mock(AnalysisMode.class)); - Highlightable perspective = builder.loadPerspective(Highlightable.class, component); + Highlightable perspective = builder.loadPerspective(Highlightable.class, new DefaultInputModule("struts")); assertThat(perspective).isNull(); } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/SymbolizableBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/SymbolizableBuilderTest.java index 9f50909105c..9a5e2edbc9f 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/SymbolizableBuilderTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/source/SymbolizableBuilderTest.java @@ -24,11 +24,7 @@ import org.sonar.api.batch.AnalysisMode; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.component.Perspective; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Resource; import org.sonar.api.source.Symbolizable; -import org.sonar.scanner.index.BatchComponent; import org.sonar.scanner.sensor.DefaultSensorStorage; import static org.assertj.core.api.Assertions.assertThat; @@ -38,21 +34,16 @@ public class SymbolizableBuilderTest { @Test public void should_load_perspective() { - Resource file = File.create("foo.c").setEffectiveKey("myproject:path/to/foo.c"); - BatchComponent component = new BatchComponent(1, file, null).setInputComponent(new DefaultInputFile("foo", "foo.c")); - SymbolizableBuilder perspectiveBuilder = new SymbolizableBuilder(mock(DefaultSensorStorage.class), mock(AnalysisMode.class)); - Perspective perspective = perspectiveBuilder.loadPerspective(Symbolizable.class, component); + Perspective perspective = perspectiveBuilder.loadPerspective(Symbolizable.class, new DefaultInputFile("foo", "foo.c")); assertThat(perspective).isInstanceOf(Symbolizable.class); } @Test public void project_should_not_be_highlightable() { - BatchComponent component = new BatchComponent(1, new Project("struts").setEffectiveKey("org.struts"), null).setInputComponent(new DefaultInputModule("struts")); - SymbolizableBuilder builder = new SymbolizableBuilder(mock(DefaultSensorStorage.class), mock(AnalysisMode.class)); - Perspective perspective = builder.loadPerspective(Symbolizable.class, component); + Perspective perspective = builder.loadPerspective(Symbolizable.class, new DefaultInputModule("struts")); assertThat(perspective).isNull(); } diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage.xml b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage.xml new file mode 100644 index 00000000000..e12af5e2fb2 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage.xml @@ -0,0 +1,9 @@ +<coverage version="1"> + <file path="xources/hello/NoConditions.xoo"> + <lineToCover lineNumber="6" covered="true"/> + <lineToCover lineNumber="7" covered="false"/> + </file> + <file path="xources/hello/WithConditions.xoo"> + <lineToCover lineNumber="3" covered="true" branchesToCover="2" coveredBranches="1"/> + </file> +</coverage> diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage2.xml b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage2.xml new file mode 100644 index 00000000000..1a1e02b0f7b --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/coverage2.xml @@ -0,0 +1,8 @@ +<coverage version="1"> + <file path="xources/hello/NoConditions.xoo"> + <lineToCover lineNumber="7" covered="true"/> + </file> + <file path="xources/hello/WithConditions.xoo"> + <lineToCover lineNumber="3" covered="true" branchesToCover="2" coveredBranches="2"/> + </file> +</coverage> diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/sonar-project.properties new file mode 100644 index 00000000000..ac18398a604 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/sonar-project.properties @@ -0,0 +1,3 @@ +sonar.projectKey=sample-generic-coverage +sonar.sources=xources +sonar.language=xoo diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/NoConditions.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/NoConditions.xoo new file mode 100644 index 00000000000..1d9c60d56b7 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/NoConditions.xoo @@ -0,0 +1,8 @@ +package hello; + +public class HelloJava { + + public static void main(String[] args) { + System.out.println("Hello"); + } +}
\ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/WithConditions.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/WithConditions.xoo new file mode 100644 index 00000000000..8f469103183 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-generic-coverage/xources/hello/WithConditions.xoo @@ -0,0 +1,6 @@ + object HelloWorld { + def main(args: Array[String]) { + args.isEmpty ? println("Hello, world of xoo!") : println("Hello, world of empty!") + } + } +
\ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/coverage.xml b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/coverage.xml new file mode 100644 index 00000000000..74dd213c413 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/coverage.xml @@ -0,0 +1,14 @@ +<coverage version="1"> + <file path="src/main/java/com/example/NonExisting.java"/> + <file path="src/main/java/com/example/EmptyClass.java"/> + <file path="src/main/java/com/example/ClassWithoutBranch.java"> + <lineToCover lineNumber="2" covered="false"/> + <lineToCover lineNumber="3" covered="true"/> + <lineToCover lineNumber="5" covered="true"/> + <lineToCover lineNumber="6" covered="false"/> + </file> + <file path="src/main/java/com/example/ClassWithBranches.java"> + <lineToCover lineNumber="3" covered="true" branchesToCover="8" coveredBranches="5"/> + <lineToCover lineNumber="4" covered="true" branchesToCover="2"/> + </file> +</coverage> diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest.xml b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest.xml new file mode 100644 index 00000000000..f1d12ec0f50 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest.xml @@ -0,0 +1,15 @@ +<unitTest version="1"> + <file path="src/main/java/com/example/EmptyClass.java"/> + <file path="src/main/java/com/example/ClassWithoutBranch.java"> + <testCase name="test1" duration="5"/> + <testCase name="test2" duration="500"> + <skipped message="short message">other</skipped> + </testCase> + <testCase name="test3" duration="100"> + <failure message="short">stacktrace</failure> + </testCase> + <testCase name="test4" duration="500"> + <error message="short">stacktrace</error> + </testCase> + </file> +</unitTest> diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest2.xml b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest2.xml new file mode 100644 index 00000000000..35ff4ad5912 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/genericcoverage/unittest2.xml @@ -0,0 +1,15 @@ +<unitTest version="1"> + <file path="src/main/java/com/example/EmptyClass.java"/> + <file path="src/main/java/com/example/ClassWithBranches.java"> + <testCase name="test1" duration="500"> + <skipped message="short message">other</skipped> + </testCase> + <testCase name="test2" duration="300"> + <failure message="short">stacktrace</failure> + </testCase> + <testCase name="test3" duration="300" /> + <testCase name="test4" duration="300"> + <ok message="aaa">long</ok> + </testCase> + </file> +</unitTest> |