diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD index 4dac359e46cd0e..a78adfaf0abccd 100644 --- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD @@ -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", ], ) diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/instrumentation/JacocoInstrumentationProcessor.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/instrumentation/JacocoInstrumentationProcessor.java index c726d5672d9aa6..adf03a4eb11690 100644 --- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/instrumentation/JacocoInstrumentationProcessor.java +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/instrumentation/JacocoInstrumentationProcessor.java @@ -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 args) @@ -45,17 +67,25 @@ public static JacocoInstrumentationProcessor create(List 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; } /** @@ -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 { @@ -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() { + new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { @@ -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 branchDetails) + throws IOException { + ImmutableSet 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(); + } } } diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java index ca79fca1d65787..634270f63d41ed 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java +++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java @@ -71,6 +71,7 @@ public final class JavaCompilationHelper { private NestedSet javaBuilderJvmFlags = NestedSetBuilder.emptySet(Order.STABLE_ORDER); private final JavaSemantics semantics; private final ImmutableList additionalInputsForDatabinding; + @Nullable private final Artifact baselineCoverageFile; private boolean enableJspecify = true; private boolean enableDirectClasspath = true; private final String execGroup; @@ -81,13 +82,15 @@ public JavaCompilationHelper( ImmutableList javacOpts, JavaTargetAttributes.Builder attributes, JavaToolchainProvider javaToolchainProvider, - ImmutableList additionalInputsForDatabinding) { + ImmutableList 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(); @@ -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 classpath = @@ -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 = diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java index 2b3ed34c9270fa..eaf3d84e5cd95b 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileActionBuilder.java @@ -136,7 +136,8 @@ public void extend(ExtraActionInfo.Builder builder, ImmutableList argume private final JavaToolchainProvider toolchain; private final String execGroup; private ImmutableSet additionalOutputs = ImmutableSet.of(); - private Artifact coverageArtifact; + @Nullable private Artifact coverageArtifact; + @Nullable private Artifact baselineCoverageFile; private ImmutableSet sourceFiles = ImmutableSet.of(); private ImmutableList sourceJars = ImmutableList.of(); private StrictDepsMode strictJavaDeps = StrictDepsMode.ERROR; @@ -269,7 +270,12 @@ public JavaCompileAction build() throws RuleErrorException, InterruptedException private ImmutableSet allOutputs() { ImmutableSet.Builder result = ImmutableSet.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(); @@ -326,6 +332,13 @@ private CustomCommandLine buildParamFileContents(ImmutableList 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(); } @@ -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; diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java index 61e5aa095da5dd..bd7b7e0a31a7e7 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java +++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaStarlarkCommon.java @@ -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, @@ -207,7 +207,8 @@ public void createCompilationAction( boolean enableJSpecify, boolean enableDirectClasspath, Sequence additionalInputs, - Sequence additionalOutputs) + Sequence additionalOutputs, + Object baselineCoverageFile) throws EvalException, TypeException, RuleErrorException, @@ -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); diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java index 68a7d317137d62..3b8a04db2eef92 100644 --- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java +++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/java/JavaCommonApi.java @@ -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, @@ -554,7 +555,8 @@ void createCompilationAction( boolean enableJSpecify, boolean enableDirectClasspath, Sequence additionalInputs, - Sequence additionalOutputs) + Sequence additionalOutputs, + Object baselineCoverageFile) throws EvalException, TypeException, RuleErrorException, diff --git a/src/test/shell/bazel/bazel_coverage_java_test.sh b/src/test/shell/bazel/bazel_coverage_java_test.sh index d6b7c5526482e9..2a161d4ea526f2 100755 --- a/src/test/shell/bazel/bazel_coverage_java_test.sh +++ b/src/test/shell/bazel/bazel_coverage_java_test.sh @@ -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:: ()V +FN:6,com/example/Collatz::getCollatzFinal (I)I +FNDA:0,com/example/Collatz:: ()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() {