Skip to content

Commit b62b281

Browse files
authored
Fix coverage for crates with mixed sources (bazelbuild#4106)
An expansion of bazelbuild#4079 with some added testing. closes bazelbuild#4079
1 parent 39a22fe commit b62b281

5 files changed

Lines changed: 179 additions & 26 deletions

File tree

.bazelci/presubmit.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ coverage_validation_post_shell_commands: &coverage_validation_post_shell_command
7171
; 1>&2 head -50 bazel-out/_coverage/_coverage_report.dat \
7272
; exit 1 \
7373
; }
74+
# Regression check: mixed in-tree/generated-source crates must produce
75+
# real coverage, not just the zero-LF baseline record.
76+
- |
77+
awk '/^SF:test\/generated_inputs\/lib\.rs$/,/^end_of_record$/' \
78+
bazel-out/_coverage/_coverage_report.dat \
79+
| grep -qE '^LF:[1-9]' \
80+
|| { 1>&2 echo "No coverage for mixed-source crate (test/generated_inputs/lib.rs)" \
81+
; 1>&2 echo "Coverage was silently dropped for crates with generated sources." \
82+
; 1>&2 awk '/^SF:test\/generated_inputs\/lib\.rs$/,/^end_of_record$/' \
83+
bazel-out/_coverage/_coverage_report.dat \
84+
; exit 1 \
85+
; }
7486
split_coverage_postprocessing_shell_commands: &split_coverage_postprocessing_shell_commands
7587
- echo "coverage --experimental_fetch_all_coverage_outputs" >> user.bazelrc
7688
- echo "coverage --experimental_split_coverage_postprocessing" >> user.bazelrc

rust/private/rustc.bzl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,14 @@ def _has_location_expansion(flags):
897897
return True
898898
return False
899899

900+
def _args_map_bin_dir(file):
901+
"""Extract `bazel-out/<config>/bin` from a File whose path lives in the configuration's bin directory.
902+
903+
Evaluated at action-execution time so that Bazel's path mapping (`--experimental_output_paths=strip`) can
904+
rewrite the `<config>` segment to `cfg` before we slice it off.
905+
"""
906+
return "/".join(file.path.split("/", 3)[:3])
907+
900908
def construct_arguments(
901909
*,
902910
ctx,
@@ -1326,6 +1334,25 @@ def construct_arguments(
13261334
# https://doc.rust-lang.org/rustc/instrument-coverage.html
13271335
rustc_flags.add("--codegen=instrument-coverage")
13281336

1337+
# Crates with generated sources are compiled from the output tree
1338+
# (see `transform_sources`), so the coverage mapping records their
1339+
# files with a `bazel-out/<config>/bin/` prefix. Bazel's lcov
1340+
# merger silently drops all coverage for such crates, so we remap
1341+
# the prefix away. The prefix is derived from the crate's own
1342+
# output File (rather than `ctx.bin_dir`) so that Bazel's path
1343+
# mapping (`--experimental_output_paths=strip`) can rewrite the
1344+
# `<config>` segment to `cfg` before the value reaches rustc —
1345+
# `ctx.bin_dir` is a `root`, not a `File`, and is not subject to
1346+
# path-mapping rewriting. Skipped for rustdoc (which passes
1347+
# `remap_path_prefix=None`), since rustdoc only supports
1348+
# `--remap-path-prefix` behind `-Zunstable-options`.
1349+
if remap_path_prefix != None:
1350+
rustc_flags.add_all(
1351+
[crate_info.output],
1352+
format_each = "--remap-path-prefix=%s/=",
1353+
map_each = _args_map_bin_dir,
1354+
)
1355+
13291356
if toolchain._experimental_link_std_dylib:
13301357
rustc_flags.add("--codegen=prefer-dynamic")
13311358

test/unit/common.bzl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,25 @@ def assert_env_value(env, action, key, value):
149149
real_value = action.env[key],
150150
),
151151
)
152+
153+
def get_bin_dir_from_action(action):
154+
"""Extract `bazel-out/<config>/bin` from an action.
155+
156+
Inspects argv first so the path-mapping (`--experimental_output_paths=strip`)
157+
form `bazel-out/cfg/bin` is returned when active, otherwise falls back to
158+
the first output's `.dirname` (handles `-ST-<hash>` config-transition
159+
suffixes).
160+
161+
Args:
162+
action: The action to extract the bin directory from.
163+
164+
Returns:
165+
The bin directory path as a string.
166+
"""
167+
for arg in action.argv:
168+
if arg.startswith("bazel-out/cfg/bin/"):
169+
return "bazel-out/cfg/bin"
170+
bin_dir = action.outputs.to_list()[0].dirname
171+
if "/bin/" in bin_dir:
172+
bin_dir = bin_dir.split("/bin/")[0] + "/bin"
173+
return bin_dir

test/unit/native_deps/native_deps_test.bzl

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,12 @@ load(
1111
"assert_argv_contains_prefix_not",
1212
"assert_argv_contains_prefix_suffix",
1313
"assert_list_contains_adjacent_elements",
14+
"get_bin_dir_from_action",
1415
)
1516

1617
def _get_toolchain(ctx):
1718
return ctx.attr._toolchain[platform_common.ToolchainInfo]
1819

19-
def _get_bin_dir_from_action(action):
20-
"""Extract the bin directory from an action's outputs.
21-
22-
This handles config transitions that add suffixes like -ST-<hash>, as
23-
well as Bazel path mapping (`--experimental_output_paths=strip` plus a
24-
`supports-path-mapping` requirement on the action), which rewrites
25-
argv entries to live under `bazel-out/cfg/bin/...` even though the
26-
File's `.dirname` still returns the un-mapped, configuration-specific
27-
path.
28-
29-
Args:
30-
action: The action to extract the bin directory from.
31-
32-
Returns:
33-
The bin directory path as a string.
34-
"""
35-
for arg in action.argv:
36-
if arg.startswith("bazel-out/cfg/bin/"):
37-
return "bazel-out/cfg/bin"
38-
bin_dir = action.outputs.to_list()[0].dirname
39-
if "/bin/" in bin_dir:
40-
bin_dir = bin_dir.split("/bin/")[0] + "/bin"
41-
return bin_dir
42-
4320
def _get_darwin_component(arg):
4421
"""Extract darwin component from a path.
4522
@@ -213,7 +190,7 @@ def _bin_has_native_dep_and_alwayslink_test_impl(ctx, use_cc_linker):
213190

214191
toolchain = _get_toolchain(ctx)
215192
link_args = _extract_linker_args(action.argv)
216-
bin_dir = _get_bin_dir_from_action(action)
193+
bin_dir = get_bin_dir_from_action(action)
217194

218195
# Validate bin_dir structure (ignoring ST-{hash} suffix from config transitions)
219196
_assert_bin_dir_structure(env, ctx, bin_dir, toolchain)
@@ -296,7 +273,7 @@ def _cdylib_has_native_dep_and_alwayslink_test_impl(ctx, use_cc_linker):
296273
action = tut.actions[0]
297274

298275
linker_args = _extract_linker_args(action.argv)
299-
bin_dir = _get_bin_dir_from_action(action)
276+
bin_dir = get_bin_dir_from_action(action)
300277

301278
# Validate bin_dir structure (ignoring ST-{hash} suffix from config transitions)
302279
_assert_bin_dir_structure(env, ctx, bin_dir, toolchain)

test/unit/remap_path_prefix/remap_path_prefix_test.bzl

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ load("//rust:defs.bzl", "rust_binary", "rust_library")
66
load(
77
"//test/unit:common.bzl",
88
"assert_action_mnemonic",
9+
"assert_argv_contains",
10+
"assert_argv_contains_not",
911
"assert_list_contains_adjacent_elements",
12+
"get_bin_dir_from_action",
1013
)
1114

1215
def _remap_path_prefix_test_impl(ctx):
@@ -42,6 +45,68 @@ def _subst_flags_test_impl(ctx):
4245

4346
_subst_flags_test = analysistest.make(_subst_flags_test_impl)
4447

48+
def _coverage_remap_path_prefix_test_impl(ctx):
49+
"""Verify a single `--remap-path-prefix` flag covers the bin directory.
50+
51+
The flag is derived from a `File` via `map_each` so Bazel's path
52+
mapping (`--experimental_output_paths=strip`) rewrites the
53+
`<config>` segment to `cfg` before it reaches rustc — one flag
54+
works for both the path-mapped and un-mapped forms.
55+
"""
56+
env = analysistest.begin(ctx)
57+
target = analysistest.target_under_test(env)
58+
59+
action = target.actions[0]
60+
assert_action_mnemonic(env, action, "Rustc")
61+
62+
# If the host toolchain does not support coverage (no `llvm_cov` or
63+
# missing `profiler_builtins`) the instrument-coverage flag will be
64+
# absent and the remap flag is not expected either.
65+
if "--codegen=instrument-coverage" not in action.argv:
66+
return analysistest.end(env)
67+
68+
bin_dir = get_bin_dir_from_action(action)
69+
assert_argv_contains(env, action, "--remap-path-prefix={}/=".format(bin_dir))
70+
71+
return analysistest.end(env)
72+
73+
_coverage_remap_path_prefix_test = analysistest.make(
74+
_coverage_remap_path_prefix_test_impl,
75+
config_settings = {
76+
"//command_line_option:collect_code_coverage": True,
77+
},
78+
)
79+
80+
_coverage_remap_path_prefix_path_mapping_test = analysistest.make(
81+
_coverage_remap_path_prefix_test_impl,
82+
config_settings = {
83+
"//command_line_option:collect_code_coverage": True,
84+
"//command_line_option:experimental_output_paths": "strip",
85+
},
86+
)
87+
88+
def _no_coverage_remap_path_prefix_test_impl(ctx):
89+
"""Verify the coverage-specific remap flag is absent without coverage."""
90+
env = analysistest.begin(ctx)
91+
target = analysistest.target_under_test(env)
92+
93+
action = target.actions[0]
94+
assert_action_mnemonic(env, action, "Rustc")
95+
96+
assert_argv_contains_not(env, action, "--codegen=instrument-coverage")
97+
98+
bin_dir = get_bin_dir_from_action(action)
99+
assert_argv_contains_not(env, action, "--remap-path-prefix={}/=".format(bin_dir))
100+
101+
return analysistest.end(env)
102+
103+
_no_coverage_remap_path_prefix_test = analysistest.make(
104+
_no_coverage_remap_path_prefix_test_impl,
105+
config_settings = {
106+
"//command_line_option:collect_code_coverage": False,
107+
},
108+
)
109+
45110
def remap_path_prefix_test_suite(name):
46111
"""Entry-point macro called from the BUILD file.
47112
@@ -78,6 +143,38 @@ def remap_path_prefix_test_suite(name):
78143
edition = "2021",
79144
)
80145

146+
# A library whose source set contains a generated file — this is the
147+
# case `transform_sources` is designed for, and the one that
148+
# previously lost all coverage data because the records pointed at
149+
# `bazel-out/.../bin/...`.
150+
write_file(
151+
name = "mixed_inline_src",
152+
out = "mixed_inline.rs",
153+
content = [
154+
"pub fn inline() {}",
155+
"",
156+
],
157+
)
158+
159+
write_file(
160+
name = "mixed_generated_src",
161+
out = "mixed_generated.rs",
162+
content = [
163+
"pub fn generated() {}",
164+
"",
165+
],
166+
)
167+
168+
rust_library(
169+
name = "remap_mixed_lib",
170+
srcs = [
171+
":mixed_inline.rs",
172+
":mixed_generated_src",
173+
],
174+
crate_root = ":mixed_inline.rs",
175+
edition = "2021",
176+
)
177+
81178
_remap_path_prefix_test(
82179
name = "remap_path_prefix_lib_test",
83180
target_under_test = ":remap_lib",
@@ -98,11 +195,29 @@ def remap_path_prefix_test_suite(name):
98195
target_under_test = ":remap_bin",
99196
)
100197

198+
_coverage_remap_path_prefix_test(
199+
name = "coverage_remap_path_prefix_mixed_lib_test",
200+
target_under_test = ":remap_mixed_lib",
201+
)
202+
203+
_coverage_remap_path_prefix_path_mapping_test(
204+
name = "coverage_remap_path_prefix_path_mapping_mixed_lib_test",
205+
target_under_test = ":remap_mixed_lib",
206+
)
207+
208+
_no_coverage_remap_path_prefix_test(
209+
name = "no_coverage_remap_path_prefix_lib_test",
210+
target_under_test = ":remap_lib",
211+
)
212+
101213
tests = [
102214
":remap_path_prefix_lib_test",
103215
":remap_path_prefix_bin_test",
104216
":subst_flags_lib_test",
105217
":subst_flags_bin_test",
218+
":coverage_remap_path_prefix_mixed_lib_test",
219+
":coverage_remap_path_prefix_path_mapping_mixed_lib_test",
220+
":no_coverage_remap_path_prefix_lib_test",
106221
]
107222

108223
native.test_suite(

0 commit comments

Comments
 (0)