org.sonar.plugins.dotnet.tests.OpenCoverReportParser Maven / Gradle / Ivy
The newest version!
/*
* SonarSource :: .NET :: Shared library
* Copyright (C) 2014-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plugins.dotnet.tests;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.sonarsource.dotnet.shared.CallableUtils.lazy;
public class OpenCoverReportParser implements CoverageParser {
private static final Logger LOG = LoggerFactory.getLogger(OpenCoverReportParser.class);
private final FileService fileService;
OpenCoverReportParser(FileService fileService) {
this.fileService = fileService;
}
@Override
public void accept(File file, Coverage coverage) {
LOG.debug("The current user dir is '{}'.", lazy(() -> System.getProperty("user.dir")));
LOG.info("Parsing the OpenCover report {}", file.getAbsolutePath());
new Parser(file, coverage).parse();
}
private class Parser {
private final File file;
private final Coverage coverage;
// the key is the file ID, the value is the CoveredFile
private final Map files = new HashMap<>();
// The "uid" attribute of the "FileRef" XML tag.
// This is needed when the "SequencePoint" or "BranchPoint" XML tags do not contain the "fileid" attribute.
private String currentFileRefUid;
Parser(File file, Coverage coverage) {
this.file = file;
this.coverage = coverage;
}
public void parse() {
try (XmlParserHelper xmlParserHelper = new XmlParserHelper(file)) {
xmlParserHelper.checkRootTag("CoverageSession");
dispatchTags(xmlParserHelper);
} catch (IOException e) {
throw new IllegalStateException("Unable to close report", e);
}
}
private void dispatchTags(XmlParserHelper xmlParserHelper) {
String tagName;
while ((tagName = xmlParserHelper.nextStartTag()) != null) {
if ("File".equals(tagName)) {
handleFileTag(xmlParserHelper);
} else if ("FileRef".equals(tagName)) {
handleFileRef(xmlParserHelper);
} else if ("SequencePoint".equals(tagName)) {
handleSequencePointTag(xmlParserHelper);
} else if ("BranchPoint".equals(tagName)) {
handleBranchPointTag(xmlParserHelper);
}
}
}
private void handleFileRef(XmlParserHelper xmlParserHelper) {
this.currentFileRefUid = xmlParserHelper.getRequiredAttribute("uid");
}
private void handleFileTag(XmlParserHelper xmlParserHelper) {
String uid = xmlParserHelper.getRequiredAttribute("uid");
String pathInCoverageFile = xmlParserHelper.getRequiredAttribute("fullPath");
String canonicalPath;
try {
canonicalPath = new File(pathInCoverageFile).getCanonicalPath();
} catch (IOException e) {
LOG.debug("Skipping the import of OpenCover code coverage for the invalid file path: " + pathInCoverageFile
+ " at line " + xmlParserHelper.stream().getLocation().getLineNumber(), e);
return;
}
CoveredFile coveredFile = createCoveredFile(canonicalPath, uid, pathInCoverageFile);
LOG.debug("CoveredFile created: {}.", coveredFile);
files.put(uid, coveredFile);
}
private void handleSequencePointTag(XmlParserHelper xmlParserHelper) {
// Open Cover lacks model documentation but the details can be found in the source code:
// https://github.com/OpenCover/opencover/blob/4.7.922/main/OpenCover.Framework/Model/SequencePoint.cs
int line = xmlParserHelper.getRequiredIntAttribute("sl");
int visitCount = xmlParserHelper.getRequiredIntAttribute("vc");
String fileId = xmlParserHelper.getAttribute("fileid");
if (fileId == null) {
fileId = currentFileRefUid;
}
if (files.containsKey(fileId)) {
CoveredFile coveredFile = files.get(fileId);
if (coveredFile.isIndexed()) {
coverage.addHits(coveredFile.indexedPath, line, visitCount);
LOG.trace("OpenCover parser: add hits for file {}, line '{}', visitCount '{}'.",
coveredFile, line, visitCount);
} else {
LOG.debug("Skipping the file {}, line '{}', visitCount '{}' because file is not indexed or does not have the supported language. "
+ VERIFY_SONARPROJECTPROPERTIES_MESSAGE,
coveredFile, line, visitCount);
}
} else {
LOG.debug("OpenCover parser (handleSequencePointTag): the fileId '{}' key is not contained in files (entry for line '{}', visitCount '{}').",
fileId, line, visitCount);
}
}
private void handleBranchPointTag(XmlParserHelper xmlParserHelper) {
String fileId = xmlParserHelper.getAttribute("fileid");
if (fileId == null) {
fileId = currentFileRefUid;
}
if (files.containsKey(fileId)) {
CoveredFile coveredFile = files.get(fileId);
int line = xmlParserHelper.getIntAttributeOrZero("sl");
if (line == 0){
LOG.warn("OpenCover parser: invalid start line for file {}.", coveredFile);
return;
}
int offset = xmlParserHelper.getRequiredIntAttribute("offset");
int offsetEnd = xmlParserHelper.getRequiredIntAttribute("offsetend");
int path = xmlParserHelper.getRequiredIntAttribute("path");
int visitCount = xmlParserHelper.getRequiredIntAttribute("vc");
if (coveredFile.isIndexed()) {
coverage.add(new BranchPoint(coveredFile.indexedPath, line, offset, offsetEnd, path, visitCount, file.getPath()));
LOG.trace("OpenCover parser: add branch hits for file {}, line '{}', offset '{}', visitCount '{}'.",
coveredFile, line, offset, visitCount);
} else {
LOG.debug("OpenCover parser: Skipping branch hits for file {}, line '{}', offset '{}', visitCount '{}' because file" +
" is not indexed or does not have the supported language. " + VERIFY_SONARPROJECTPROPERTIES_MESSAGE,
coveredFile, line, offset, visitCount);
}
} else {
LOG.debug("OpenCover parser (handleBranchPointTag): the fileId '{}' key is not contained in files.", fileId);
}
}
private CoveredFile createCoveredFile(String canonicalPath, String uid, String pathInCoverageFile){
if (fileService.isSupportedAbsolute(canonicalPath)) {
return new CoveredFile(uid, pathInCoverageFile, canonicalPath);
} else {
// maybe it's a deterministic build file path
return fileService.getAbsolutePath(pathInCoverageFile)
.map(path -> new CoveredFile(uid, pathInCoverageFile, path))
.orElseGet(() -> new CoveredFile(uid, pathInCoverageFile, null));
}
}
}
/**
* Represents a file mentioned in the OpenCover report list of files, which may or may not be supported by the FileService.
* @see FileService
*/
private static class CoveredFile {
// the file ID from the OpenCover report
final String uid;
// the path from the coverage file
final String originalPath;
// the path which is indexed by the scanner
final String indexedPath;
CoveredFile(String uid, String originalPath, @Nullable String indexedPath) {
this.uid = uid;
this.originalPath = originalPath;
this.indexedPath = indexedPath;
}
/**
* @return True if indexedPath is supported/resolved by the FileService, which means it's indexed by the Scanner.
* @see FileService#isSupportedAbsolute(String)
* @see FileService#getAbsolutePath(String)
*/
boolean isIndexed() {
return indexedPath != null;
}
@Override
public String toString() {
return indexedPath == null
? String.format("(ID '%s', path '%s', NO INDEXED PATH)", uid, originalPath)
: String.format("(ID '%s', path '%s', indexed as '%s')", uid, originalPath, indexedPath);
}
}
}