Skip to content
54 changes: 54 additions & 0 deletions rust/private/rust.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,39 @@ def _rust_test_impl(ctx):
env["CC_CODE_COVERAGE_SCRIPT"] = ctx.executable._collect_cc_coverage.path
components = "{}/{}".format(ctx.label.workspace_root, ctx.label.package).split("/")
env["CARGO_MANIFEST_DIR"] = "/".join([c for c in components if c])

if ctx.attr.junit:
test_bin_short = output.short_path
if test_bin_short.startswith("../"):
rust_test_bin_rloc = test_bin_short[len("../"):]
else:
rust_test_bin_rloc = ctx.workspace_name + "/" + test_bin_short
env["RUST_TEST_BIN"] = rust_test_bin_rloc

# RUST_TEST_BIN above is a runfiles path and only resolves against
# RUNFILES_DIR. Coverage postprocessing may run with RUNFILES_DIR
# unset (--experimental_split_coverage_postprocessing), so also hand
# over the binary's execroot-relative path directly instead of making
# collect_coverage reconstruct it.
env["RUST_TEST_BIN_EXECROOT_PATH"] = output.path

junit_runner = ctx.actions.declare_file(ctx.label.name + "_junit_runner" + toolchain.binary_ext)
ctx.actions.symlink(
output = junit_runner,
target_file = ctx.executable._junit_runner,
is_executable = True,
)

original_default_info = providers[0]
runner_runfiles = ctx.attr._junit_runner[DefaultInfo].default_runfiles
test_bin_runfiles = ctx.runfiles(files = [output])
merged_runfiles = original_default_info.default_runfiles.merge(runner_runfiles).merge(test_bin_runfiles)
providers[0] = DefaultInfo(
files = original_default_info.files,
runfiles = merged_runfiles,
executable = junit_runner,
)

providers.append(RunEnvironmentInfo(
environment = env,
inherited_environment = ctx.attr.env_inherit,
Expand Down Expand Up @@ -996,6 +1029,27 @@ _RUST_TEST_ATTRS = {
E.g. `bazel test //src:rust_test --test_arg=foo::test::test_fn`.
"""),
),
"junit": attr.bool(
default = True,
doc = dedent("""\
If True (default), wrap the test binary with a JUnit XML runner that
parses libtest output and writes JUnit XML to `$XML_OUTPUT_FILE` when
run under `bazel test`. When `$XML_OUTPUT_FILE` is not set (e.g.
`bazel run`), the runner execs the test binary directly with zero
overhead. Set to False to bypass the wrapper entirely (useful for
debugging or attaching a debugger).
"""),
),
"_junit_runner": attr.label(
default = Label("//util/junit_runner"),
executable = True,
# Built for the exec platform, like the other test-support tools
# (process_wrapper, collect_coverage). This keeps the runner off the
# target configuration, so it doesn't inherit target-only settings such
# as a custom #[global_allocator] or cc_common.link, which it has no way
# to satisfy and which would otherwise fail to link.
cfg = "exec",
),
} | _COVERAGE_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS

rust_library = rule(
Expand Down
6 changes: 6 additions & 0 deletions rust/runfiles/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ rust_test(
name = "runfiles_test",
crate = ":runfiles",
data = ["data/sample.txt"],
# These tests inspect the process's own runfiles -- the manifest layout and
# the repo mapping that rlocation! resolves against. The junit wrapper runs
# the test as a child under the wrapper's runfiles, which is a different
# ambient environment than the one being asserted on, so the runfiles
# library's own tests need to run unwrapped.
junit = False,
)

rust_doc(
Expand Down
14 changes: 14 additions & 0 deletions test/junit/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("//rust:defs.bzl", "rust_test")

rust_test(
name = "junit_test",
srcs = ["lib.rs"],
edition = "2021",
)

rust_test(
name = "no_junit_test",
srcs = ["lib.rs"],
edition = "2021",
junit = False,
)
11 changes: 11 additions & 0 deletions test/junit/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[cfg(test)]
mod tests {
#[test]
fn test_passing() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn test_ignored() {}
}
63 changes: 51 additions & 12 deletions util/collect_coverage/collect_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ fn main() {
None => debug_log!("RUNFILES_DIR: not set (split coverage postprocessing)"),
}

let coverage_output_file = coverage_dir.join("coverage.dat");
let coverage_output_file = env::var("COVERAGE_OUTPUT_FILE")
.map(PathBuf::from)
.unwrap_or_else(|_| coverage_dir.join("coverage.dat"));
let profdata_file = coverage_dir.join("coverage.profdata");
let llvm_cov_path = env::var("RUST_LLVM_COV").unwrap();
let llvm_profdata_path = env::var("RUST_LLVM_PROFDATA").unwrap();
Expand All @@ -141,15 +143,50 @@ fn main() {
Some(ref rd) => find_metadata_file(&execroot, rd, &llvm_profdata_path),
None => execroot.join(&llvm_profdata_path),
};
let test_binary = match runfiles_dir {
Some(ref rd) => find_test_binary(&execroot, rd),
None => {
let bin_dir = config_bin_dir(&execroot, &coverage_dir);
let test_binary = execroot
.join(bin_dir)
.join(env::var("TEST_BINARY").unwrap());
debug_log!("Resolved TEST_BINARY to: {}", test_binary.display());
test_binary
// When the JUnit runner wraps the test, TEST_BINARY points to the runner
// and RUST_TEST_BIN holds the actual instrumented binary that llvm-cov needs.
//
// RUST_TEST_BIN is a runfiles path (workspace-prefixed) and only resolves
// against RUNFILES_DIR. When RUNFILES_DIR is gone -- Bazel drops it under
// --experimental_split_coverage_postprocessing -- we need the binary's
// location relative to the execroot instead. Rather than try to rebuild
// that here by stitching the workspace-prefixed runfiles path onto the
// bin dir (which mislays the workspace segment and points at a file that
// isn't there), read RUST_TEST_BIN_EXECROOT_PATH, which the test rule
// already knows exactly.
let test_binary = if let Ok(rust_test_bin) = env::var("RUST_TEST_BIN") {
debug_log!("Using RUST_TEST_BIN: {}", rust_test_bin);
let execroot_path = || {
env::var("RUST_TEST_BIN_EXECROOT_PATH")
.map(|p| execroot.join(p))
.unwrap_or_else(|_| {
let bin_dir = config_bin_dir(&execroot, &coverage_dir);
execroot.join(bin_dir).join(&rust_test_bin)
})
};
match runfiles_dir {
Some(ref rd) => {
let candidate = rd.join(&rust_test_bin);
if candidate.exists() {
candidate
} else {
debug_log!("RUST_TEST_BIN missing under RUNFILES_DIR; using execroot path");
execroot_path()
}
}
None => execroot_path(),
}
} else {
match runfiles_dir {
Some(ref rd) => find_test_binary(&execroot, rd),
None => {
let bin_dir = config_bin_dir(&execroot, &coverage_dir);
let test_binary = execroot
.join(bin_dir)
.join(env::var("TEST_BINARY").unwrap());
debug_log!("Resolved TEST_BINARY to: {}", test_binary.display());
test_binary
}
}
};
let profraw_files: Vec<PathBuf> = fs::read_dir(coverage_dir)
Expand Down Expand Up @@ -199,7 +236,8 @@ fn main() {
.arg("-format=lcov")
.arg("-instr-profile")
.arg(&profdata_file)
.arg("-ignore-filename-regex=.*external/.+")
.arg(r"-ignore-filename-regex=.*external[/\\].+")
.arg(r"-ignore-filename-regex=.*rustc[/\\].+")
.arg("-ignore-filename-regex=/tmp/.+")
.arg(format!("-path-equivalence=.,{}", execroot.display()))
.arg(test_binary)
Expand Down Expand Up @@ -234,7 +272,8 @@ fn main() {
coverage_output_file,
report_str
.replace("#/proc/self/cwd/", "")
.replace(&execroot.display().to_string(), ""),
.replace(&execroot.display().to_string(), "")
.replace('\\', "/"),
)
.unwrap();

Expand Down
20 changes: 20 additions & 0 deletions util/junit_runner/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("//rust:defs.bzl", "rust_binary", "rust_test")

rust_binary(
name = "junit_runner",
srcs = ["junit_runner.rs"],
edition = "2021",
rustc_flags = select({
"@platforms//os:linux": ["-Cstrip=debuginfo"],
"@platforms//os:macos": ["-Cstrip=symbols"],
"//conditions:default": [],
}),
visibility = ["//visibility:public"],
)

rust_test(
name = "junit_runner_test",
srcs = ["junit_runner.rs"],
edition = "2021",
junit = False,
)
Loading