Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,14 @@ java_library(
"//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:errorprone",
"//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:processing",
"//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/statistics",
"//src/java_tools/junitrunner/java/com/google/testing/coverage:JacocoCoverageLib",
"//src/main/java/com/google/devtools/build/lib/worker:work_request_handlers",
"//third_party:error_prone",
"//third_party:error_prone_annotations",
"//third_party:guava",
"//third_party:jsr305",
"//third_party/java/jacoco:core",
"//third_party/java/jacoco:report",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,50 @@

package com.google.devtools.build.buildjar.instrumentation;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newBufferedReader;
import static java.nio.file.Files.newBufferedWriter;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import com.google.devtools.build.buildjar.InvalidCommandLineException;
import com.google.devtools.build.buildjar.JavaLibraryBuildRequest;
import com.google.devtools.build.buildjar.jarhelper.JarCreator;
import java.io.BufferedInputStream;
import com.google.testing.coverage.BranchCoverageDetail;
import com.google.testing.coverage.BranchDetailAnalyzer;
import com.google.testing.coverage.JacocoLCOVFormatter;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.instr.Instrumenter;
import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator;
import org.jacoco.report.ISourceFileLocator;

/** Instruments compiled java classes using Jacoco instrumentation library. */
/**
* Instruments compiled java classes using Jacoco instrumentation library and optionally analyzes
* them to generate a baseline coverage report.
*/
public final class JacocoInstrumentationProcessor {

public static JacocoInstrumentationProcessor create(List<String> args)
Expand All @@ -45,17 +67,25 @@ public static JacocoInstrumentationProcessor create(List<String> args)
throw new InvalidCommandLineException(
"Number of arguments for Jacoco instrumentation should be 1+ (given "
+ args.size()
+ ": pathsForCoverageFile");
+ ": pathsForCoverageFile [baselineCoverageFile].");
}
Path pathsForCoverageFile = Path.of(args.get(0));
Path baselineCoverageFile = null;
if (args.size() > 1) {
baselineCoverageFile = Path.of(args.get(1));
}

return new JacocoInstrumentationProcessor(args.get(0));
return new JacocoInstrumentationProcessor(pathsForCoverageFile, baselineCoverageFile);
}

private Path instrumentedClassesDirectory;
private final String coverageInformation;
private final Path pathsForCoverageFile;
@Nullable private final Path baselineCoverageFile;

private JacocoInstrumentationProcessor(String coverageInfo) {
this.coverageInformation = coverageInfo;
private JacocoInstrumentationProcessor(
Path pathsForCoverageFile, @Nullable Path baselineCoverageFile) {
this.pathsForCoverageFile = pathsForCoverageFile;
this.baselineCoverageFile = baselineCoverageFile;
}

/**
Expand All @@ -72,7 +102,7 @@ public void processRequest(JavaLibraryBuildRequest build, JarCreator jar) throws
Instrumenter instr = new Instrumenter(new OfflineInstrumentationAccessGenerator());
instrumentRecursively(instr, build.getClassDir());
jar.addDirectory(instrumentedClassesDirectory);
jar.addEntry(coverageInformation, coverageInformation);
jar.addEntry(pathsForCoverageFile.toString(), pathsForCoverageFile);
}

public void cleanup() throws IOException {
Expand All @@ -91,9 +121,14 @@ private static Path getMetadataDirRelativeToJar(Path outputJar) {
* Runs Jacoco instrumentation processor over all .class files recursively, starting with root.
*/
private void instrumentRecursively(Instrumenter instr, Path root) throws IOException {
var emptyExecutionDataStore = new ExecutionDataStore();
var baselineCoverageBuilder = new CoverageBuilder();
var baselineCoverageAnalyzer = new Analyzer(emptyExecutionDataStore, baselineCoverageBuilder);
var baselineBranchDetailAnalyzer = new BranchDetailAnalyzer(emptyExecutionDataStore);

Files.walkFileTree(
root,
new SimpleFileVisitor<Path>() {
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Expand All @@ -115,15 +150,61 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
instrumentedClassesDirectory.resolve(root.relativize(absoluteUninstrumentedCopy));
Files.createDirectories(uninstrumentedCopy.getParent());
Files.copy(file, uninstrumentedCopy);
try (InputStream input =
new BufferedInputStream(Files.newInputStream(uninstrumentedCopy));

byte[] uninstrumentedBytes = Files.readAllBytes(uninstrumentedCopy);
String location = file.toString();
try (InputStream input = new ByteArrayInputStream(uninstrumentedBytes);
OutputStream output =
new BufferedOutputStream(
Files.newOutputStream(instrumentedCopy, TRUNCATE_EXISTING))) {
instr.instrument(input, output, file.toString());
instr.instrument(input, output, location);
}
if (baselineCoverageFile != null) {
baselineCoverageAnalyzer.analyzeClass(uninstrumentedBytes, location);
baselineBranchDetailAnalyzer.analyzeClass(uninstrumentedBytes, location);
}

return FileVisitResult.CONTINUE;
}
});

if (baselineCoverageFile != null) {
generateBaselineCoverageReport(
baselineCoverageFile,
baselineCoverageBuilder.getBundle("isthisevenused"),
baselineBranchDetailAnalyzer.getBranchDetails());
}
}

private void generateBaselineCoverageReport(
Path report, IBundleCoverage bundleCoverage, Map<String, BranchCoverageDetail> branchDetails)
throws IOException {
ImmutableSet<String> execPathsSet;
try (var reader = newBufferedReader(pathsForCoverageFile)) {
execPathsSet = reader.lines().collect(toImmutableSet());
}

var formatter = new JacocoLCOVFormatter(execPathsSet);
try (var writer = new PrintWriter(newBufferedWriter(report, UTF_8, CREATE_NEW))) {
var visitor = formatter.createVisitor(writer, branchDetails);
visitor.visitInfo(ImmutableList.of(), ImmutableList.of());
// Note the API requires a sourceFileLocator because the HTML and XML formatters display a
// page of code annotated with coverage information. Having the source files is not actually
// needed for generating the lcov report.
visitor.visitBundle(
bundleCoverage,
new ISourceFileLocator() {
@Override
public Reader getSourceFile(String packageName, String fileName) {
return null;
}

@Override
public int getTabWidth() {
return 0;
}
});
visitor.visitEnd();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public final class JavaCompilationHelper {
private NestedSet<String> javaBuilderJvmFlags = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
private final JavaSemantics semantics;
private final ImmutableList<Artifact> additionalInputsForDatabinding;
@Nullable private final Artifact baselineCoverageFile;
private boolean enableJspecify = true;
private boolean enableDirectClasspath = true;
private final String execGroup;
Expand All @@ -81,13 +82,15 @@ public JavaCompilationHelper(
ImmutableList<String> javacOpts,
JavaTargetAttributes.Builder attributes,
JavaToolchainProvider javaToolchainProvider,
ImmutableList<Artifact> additionalInputsForDatabinding) {
ImmutableList<Artifact> additionalInputsForDatabinding,
@Nullable Artifact baselineCoverageFile) {
this.ruleContext = ruleContext;
this.javaToolchain = Preconditions.checkNotNull(javaToolchainProvider);
this.attributes = attributes;
this.customJavacOpts = javacOptsInterner.intern(javacOpts);
this.semantics = semantics;
this.additionalInputsForDatabinding = additionalInputsForDatabinding;
this.baselineCoverageFile = baselineCoverageFile;

if (ruleContext.useAutoExecGroups()) {
this.execGroup = semantics.getJavaToolchainType();
Expand Down Expand Up @@ -247,6 +250,7 @@ && getJavaConfiguration().experimentalEnableJspecify()
builder.setTargetLabel(label);
Artifact coverageArtifact = maybeCreateCoverageArtifact(outputs.output());
builder.setCoverageArtifact(coverageArtifact);
builder.setBaselineCoverageFile(baselineCoverageFile);
BootClassPathInfo bootClassPathInfo = getBootclasspathOrDefault();
builder.setBootClassPath(bootClassPathInfo);
NestedSet<Artifact> classpath =
Expand Down Expand Up @@ -386,7 +390,9 @@ public BootClassPathInfo getBootclasspathOrDefault() throws RuleErrorException {
*/
@Nullable
private Artifact maybeCreateCoverageArtifact(Artifact compileJar) {
if (!shouldInstrumentJar()) {
// baselineCoverageFile != null is meant to be equivalent to shouldInstrumentJar(), but we need
// to check both to support older versions of rules_java that do not set baseline_coverage_file.
if (!shouldInstrumentJar() && baselineCoverageFile == null) {
return null;
}
PathFragment packageRelativePath =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ public void extend(ExtraActionInfo.Builder builder, ImmutableList<String> argume
private final JavaToolchainProvider toolchain;
private final String execGroup;
private ImmutableSet<Artifact> additionalOutputs = ImmutableSet.of();
private Artifact coverageArtifact;
@Nullable private Artifact coverageArtifact;
@Nullable private Artifact baselineCoverageFile;
private ImmutableSet<Artifact> sourceFiles = ImmutableSet.of();
private ImmutableList<Artifact> sourceJars = ImmutableList.of();
private StrictDepsMode strictJavaDeps = StrictDepsMode.ERROR;
Expand Down Expand Up @@ -269,7 +270,12 @@ public JavaCompileAction build() throws RuleErrorException, InterruptedException
private ImmutableSet<Artifact> allOutputs() {
ImmutableSet.Builder<Artifact> result =
ImmutableSet.<Artifact>builder().add(outputs.output()).addAll(additionalOutputs);
Stream.of(outputs.depsProto(), outputs.nativeHeader(), genSourceOutput, manifestOutput)
Stream.of(
outputs.depsProto(),
outputs.nativeHeader(),
genSourceOutput,
manifestOutput,
baselineCoverageFile)
.filter(Objects::nonNull)
.forEachOrdered(result::add);
return result.build();
Expand Down Expand Up @@ -326,6 +332,13 @@ private CustomCommandLine buildParamFileContents(ImmutableList<String> javacOpts
if (coverageArtifact != null) {
result.add("--post_processor");
result.addExecPath(JACOCO_INSTRUMENTATION_PROCESSOR, coverageArtifact);
if (baselineCoverageFile != null) {
result.addExecPath(baselineCoverageFile);
}
} else {
Preconditions.checkState(
baselineCoverageFile == null,
"baselineCoverageFile should be null if coverageArtifact is null");
}
return result.build();
}
Expand Down Expand Up @@ -450,11 +463,17 @@ public JavaCompileActionBuilder setJavaBuilder(JavaToolchainTool javaBuilder) {
}

@CanIgnoreReturnValue
public JavaCompileActionBuilder setCoverageArtifact(Artifact coverageArtifact) {
public JavaCompileActionBuilder setCoverageArtifact(@Nullable Artifact coverageArtifact) {
this.coverageArtifact = coverageArtifact;
return this;
}

@CanIgnoreReturnValue
public JavaCompileActionBuilder setBaselineCoverageFile(@Nullable Artifact baselineCoverageFile) {
this.baselineCoverageFile = baselineCoverageFile;
return this;
}

@CanIgnoreReturnValue
public JavaCompileActionBuilder setTargetLabel(Label targetLabel) {
this.targetLabel = targetLabel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ public void createHeaderCompilationAction(
JavaHelper.tokenizeJavaOptions(Depset.cast(javacOpts, String.class, "javac_opts")),
attributesBuilder,
JavaToolchainProvider.wrap(toolchain),
Sequence.cast(additionalInputs, Artifact.class, "additional_inputs")
.getImmutableList());
Sequence.cast(additionalInputs, Artifact.class, "additional_inputs").getImmutableList(),
/* baselineCoverageFile= */ null);
compilationHelper.enableDirectClasspath(enableDirectClasspath);
compilationHelper.createHeaderCompilationAction(
headerJar,
Expand Down Expand Up @@ -207,7 +207,8 @@ public void createCompilationAction(
boolean enableJSpecify,
boolean enableDirectClasspath,
Sequence<?> additionalInputs,
Sequence<?> additionalOutputs)
Sequence<?> additionalOutputs,
Object baselineCoverageFile)
throws EvalException,
TypeException,
RuleErrorException,
Expand Down Expand Up @@ -261,8 +262,8 @@ public void createCompilationAction(
JavaHelper.tokenizeJavaOptions(Depset.cast(javacOpts, String.class, "javac_opts")),
attributesBuilder,
JavaToolchainProvider.wrap(javaToolchain),
Sequence.cast(additionalInputs, Artifact.class, "additional_inputs")
.getImmutableList());
Sequence.cast(additionalInputs, Artifact.class, "additional_inputs").getImmutableList(),
baselineCoverageFile == Starlark.NONE ? null : (Artifact) baselineCoverageFile);
compilationHelper.javaBuilderJvmFlags(
Depset.cast(javaBuilderJvmFlags, String.class, "javabuilder_jvm_flags"));
compilationHelper.enableJspecify(enableJSpecify);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ void createHeaderCompilationAction(
@Param(name = "enable_direct_classpath", defaultValue = "True", named = true),
@Param(name = "additional_inputs", defaultValue = "[]", named = true),
@Param(name = "additional_outputs", defaultValue = "[]", named = true),
@Param(name = "baseline_coverage_file", defaultValue = "None", named = true),
})
void createCompilationAction(
StarlarkRuleContextT ctx,
Expand Down Expand Up @@ -554,7 +555,8 @@ void createCompilationAction(
boolean enableJSpecify,
boolean enableDirectClasspath,
Sequence<?> additionalInputs,
Sequence<?> additionalOutputs)
Sequence<?> additionalOutputs,
Object baselineCoverageFile)
throws EvalException,
TypeException,
RuleErrorException,
Expand Down
25 changes: 25 additions & 0 deletions src/test/shell/bazel/bazel_coverage_java_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,31 @@ LF:6
end_of_record"

assert_coverage_result "$expected_result" "./bazel-out/_coverage/_coverage_report.dat"

local expected_baseline_result="SF:src/main/com/example/Collatz.java
FN:3,com/example/Collatz::<init> ()V
FN:6,com/example/Collatz::getCollatzFinal (I)I
FNDA:0,com/example/Collatz::<init> ()V
FNDA:0,com/example/Collatz::getCollatzFinal (I)I
FNF:2
FNH:0
BRDA:6,0,0,-
BRDA:6,0,1,-
BRDA:9,0,0,-
BRDA:9,0,1,-
BRF:4
BRH:0
DA:3,0
DA:6,0
DA:7,0
DA:9,0
DA:10,0
DA:12,0
LH:0
LF:6
end_of_record"
# TODO(#5716): Enable this check after the next rules_java update.
# assert_coverage_result "$expected_baseline_result" "./bazel-out/_coverage/_baseline_report.dat"
}

function test_java_test_java_import_coverage() {
Expand Down