Skip to content
Draft
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
12 changes: 12 additions & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ coverage_validation_post_shell_commands: &coverage_validation_post_shell_command
bazel-out/_coverage/_coverage_report.dat \
; exit 1 \
; }
# Regression check: lines reached only by a binary the test runs as a
# subprocess must be reported.
- |
awk '/^SF:test\/coverage_subprocess\/greeter\.rs$/,/^end_of_record$/' \
bazel-out/_coverage/_coverage_report.dat \
| grep -qE '^LH:[1-9]' \
|| { 1>&2 echo "No coverage for a subprocess binary (test/coverage_subprocess/greeter.rs)" \
; 1>&2 echo "Counters from binaries in a test's data were dropped." \
; 1>&2 awk '/^SF:test\/coverage_subprocess\/greeter\.rs$/,/^end_of_record$/' \
bazel-out/_coverage/_coverage_report.dat \
; exit 1 \
; }
split_coverage_postprocessing_shell_commands: &split_coverage_postprocessing_shell_commands
- echo "coverage --experimental_fetch_all_coverage_outputs" >> user.bazelrc
- echo "coverage --experimental_split_coverage_postprocessing" >> user.bazelrc
Expand Down
21 changes: 21 additions & 0 deletions rust/coverage/collect_rust_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
//! - `RUNFILES_DIR` (optional): Location of the test's runfiles. Not set in split
//! coverage postprocessing mode (`--experimental_split_coverage_postprocessing`).
//! - `TEST_BINARY`: Runfiles-relative path to the test binary (used when `RUNFILES_DIR` is absent).
//! - `RUST_COVERAGE_OBJECTS` (optional): Instrumented binaries the test runs as
//! subprocesses, separated by the host path separator. Each is resolved like
//! `RUST_LLVM_COV`.
//! - `VERBOSE_COVERAGE`: Print debug info from the coverage scripts
//!
//! The script looks in $COVERAGE_DIR for the Rust metadata coverage files
//! (profraw) and uses lcov to get the coverage data. The coverage data
//! is placed in $COVERAGE_DIR as a `coverage.dat` file.

use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -152,6 +156,18 @@ fn main() {
test_binary
}
};
let coverage_objects: Vec<PathBuf> = env::var("RUST_COVERAGE_OBJECTS")
.map(|objects| {
env::split_paths(&objects)
.map(|object| match runfiles_dir {
Some(ref rd) => find_metadata_file(&execroot, rd, &object.to_string_lossy()),
None => execroot.join(object),
})
.collect()
})
.unwrap_or_default();
debug_log!("Additional coverage objects: {:#?}", coverage_objects);

let profraw_files: Vec<PathBuf> = fs::read_dir(coverage_dir)
.unwrap()
.flatten()
Expand Down Expand Up @@ -203,6 +219,11 @@ fn main() {
.arg("-ignore-filename-regex=/tmp/.+")
.arg(format!("-path-equivalence=.,{}", execroot.display()))
.arg(test_binary)
.args(
coverage_objects
.iter()
.flat_map(|object| [OsStr::new("-object"), object.as_os_str()]),
)
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped());

Expand Down
19 changes: 19 additions & 0 deletions rust/private/rust.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,25 @@ def _rust_test_impl(ctx):
# must set them explicitly.
env["GENERATE_LLVM_LCOV"] = "1"
env["CC_CODE_COVERAGE_SCRIPT"] = ctx.executable._collect_cc_coverage.path

# Binaries the test runs as subprocesses write their `.profraw` beside
# the test's own, but `llvm-cov export` reads counters only for the
# objects it is given. Name them so the collector can pass them along.
coverage_objects = []
for dep in data:
if rust_common.crate_info not in dep or not ctx.coverage_instrumented(dep):
continue
dep_crate_info = dep[rust_common.crate_info]
if dep_crate_info.type != "bin":
continue
if toolchain._experimental_use_coverage_metadata_files:
coverage_objects.append(dep_crate_info.output.path)
elif dep_crate_info.output.short_path.startswith("../"):
coverage_objects.append(dep_crate_info.output.short_path[len("../"):])
else:
coverage_objects.append("{}/{}".format(ctx.workspace_name, dep_crate_info.output.short_path))
if coverage_objects:
env["RUST_COVERAGE_OBJECTS"] = ctx.configuration.host_path_separator.join(coverage_objects)
components = "{}/{}".format(ctx.label.workspace_root, ctx.label.package).split("/")
env["CARGO_MANIFEST_DIR"] = "/".join([c for c in components if c])
providers.append(RunEnvironmentInfo(
Expand Down
2 changes: 1 addition & 1 deletion rust/private/rustc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2243,7 +2243,7 @@ def rustc_compile_action(
executable = crate_info.output if crate_info.type == "bin" or crate_info.is_test else None

instrumented_files_kwargs = {
"dependency_attributes": ["deps", "crate"],
"dependency_attributes": ["deps", "crate", "data"],
"extensions": ["rs"],
"source_attributes": ["srcs"],
}
Expand Down
16 changes: 16 additions & 0 deletions test/coverage_subprocess/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//rust:defs.bzl", "rust_binary", "rust_test")

rust_binary(
name = "greeter",
srcs = ["greeter.rs"],
edition = "2021",
)

# Runs `:greeter` as a subprocess. Under `bazel coverage` the lines of
# `greeter.rs` are reached only through that subprocess.
rust_test(
name = "greeter_test",
srcs = ["greeter_test.rs"],
data = [":greeter"],
edition = "2021",
)
12 changes: 12 additions & 0 deletions test/coverage_subprocess/greeter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! A binary whose coverage is reached only by a test that spawns it.

fn greeting(name: &str) -> String {
format!("hello, {name}")
}

fn main() {
let name = std::env::args()
.nth(1)
.unwrap_or_else(|| "world".to_owned());
println!("{}", greeting(&name));
}
13 changes: 13 additions & 0 deletions test/coverage_subprocess/greeter_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Runs `greeter` through `CARGO_BIN_EXE_greeter`.

use std::process::Command;

#[test]
fn greeter_greets() {
let output = Command::new(env!("CARGO_BIN_EXE_greeter"))
.arg("bazel")
.output()
.expect("failed to run greeter");
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "hello, bazel\n");
}