From 2bc82c281b9c1384940c38ec31d2761f000c7566 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Mon, 27 Jan 2014 21:25:27 +0100 Subject: [PATCH] SONAR-3024 Provide a new CoberturaReportParser that do not rely on deprecated Resource API --- .../sonar/batch/scan/ModuleScanContainer.java | 23 ++- .../batch/coverage/CoberturaReportParser.java | 159 ++++++++++++++++ .../api/utils/CoberturaReportParserUtils.java | 3 + .../coverage/CoberturaReportParserTest.java | 170 ++++++++++++++++++ 4 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/batch/coverage/CoberturaReportParser.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/batch/coverage/CoberturaReportParserTest.java diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java index fde6e9c3ec7..55cfa513a5e 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java @@ -19,6 +19,8 @@ */ package org.sonar.batch.scan; +import org.sonar.api.batch.coverage.CoberturaReportParser; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.BatchExtension; @@ -29,7 +31,12 @@ import org.sonar.api.platform.ComponentContainer; import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; import org.sonar.api.scan.filesystem.FileExclusions; -import org.sonar.batch.*; +import org.sonar.batch.DefaultProjectClasspath; +import org.sonar.batch.DefaultSensorContext; +import org.sonar.batch.DefaultTimeMachine; +import org.sonar.batch.ProjectTree; +import org.sonar.batch.ResourceFilters; +import org.sonar.batch.ViolationFilters; import org.sonar.batch.bootstrap.BatchExtensionDictionnary; import org.sonar.batch.bootstrap.ExtensionInstaller; import org.sonar.batch.bootstrap.ExtensionMatcher; @@ -47,7 +54,17 @@ import org.sonar.batch.rule.ModuleQProfiles; import org.sonar.batch.rule.ModuleRulesProvider; import org.sonar.batch.rule.QProfileSensor; import org.sonar.batch.rule.RulesProfileProvider; -import org.sonar.batch.scan.filesystem.*; +import org.sonar.batch.scan.filesystem.ComponentIndexer; +import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; +import org.sonar.batch.scan.filesystem.DeprecatedFileFilters; +import org.sonar.batch.scan.filesystem.ExclusionFilters; +import org.sonar.batch.scan.filesystem.FileHashes; +import org.sonar.batch.scan.filesystem.FileIndex; +import org.sonar.batch.scan.filesystem.FileSystemLogger; +import org.sonar.batch.scan.filesystem.LanguageRecognizer; +import org.sonar.batch.scan.filesystem.ModuleFileSystemInitializer; +import org.sonar.batch.scan.filesystem.ProjectFileSystemAdapter; +import org.sonar.batch.scan.filesystem.RemoteFileHashes; import org.sonar.batch.scan.language.DefaultModuleLanguages; import org.sonar.batch.scan.report.ComponentSelectorFactory; import org.sonar.batch.scan.report.JsonReport; @@ -135,6 +152,8 @@ public class ModuleScanContainer extends ComponentContainer { IssuableFactory.class, ModuleIssues.class, + CoberturaReportParser.class, + ScanPerspectives.class); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/coverage/CoberturaReportParser.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/coverage/CoberturaReportParser.java new file mode 100644 index 00000000000..a0cb0329b01 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/coverage/CoberturaReportParser.java @@ -0,0 +1,159 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.api.batch.coverage; + +import com.google.common.base.Joiner; +import com.google.common.collect.Maps; +import org.apache.commons.lang.StringUtils; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.jfree.util.Log; +import org.sonar.api.BatchComponent; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.measures.CoverageMeasuresBuilder; +import org.sonar.api.measures.Measure; +import org.sonar.api.scan.filesystem.InputFile; +import org.sonar.api.scan.filesystem.ModuleFileSystem; +import org.sonar.api.utils.StaxParser; +import org.sonar.api.utils.XmlParserException; + +import javax.xml.stream.XMLStreamException; + +import java.io.File; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.Locale.ENGLISH; +import static org.sonar.api.utils.ParsingUtils.parseNumber; + +/** + * Parse a provided cobertura report and create appropriate measures. + * @since 4.2 + */ +public class CoberturaReportParser implements BatchComponent { + + private ModuleFileSystem fs; + + public CoberturaReportParser(ModuleFileSystem fs) { + this.fs = fs; + } + + /** + * Parse a Cobertura xml report and create measures accordingly + */ + public void parseReport(File xmlFile, final SensorContext context) { + try { + StaxParser parser = new StaxParser(new StaxParser.XmlStreamHandler() { + + public void stream(SMHierarchicCursor rootCursor) throws XMLStreamException { + rootCursor.advance(); + SMInputCursor rootChildCursor = rootCursor.childElementCursor(); + + List sourceDirs = new ArrayList(); + + while (rootChildCursor.getNext() != null) { + handleRootChildElement(sourceDirs, context, rootChildCursor); + } + } + + }); + parser.parse(xmlFile); + } catch (XMLStreamException e) { + throw new XmlParserException(e); + } + } + + private void handleRootChildElement(List sourceDirs, SensorContext context, SMInputCursor rootChildCursor) throws XMLStreamException { + if ("sources".equals(rootChildCursor.getLocalName())) { + collectSourceFolders(rootChildCursor.childElementCursor(), sourceDirs); + } else if ("packages".equals(rootChildCursor.getLocalName())) { + collectPackageMeasures(rootChildCursor.childElementCursor(), context, sourceDirs); + } + } + + private void collectSourceFolders(SMInputCursor source, List sourceDirs) throws XMLStreamException { + while (source.getNext() != null) { + sourceDirs.add(new File(source.collectDescendantText())); + } + } + + private void collectPackageMeasures(SMInputCursor pack, SensorContext context, List sourceDirs) throws XMLStreamException { + while (pack.getNext() != null) { + Map builderByFilename = Maps.newHashMap(); + collectFileMeasures(pack.descendantElementCursor("class"), builderByFilename); + for (Map.Entry entry : builderByFilename.entrySet()) { + String filename = entry.getKey(); + + InputFile inputfile = findInputFile(filename, sourceDirs); + if (inputfile != null) { + for (Measure measure : entry.getValue().createMeasures()) { + context.saveMeasure(inputfile, measure); + } + } + } + } + } + + private InputFile findInputFile(String filename, List sourceDirs) { + for (File srcDir : sourceDirs) { + File possibleFile = new File(srcDir, filename); + InputFile inputFile = fs.inputFile(possibleFile); + if (inputFile != null) { + return inputFile; + } + } + Log.debug("Filename " + filename + " was not found as an InputFile is any of the source folders: " + Joiner.on(", ").join(sourceDirs)); + return null; + } + + private static void collectFileMeasures(SMInputCursor clazz, Map builderByFilename) throws XMLStreamException { + while (clazz.getNext() != null) { + String fileName = clazz.getAttrValue("filename"); + CoverageMeasuresBuilder builder = builderByFilename.get(fileName); + if (builder == null) { + builder = CoverageMeasuresBuilder.create(); + builderByFilename.put(fileName, builder); + } + collectFileData(clazz, builder); + } + } + + private static void collectFileData(SMInputCursor clazz, CoverageMeasuresBuilder builder) throws XMLStreamException { + SMInputCursor line = clazz.childElementCursor("lines").advance().childElementCursor("line"); + while (line.getNext() != null) { + int lineId = Integer.parseInt(line.getAttrValue("number")); + try { + builder.setHits(lineId, (int) parseNumber(line.getAttrValue("hits"), ENGLISH)); + } catch (ParseException e) { + throw new XmlParserException(e); + } + + String isBranch = line.getAttrValue("branch"); + String text = line.getAttrValue("condition-coverage"); + if (StringUtils.equals(isBranch, "true") && StringUtils.isNotBlank(text)) { + String[] conditions = StringUtils.split(StringUtils.substringBetween(text, "(", ")"), "/"); + builder.setConditions(lineId, Integer.parseInt(conditions[1]), Integer.parseInt(conditions[0])); + } + } + } + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/CoberturaReportParserUtils.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/CoberturaReportParserUtils.java index 71a8a703b80..f7e376f631d 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/CoberturaReportParserUtils.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/CoberturaReportParserUtils.java @@ -25,6 +25,7 @@ import org.apache.commons.lang.StringUtils; import org.codehaus.staxmate.in.SMHierarchicCursor; import org.codehaus.staxmate.in.SMInputCursor; import org.sonar.api.batch.SensorContext; +import org.sonar.api.batch.coverage.CoberturaReportParser; import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.Resource; @@ -40,7 +41,9 @@ import static org.sonar.api.utils.ParsingUtils.parseNumber; /** * @since 3.7 + * @deprecated since 4.2 use {@link CoberturaReportParser} */ +@Deprecated public class CoberturaReportParserUtils { private CoberturaReportParserUtils() { diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/coverage/CoberturaReportParserTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/coverage/CoberturaReportParserTest.java new file mode 100644 index 00000000000..a620ec8eb12 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/coverage/CoberturaReportParserTest.java @@ -0,0 +1,170 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.api.batch.coverage; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.resources.JavaFile; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Scopes; +import org.sonar.api.scan.filesystem.InputFile; +import org.sonar.api.scan.filesystem.ModuleFileSystem; +import org.sonar.api.test.IsMeasure; +import org.sonar.api.test.IsResource; + +import java.io.File; +import java.net.URISyntaxException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CoberturaReportParserTest { + + private SensorContext context; + private ModuleFileSystem fs; + private CoberturaReportParser parser; + + @Before + public void setUp() { + context = mock(SensorContext.class); + fs = mock(ModuleFileSystem.class); + parser = new CoberturaReportParser(fs); + } + + @Test + public void collectFileLineCoverage() throws URISyntaxException { + InputFile inputFile = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/commons-chain/src/java", "org/apache/commons/chain/config/ConfigParser.java")))).thenReturn(inputFile); + parser.parseReport(getCoverageReport(), context); + + verify(context).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER, 30.0))); + verify(context).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES, 5.0))); + } + + @Test + public void collectFileBranchCoverage() throws URISyntaxException { + InputFile inputFile = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/commons-chain/src/java", "org/apache/commons/chain/config/ConfigParser.java")))).thenReturn(inputFile); + parser.parseReport(getCoverageReport(), context); + + verify(context).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.CONDITIONS_TO_COVER, 6.0))); + verify(context).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.UNCOVERED_CONDITIONS, 2.0))); + } + + @Test + public void testDoNotSaveMeasureOnResourceWhichDoesntExistInTheFileSystem() throws URISyntaxException { + when(fs.inputFile(any(File.class))).thenReturn(null); + parser.parseReport(getCoverageReport(), context); + verify(context, never()).saveMeasure(any(InputFile.class), any(Measure.class)); + } + + @Test + public void javaInterfaceHasNoCoverage() throws URISyntaxException { + InputFile inputFile = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/commons-chain/src/java", "org/apache/commons/chain/Chain.java")))).thenReturn(inputFile); + parser.parseReport(getCoverageReport(), context); + + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.COVERAGE))); + + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.LINE_COVERAGE))); + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER))); + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES))); + + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.BRANCH_COVERAGE))); + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.CONDITIONS_TO_COVER))); + verify(context, never()).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.UNCOVERED_CONDITIONS))); + } + + @Test + public void shouldInsertCoverageAtFileLevel() throws URISyntaxException { + File coverage = new File(getClass().getResource( + "/org/sonar/api/utils/CoberturaReportParserUtilsTest/shouldInsertCoverageAtFileLevel/coverage.xml").toURI()); + + InputFile innerClass = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/sonar/trunk/tests/integration/reference-projects/reference/src/main/java", + "org/sonar/samples/InnerClass.java")))).thenReturn(innerClass); + parser.parseReport(coverage, context); + + verify(context).saveMeasure(eq(innerClass), argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER, 35.0))); + verify(context).saveMeasure(eq(innerClass), argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES, 22.0))); + + verify(context).saveMeasure(eq(innerClass), argThat(new IsMeasure(CoreMetrics.CONDITIONS_TO_COVER, 4.0))); + verify(context).saveMeasure(eq(innerClass), argThat(new IsMeasure(CoreMetrics.UNCOVERED_CONDITIONS, 3.0))); + + verify(context, never()).saveMeasure( + argThat(new IsResource(Scopes.FILE, Qualifiers.FILE, "org.sonar.samples.InnerClass$InnerClassInside")), + argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER))); + verify(context, never()).saveMeasure( + argThat(new IsResource(Scopes.FILE, Qualifiers.FILE, "org.sonar.samples.InnerClass$InnerClassInside")), + argThat(new IsMeasure(CoreMetrics.CONDITIONS_TO_COVER))); + verify(context, never()).saveMeasure( + argThat(new IsResource(Scopes.FILE, Qualifiers.FILE, "org.sonar.samples.InnerClass$InnerClassInside")), + argThat(new IsMeasure(CoreMetrics.UNCOVERED_CONDITIONS))); + verify(context, never()).saveMeasure( + argThat(new IsResource(Scopes.FILE, Qualifiers.FILE, "org.sonar.samples.InnerClass$InnerClassInside")), + argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES))); + + verify(fs, never()).inputFile(eq(new File("/Users/simon/projects/sonar/trunk/tests/integration/reference-projects/reference/src/main/java", + "org/sonar/samples/PrivateClass.java"))); + + verify(context) + .saveMeasure( + eq(innerClass), + argThat(new IsMeasure( + CoreMetrics.COVERAGE_LINE_HITS_DATA, + "22=2;25=0;26=0;29=0;30=0;31=0;34=1;35=1;36=1;37=0;39=1;41=1;44=2;46=1;47=1;50=0;51=0;52=0;53=0;55=0;57=0;60=0;61=0;64=1;71=1;73=1;76=0;77=0;80=0;81=0;85=0;87=0;91=0;93=0;96=1"))); + } + + @Test + public void collectFileLineHitsData() throws URISyntaxException { + InputFile inputFile = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/commons-chain/src/java", "org/apache/commons/chain/impl/CatalogBase.java")))).thenReturn(inputFile); + when(context.getResource(any(Resource.class))).thenReturn(new JavaFile("org.sonar.MyClass")); + parser.parseReport(getCoverageReport(), context); + verify(context).saveMeasure( + eq(inputFile), + argThat(new IsMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA, + "48=117;56=234;66=0;67=0;68=0;84=999;86=999;98=318;111=18;121=0;122=0;125=0;126=0;127=0;128=0;131=0;133=0"))); + } + + @Test + public void shouldNotCountTwiceAnonymousClasses() throws URISyntaxException { + File coverage = new File(getClass().getResource("/org/sonar/api/utils/CoberturaReportParserUtilsTest/shouldNotCountTwiceAnonymousClasses.xml").toURI()); + InputFile inputFile = mock(InputFile.class); + when(fs.inputFile(eq(new File("/Users/simon/projects/sonar/trunk/tests/integration/reference-projects/reference/src/main/java", + "org/sonar/samples/MyFile.java")))).thenReturn(inputFile); + parser.parseReport(coverage, context); + + verify(context).saveMeasure(eq(inputFile), argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER, 5.0))); // do not count line 26 twice + } + + private File getCoverageReport() throws URISyntaxException { + return new File(getClass().getResource("/org/sonar/api/utils/CoberturaReportParserUtilsTest/commons-chain-coverage.xml").toURI()); + } +} -- 2.39.5