blob: 2f5acf5f1f3a7204562bea70b9bb137f83ae9b87 [file] [log] [blame]
// Copyright 2016 The Bazel Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.testing.coverage;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.analysis.IClassCoverage;
import org.jacoco.core.analysis.ICounter;
import org.jacoco.core.analysis.IMethodCoverage;
import org.jacoco.core.analysis.IPackageCoverage;
import org.jacoco.core.analysis.ISourceFileCoverage;
import org.jacoco.core.data.ExecutionData;
import org.jacoco.core.data.SessionInfo;
import org.jacoco.report.IReportGroupVisitor;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.ISourceFileLocator;
/**
* Simple lcov formatter to be used with lcov_merger.par.
*
* <p>The lcov format is documented here: http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
*/
public class JacocoLCOVFormatter {
// Exec paths of the uninstrumented files that are being analyzed. This is helpful for files in
// jars passed through java_import or some custom rule where blaze doesn't have enough context to
// compute the right paths, but relies on these pre-computed exec paths.
private final ImmutableSet<String> execPathsOfUninstrumentedFiles;
public JacocoLCOVFormatter(ImmutableSet<String> execPathsOfUninstrumentedFiles) {
this.execPathsOfUninstrumentedFiles = execPathsOfUninstrumentedFiles;
}
public JacocoLCOVFormatter() {
this.execPathsOfUninstrumentedFiles = ImmutableSet.of();
}
public IReportVisitor createVisitor(
final File output, final Map<String, BranchCoverageDetail> branchCoverageDetail) {
return new IReportVisitor() {
private Map<String, Map<String, IClassCoverage>> sourceToClassCoverage = new TreeMap<>();
private Map<String, ISourceFileCoverage> sourceToFileCoverage = new TreeMap<>();
private String getExecPathForEntryName(String fileName) {
if (execPathsOfUninstrumentedFiles.isEmpty()) {
return fileName;
}
for (String execPath : execPathsOfUninstrumentedFiles) {
if (execPath.endsWith("/" + fileName)) {
return execPath;
}
}
return null;
}
@Override
public void visitInfo(List<SessionInfo> sessionInfos, Collection<ExecutionData> executionData)
throws IOException {}
@Override
public void visitEnd() throws IOException {
try (Writer fileWriter = Files.newBufferedWriter(output.toPath(), UTF_8, CREATE, APPEND);
PrintWriter printWriter = new PrintWriter(fileWriter)) {
for (String sourceFile : sourceToClassCoverage.keySet()) {
processSourceFile(printWriter, sourceFile);
}
}
}
@Override
public void visitBundle(IBundleCoverage bundle, ISourceFileLocator locator)
throws IOException {
// Jacoco's API is geared towards HTML/XML reports which have a hierarchical nature. The
// following loop would call the report generators for packages, classes, methods, and
// finally link the source view (which would be generated by walking the actual source file
// and annotating the coverage data). For lcov, we don't really need the source file, but
// we need to output FN/FNDA pairs with method coverage, which means we need to index this
// information and process everything at the end.
for (IPackageCoverage pkgCoverage : bundle.getPackages()) {
for (IClassCoverage clsCoverage : pkgCoverage.getClasses()) {
String fileName = getExecPathForEntryName(
clsCoverage.getPackageName() + "/" + clsCoverage.getSourceFileName());
if (fileName == null) {
continue;
}
if (!sourceToClassCoverage.containsKey(fileName)) {
sourceToClassCoverage.put(fileName, new TreeMap<String, IClassCoverage>());
}
sourceToClassCoverage.get(fileName).put(clsCoverage.getName(), clsCoverage);
}
for (ISourceFileCoverage srcCoverage : pkgCoverage.getSourceFiles()) {
String sourceName = getExecPathForEntryName(
srcCoverage.getPackageName() + "/" + srcCoverage.getName());
if (sourceName != null) {
sourceToFileCoverage.put(sourceName, srcCoverage);
}
}
}
}
@Override
public IReportGroupVisitor visitGroup(String name) throws IOException {
return null;
}
private void processSourceFile(PrintWriter writer, String sourceFile) {
writer.printf("SF:%s\n", sourceFile);
ISourceFileCoverage srcCoverage = sourceToFileCoverage.get(sourceFile);
if (srcCoverage != null) {
// List methods, including methods from nested classes, in FN/FNDA pairs
for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) {
for (IMethodCoverage mthCoverage : clsCoverage.getMethods()) {
String name = constructFunctionName(mthCoverage, clsCoverage.getName());
writer.printf("FN:%d,%s\n", mthCoverage.getFirstLine(), name);
writer.printf("FNDA:%d,%s\n", mthCoverage.getMethodCounter().getCoveredCount(), name);
}
}
for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) {
BranchCoverageDetail detail = branchCoverageDetail.get(clsCoverage.getName());
if (detail != null) {
for (int line : detail.linesWithBranches()) {
int numBranches = detail.getBranches(line);
boolean executed = detail.getExecutedBit(line);
if (executed) {
for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) {
if (detail.getTakenBit(line, branchIdx)) {
writer.printf("BA:%d,%d\n", line, 2); // executed, taken
} else {
writer.printf("BA:%d,%d\n", line, 1); // executed, not taken
}
}
} else {
for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) {
writer.printf("BA:%d,%d\n", line, 0); // not executed
}
}
}
}
}
// List of DA entries matching source lines
int firstLine = srcCoverage.getFirstLine();
int lastLine = srcCoverage.getLastLine();
for (int line = firstLine; line <= lastLine; line++) {
ICounter instructionCounter = srcCoverage.getLine(line).getInstructionCounter();
if (instructionCounter.getTotalCount() != 0) {
writer.printf("DA:%d,%d\n", line, instructionCounter.getCoveredCount());
}
}
}
writer.println("end_of_record");
}
private String constructFunctionName(IMethodCoverage mthCoverage, String clsName) {
// The lcov spec doesn't of course cover Java formats, so we output the method signature.
// lcov_merger doesn't seem to care about these entries.
return clsName + "::" + mthCoverage.getName() + " " + mthCoverage.getDesc();
}
};
}
}