diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 60c7448389..2718cc6683 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -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 diff --git a/rust/coverage/collect_rust_coverage.rs b/rust/coverage/collect_rust_coverage.rs index 7fbe08a35b..b41ef2ca82 100644 --- a/rust/coverage/collect_rust_coverage.rs +++ b/rust/coverage/collect_rust_coverage.rs @@ -15,6 +15,9 @@ //! - `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 @@ -22,6 +25,7 @@ //! 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; @@ -152,6 +156,18 @@ fn main() { test_binary } }; + let coverage_objects: Vec = 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 = fs::read_dir(coverage_dir) .unwrap() .flatten() @@ -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()); diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index d150068d9e..16a673bc85 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -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( diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 4b24462e0b..37fe04dbc2 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -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"], } diff --git a/test/coverage_subprocess/BUILD.bazel b/test/coverage_subprocess/BUILD.bazel new file mode 100644 index 0000000000..57c68169ef --- /dev/null +++ b/test/coverage_subprocess/BUILD.bazel @@ -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", +) diff --git a/test/coverage_subprocess/greeter.rs b/test/coverage_subprocess/greeter.rs new file mode 100644 index 0000000000..15a1c5ac86 --- /dev/null +++ b/test/coverage_subprocess/greeter.rs @@ -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)); +} diff --git a/test/coverage_subprocess/greeter_test.rs b/test/coverage_subprocess/greeter_test.rs new file mode 100644 index 0000000000..433933b1e5 --- /dev/null +++ b/test/coverage_subprocess/greeter_test.rs @@ -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"); +}