/*
* Pretty heavily modified version of the existing SonarQube LCOV parser implementation
*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-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 com.pablissimo.sonar;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.coverage.CoverageType;
import org.sonar.api.batch.sensor.coverage.NewCoverage;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
/**
* http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
*/
public class LCOVParserImpl implements LCOVParser {
private static final String SF = "SF:";
private static final String DA = "DA:";
private static final String BRDA = "BRDA:";
private final Map<InputFile, NewCoverage> coverageByFile;
private final SensorContext context;
private final List<String> unresolvedPaths = new ArrayList<>();
private static final Logger LOG = Loggers.get(LCOVParser.class);
private LCOVParserImpl(List<String> lines, SensorContext context) {
this.context = context;
this.coverageByFile = parse(lines);
}
static LCOVParser create(SensorContext context, File... files) {
final List<String> lines = new LinkedList<>();
for(File file: files) {
try {
lines.addAll(Files.lines(file.toPath()).collect(Collectors.<String>toList()));
} catch (IOException e) {
throw new IllegalArgumentException("Could not read content from file: " + file, e);
}
}
return new LCOVParserImpl(lines, context);
}
Map<InputFile, NewCoverage> coverageByFile() {
return coverageByFile;
}
List<String> unresolvedPaths() {
return unresolvedPaths;
}
@Override
public Map<InputFile, NewCoverage> parseFile(File file) {
final List<String> lines;
try
{
lines = Files.readAllLines(file.toPath());
}
catch (IOException e)
{
throw new IllegalArgumentException("Could not read content from file: " + file, e);
}
return parse(lines);
}
@Override
public Map<InputFile, NewCoverage> parse(List<String> lines) {
final Map<InputFile, FileData> files = new HashMap<>();
FileData fileData = null;
for (String line : lines) {
if (line.startsWith(SF)) {
// SF:<absolute path to the source file>
fileData = loadCurrentFileData(files, line);
} else if (fileData != null) {
if (line.startsWith(DA)) {
// DA:<line number>,<execution count>[,<checksum>]
String execution = line.substring(DA.length());
String executionCount = execution.substring(execution.indexOf(',') + 1);
String lineNumber = execution.substring(0, execution.indexOf(','));
try {
fileData.addLine(Integer.valueOf(lineNumber), Integer.valueOf(executionCount));
} catch (IllegalArgumentException e) {
logWrongDataWarning("DA", lineNumber, e);
}
} else if (line.startsWith(BRDA)) {
// BRDA:<line number>,<block number>,<branch number>,<taken>
String[] tokens = line.substring(BRDA.length()).trim().split(",");
String lineNumber = tokens[0];
String branchNumber = tokens[1] + tokens[2];
String taken = tokens[3];
try {
fileData.addBranch(Integer.valueOf(lineNumber), branchNumber, "-".equals(taken) ? 0 : Integer.valueOf(taken));
} catch (IllegalArgumentException e) {
logWrongDataWarning("BRDA", lineNumber, e);
}
}
}
}
Map<InputFile, NewCoverage> coveredFiles = new HashMap<>();
for (Map.Entry<InputFile, FileData> e : files.entrySet()) {
NewCoverage newCoverage = context.newCoverage().onFile(e.getKey()).ofType(CoverageType.UNIT);
e.getValue().save(newCoverage);
coveredFiles.put(e.getKey(), newCoverage);
}
return coveredFiles;
}
private static void logWrongDataWarning(String dataType, String lineNumber, IllegalArgumentException e) {
LOG.warn(String.format("Problem during processing LCOV report: can't save %s data for line %s (%s).", dataType, lineNumber, e.getMessage()));
}
@CheckForNull
private FileData loadCurrentFileData(final Map<InputFile, FileData> files, String line) {
String filePath = line.substring(SF.length());
FileData fileData = null;
// some tools (like Istanbul, Karma) provide relative paths, so let's consider them relative to project directory
InputFile inputFile = null;
try {
Paths.get(filePath);
inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath));
}
catch (InvalidPathException ex) {
LOG.warn("LCOV file referred to path that appears invalid (not just not on disk): " + filePath, ex);
}
// Try to accommodate Angular projects that, when the angular template loader's used
// by checking for a ! in the filepath if the path isn't found - have a bash at seeking
// everything after the last ! as a second fallback pass
if (inputFile == null && filePath.contains("!") && (filePath.lastIndexOf('!') + 1) < filePath.length()) {
String amendedPath = filePath.substring(filePath.lastIndexOf('!') + 1);
LOG.debug("Failed to resolve " + filePath + " as a valid source file, so attempting " + amendedPath + " instead");
inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(amendedPath));
}
if (inputFile != null) {
fileData = files.get(inputFile);
if (fileData == null) {
fileData = new FileData(inputFile);
files.put(inputFile, fileData);
}
} else {
LOG.debug("Failed to resolve path " + filePath + " to a file in the analysis set");
unresolvedPaths.add(filePath);
}
return fileData;
}
private static class FileData {
/**
* line number -> branch number -> taken
*/
private Map<Integer, Map<String, Integer>> branches = new HashMap<>();
/**
* line number -> execution count
*/
private Map<Integer, Integer> hits = new HashMap<>();
/**
* Number of lines in the file
* Required to check if line exist in a file, see {@link #checkLine(Integer)}
*/
private final int linesInFile;
private final String filename;
private static final String WRONG_LINE_EXCEPTION_MESSAGE = "Line with number %s doesn't belong to file %s";
FileData(InputFile inputFile) {
linesInFile = inputFile.lines();
filename = inputFile.relativePath();
}
void addBranch(Integer lineNumber, String branchNumber, Integer taken) {
checkLine(lineNumber);
Map<String, Integer> branchesForLine = branches.get(lineNumber);
if (branchesForLine == null) {
branchesForLine = new HashMap<String, Integer>();
branches.put(lineNumber, branchesForLine);
}
Integer currentValue = branchesForLine.get(branchNumber);
branchesForLine.put(branchNumber, (currentValue == null ? 0 : currentValue) + taken);
}
void addLine(Integer lineNumber, Integer executionCount) {
checkLine(lineNumber);
Integer currentValue = hits.get(lineNumber);
if (currentValue == null) {
currentValue = 0;
}
hits.put(lineNumber, currentValue + executionCount);
}
void save(NewCoverage newCoverage) {
for (Map.Entry<Integer, Integer> e : hits.entrySet()) {
newCoverage.lineHits(e.getKey(), e.getValue());
}
for (Map.Entry<Integer, Map<String, Integer>> e : branches.entrySet()) {
int conditions = e.getValue().size();
int covered = 0;
for (Integer taken : e.getValue().values()) {
if (taken > 0) {
covered++;
}
}
newCoverage.conditions(e.getKey(), conditions, covered);
}
}
private void checkLine(Integer lineNumber) {
if (lineNumber < 1 || lineNumber > linesInFile) {
throw new IllegalArgumentException(String.format(WRONG_LINE_EXCEPTION_MESSAGE, lineNumber, filename));
}
}
}
}