Skip to content

Added support for parsing clover.xml files generated by the clover ka… #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 154 additions & 137 deletions src/main/java/org/sonar/plugins/clover/CloverXmlReportParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@
package org.sonar.plugins.clover;

import org.apache.commons.lang.StringUtils;
import org.codehaus.staxmate.in.SMEvent;
import org.codehaus.staxmate.in.SMFilterFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.codehaus.staxmate.in.SimpleFilter;
import org.codehaus.staxmate.in.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.SensorContext;
Expand All @@ -42,153 +38,174 @@

public class CloverXmlReportParser {

private static final Logger LOG = LoggerFactory.getLogger(CloverXmlReportParser.class);
private SensorContext context;
private final InputFileProvider inputFileProvider;
private int files;
private int unmatchedFile;
private String unmatchedFiles;
final CoverageMeasuresBuilder fileMeasuresBuilder = CoverageMeasuresBuilder.create();

CloverXmlReportParser(SensorContext context, InputFileProvider inputFileProvider) {
this.context = context;
this.inputFileProvider = inputFileProvider;
}

private static boolean reportExists(@Nullable File report) {
return report != null && report.exists() && report.isFile();
}

protected void collect(File xmlFile) {
try {
if (reportExists(xmlFile)) {
files = 0;
unmatchedFile = 0;
unmatchedFiles = "";
LOG.info("Parsing " + xmlFile.getCanonicalPath());
createStaxParser().parse(xmlFile);
LOG.info("Matched files in report : {}", getMatchedPercentage());
if (!unmatchedFiles.isEmpty()) {
LOG.warn("{} files in clover report did not match any file in SonarQube Index : {}", unmatchedFile, unmatchedFiles);
}
}
} catch (IllegalStateException e) {
LOG.error("Format of clover report file is unexpected ", e);
throw new XmlParserException(e);
} catch (Exception e) {
LOG.error("An error occured while parsing clover xml report : ", e);
throw new XmlParserException(e);
private static final Logger LOG = LoggerFactory.getLogger(CloverXmlReportParser.class);
private SensorContext context;
private final InputFileProvider inputFileProvider;
private int files;
private int unmatchedFile;
private String unmatchedFiles;
final CoverageMeasuresBuilder fileMeasuresBuilder = CoverageMeasuresBuilder.create();

CloverXmlReportParser(SensorContext context, InputFileProvider inputFileProvider) {
this.context = context;
this.inputFileProvider = inputFileProvider;
}

private static boolean reportExists(@Nullable File report) {
return report != null && report.exists() && report.isFile();
}
}

private StaxParser createStaxParser() {
return new StaxParser(new StaxParser.XmlStreamHandler() {
@Override
public void stream(SMHierarchicCursor rootCursor) throws XMLStreamException {
protected void collect(File xmlFile) {
try {
collectProjectMeasures(rootCursor.advance());
} catch (ParseException e) {
throw new XMLStreamException(e);
if (reportExists(xmlFile)) {
files = 0;
unmatchedFile = 0;
unmatchedFiles = "";
LOG.info("Parsing " + xmlFile.getCanonicalPath());
createStaxParser().parse(xmlFile);
LOG.info("Matched files in report : {}", getMatchedPercentage());
if (!unmatchedFiles.isEmpty()) {
LOG.warn("{} files in clover report did not match any file in SonarQube Index : {}", unmatchedFile, unmatchedFiles);
}
}
} catch (IllegalStateException e) {
LOG.error("Format of clover report file is unexpected ", e);
throw new XmlParserException(e);
} catch (Exception e) {
LOG.error("An error occured while parsing clover xml report : ", e);
throw new XmlParserException(e);
}
}

private StaxParser createStaxParser() {
return new StaxParser(new StaxParser.XmlStreamHandler() {
@Override
public void stream(SMHierarchicCursor rootCursor) throws XMLStreamException {
try {
collectProjectMeasures(rootCursor.advance());
} catch (ParseException e) {
throw new XMLStreamException(e);
}
}
});
}

private String getMatchedPercentage() {
if (files == 0) {
return "No files found in <project> section of report";
}
}
});
}
return (files - unmatchedFile) * 100 / files + "%";
}

private String getMatchedPercentage() {
if (files == 0) {
return "No files found in <project> section of report";
private void collectProjectMeasures(SMInputCursor rootCursor) throws ParseException, XMLStreamException {
SMInputCursor projectCursor = rootCursor.descendantElementCursor("project");
SMInputCursor projectChildrenCursor = projectCursor.advance().childElementCursor();
projectChildrenCursor.setFilter(new SimpleFilter(SMEvent.START_ELEMENT));
//Skip the metrics tag.
projectChildrenCursor.advance();
collectPackageMeasures(projectChildrenCursor);
}
return (files - unmatchedFile) * 100 / files+"%";
}

private void collectProjectMeasures(SMInputCursor rootCursor) throws ParseException, XMLStreamException {
SMInputCursor projectCursor = rootCursor.descendantElementCursor("project");
SMInputCursor projectChildrenCursor = projectCursor.advance().childElementCursor();
projectChildrenCursor.setFilter(new SimpleFilter(SMEvent.START_ELEMENT));
//Skip the metrics tag.
projectChildrenCursor.advance();
collectPackageMeasures(projectChildrenCursor);
}

private void collectPackageMeasures(SMInputCursor packCursor) throws ParseException, XMLStreamException {
while (packCursor.getNext() != null) {
SMInputCursor packChildrenCursor = packCursor.descendantElementCursor();
packChildrenCursor.setFilter(new SimpleFilter(SMEvent.START_ELEMENT));
//Skip the metrics tag.
packChildrenCursor.advance();
collectFileMeasures(packChildrenCursor);

private void collectPackageMeasures(SMInputCursor packCursor) throws ParseException, XMLStreamException {
while (packCursor.getNext() != null) {
SMInputCursor packChildrenCursor = packCursor.descendantElementCursor();
packChildrenCursor.setFilter(new SimpleFilter(SMEvent.START_ELEMENT));
//Skip the metrics tag.
packChildrenCursor.advance();
collectFileMeasures(packChildrenCursor);
}
}
}

private void collectFileMeasures(SMInputCursor fileCursor) throws ParseException, XMLStreamException {
fileCursor.setFilter(SMFilterFactory.getElementOnlyFilter("file"));
while (fileCursor.getNext() != null) {
if (fileCursor.asEvent().isStartElement()) {
String path = fileCursor.getAttrValue("path");
if (path != null) {
SMInputCursor fileChildrenCursor = fileCursor.childCursor(new SimpleFilter(SMEvent.START_ELEMENT));
// cursor should be on the metrics element
if (canBeIncludedInFileMetrics(fileChildrenCursor)) {
// cursor should be now on the line cursor
saveHitsData(getInputFile(path), fileChildrenCursor);
}

private void collectFileMeasures(SMInputCursor fileCursor) throws ParseException, XMLStreamException {
fileCursor.setFilter(SMFilterFactory.getElementOnlyFilter("file"));
while (fileCursor.getNext() != null) {
if (fileCursor.asEvent().isStartElement()) {
String path = getNullSafeAttributeValue(fileCursor, "path");
if (path != null) {
SMInputCursor fileChildrenCursor = fileCursor.childCursor(new SimpleFilter(SMEvent.START_ELEMENT));
// cursor should be on the metrics element
if (canBeIncludedInFileMetrics(fileChildrenCursor)) {
// cursor should be now on the line cursor
saveHitsData(getInputFile(path), fileChildrenCursor);
}
}
}
}
}
}
}

private InputFile getInputFile(String path) {
files++;
InputFile resource = inputFileProvider.fromPath(path);
if (resource == null) {
unmatchedFile++;
LOG.warn("Resource " + path + " was not found.");
unmatchedFiles += path + ", ";

private InputFile getInputFile(String path) {
files++;
InputFile resource = inputFileProvider.fromPath(path);
if (resource == null) {
unmatchedFile++;
LOG.warn("Resource " + path + " was not found.");
unmatchedFiles += path + ", ";
}
return resource;
}
return resource;
}

private void saveHitsData(InputFile resource, SMInputCursor lineCursor) throws ParseException, XMLStreamException {
fileMeasuresBuilder.reset();

while (lineCursor.getNext() != null) {
// skip class elements on format 2_3_2
if (isClass(lineCursor)) {
continue;
}
final int lineId = Integer.parseInt(lineCursor.getAttrValue("num"));
String count = lineCursor.getAttrValue("count");
if (StringUtils.isNotBlank(count)) {
fileMeasuresBuilder.setHits(lineId, Integer.parseInt(count));

} else {
int trueCount = (int) ParsingUtils.parseNumber(lineCursor.getAttrValue("truecount"));
int falseCount = (int) ParsingUtils.parseNumber(lineCursor.getAttrValue("falsecount"));
int coveredConditions = 0;
if (trueCount > 0) {
coveredConditions++;

private void saveHitsData(InputFile resource, SMInputCursor lineCursor) throws ParseException, XMLStreamException {
fileMeasuresBuilder.reset();

while (lineCursor.getNext() != null) {
// skip class elements on format 2_3_2
if (isClass(lineCursor)) {
continue;
}
final int lineId = Integer.parseInt(getNullSafeAttributeValue(lineCursor, "num"));
String count = getNullSafeAttributeValue(lineCursor, "count");
if (StringUtils.isNotBlank(count)) {
fileMeasuresBuilder.setHits(lineId, Integer.parseInt(count));

} else {
int trueCount = (int) ParsingUtils.parseNumber(getNullSafeAttributeValue(lineCursor, "truecount"));
int falseCount = (int) ParsingUtils.parseNumber(getNullSafeAttributeValue(lineCursor, "falsecount"));
int coveredConditions = 0;
if (trueCount > 0) {
coveredConditions++;
}
if (falseCount > 0) {
coveredConditions++;
}
fileMeasuresBuilder.setConditions(lineId, 2, coveredConditions);
}
}
if (falseCount > 0) {
coveredConditions++;
if (resource != null) {
for (Measure measure : fileMeasuresBuilder.createMeasures()) {
context.saveMeasure(resource, measure);
}
}
fileMeasuresBuilder.setConditions(lineId, 2, coveredConditions);
}
}
if (resource != null) {
for (Measure measure : fileMeasuresBuilder.createMeasures()) {
context.saveMeasure(resource, measure);
}

/**
* Helper method to ensure that if an non-existing or empty attribute is queried, we don' get exceptions when parsing it as a number.
*
* @param lineCursor
* @param attributeName
* @return
* @throws XMLStreamException
*/
private static String getNullSafeAttributeValue(SMInputCursor lineCursor, String attributeName) throws XMLStreamException {
String value = lineCursor.getAttrValue(attributeName);
if (value == null) {
value = "";
}
return value;
}
}

private static boolean canBeIncludedInFileMetrics(SMInputCursor metricsCursor) throws ParseException, XMLStreamException {
while (metricsCursor.getNext() != null && isClass(metricsCursor)) {
// skip class elements on 1.x xml format
private static boolean canBeIncludedInFileMetrics(SMInputCursor metricsCursor) throws ParseException, XMLStreamException {
while (metricsCursor.getNext() != null && isClass(metricsCursor)) {
// skip class elements on 1.x xml format
}
String elements = getNullSafeAttributeValue(metricsCursor, "elements");
if(StringUtils.isBlank(elements)) {
String statements = getNullSafeAttributeValue(metricsCursor, "statements");
return StringUtils.isNotBlank(statements) && ParsingUtils.parseNumber(statements) > 0;
}
return StringUtils.isNotBlank(elements) && ParsingUtils.parseNumber(elements) > 0;
}
return ParsingUtils.parseNumber(metricsCursor.getAttrValue("elements")) > 0;
}

private static boolean isClass(SMInputCursor cursor) throws XMLStreamException {
return "class".equals(cursor.getLocalName());
}
private static boolean isClass(SMInputCursor cursor) throws XMLStreamException {
return "class".equals(cursor.getLocalName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ public void parse_clover_3_2_2_Format() throws ParseException, URISyntaxExceptio
verify(context).saveMeasure(argThat(new SonarFileMatcher("SampleClass.java")), argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES, 3.0)));
}

@Test
public void parse_karma_clover_Format() throws ParseException, URISyntaxException {
reportParser.collect(TestUtils.getResource(getClass(), "karma_clover.xml"));
verify(context).saveMeasure(argThat(new SonarFileMatcher("test.ts")), argThat(new IsMeasure(CoreMetrics.LINES_TO_COVER, 3.0)));
verify(context).saveMeasure(argThat(new SonarFileMatcher("test.ts")), argThat(new IsMeasure(CoreMetrics.UNCOVERED_LINES, 0.0)));
}

@Test
public void collectFileMeasures() throws Exception {
reportParser.collect(xmlFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1482335568599" clover="3.2.0">
<project timestamp="1482335568599" name="All Files" >
<metrics statements="823" coveredstatements="704" conditionals="203" coveredconditionals="122" methods="231" coveredmethods="184" elements="1257" coveredelements="1010" complexity="0" packages="21" files="35" classes="35" loc="1530" ncloc="1530" />
<package name="src" >
<metrics statements="3" coveredstatements="3" conditionals="0" coveredconditionals="0" methods="1" coveredmethods="1" />
<file name="test.ts" path="/test/app/src/test.ts" >
<metrics statements="3" coveredstatements="3" conditionals="0" coveredconditionals="0" methods="1" coveredmethods="1" />
<line num="1" count="1" type="stmt" />
<line num="2" count="1" type="stmt" />
<line num="4" count="1" type="stmt" />
</file>
</package>
</project>
</coverage>