Add path-mapping test for env location expansion (#4120)

Adds regression testing for
https://github.com/bazelbuild/rules_rust/pull/4117 as well as identifies
some cases where there are further incompatibilities (namely uses of
`rust_test.crate`).
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index 112a7b3..44d2576 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -890,10 +890,19 @@
             other_flags.append(flag)
     return other_flags
 
-def _has_location_expansion(flags):
-    for flag in flags:
+def has_location_expansion(values):
+    """Return True if any string in `values` contains a Bazel location-expansion directive.
+
+    Args:
+        values: Iterable of strings (e.g. `rustc_flags`, or `rustc_env.values()`).
+
+    Returns:
+        bool: True if any value contains `$(location ...)`, `$(locations ...)`,
+        `$(execpath ...)`, or `$(execpaths ...)`.
+    """
+    for value in values:
         for directive in ("$(location ", "$(locations ", "$(execpath ", "$(execpaths "):
-            if directive in flag:
+            if directive in value:
                 return True
     return False
 
@@ -1446,17 +1455,27 @@
     all_args = [process_wrapper_flags, rustc_path, rustc_flags]
     if rust_flags_args != None:
         all_args.append(rust_flags_args)
-    has_location_expansion = _has_location_expansion(authored_rustc_flags)
-    rustc_env_attr = getattr(crate_info, "rustc_env", None)
-    if not has_location_expansion and rustc_env_attr:
-        has_location_expansion = _has_location_expansion(rustc_env_attr.values())
+
+    # Path mapping must be disabled whenever any input string carries a
+    # `$(location ...)` / `$(execpath ...)` macro: location expansion
+    # runs outside path mapping and would produce configuration-specific
+    # paths inside env values that the sandbox layout no longer matches.
+    # Check both `authored_rustc_flags` and `attr.rustc_env` (raw). We
+    # cannot rely on `crate_info.rustc_env`: `rust_test` pre-expands its
+    # own `rustc_env` inside the rule impl (see `rust.bzl`), so the
+    # markers are gone by the time they reach `crate_info`.
+    target_has_location_expansion = has_location_expansion(authored_rustc_flags)
+    if not target_has_location_expansion:
+        rustc_env_attr = getattr(attr, "rustc_env", None)
+        if rustc_env_attr:
+            target_has_location_expansion = has_location_expansion(rustc_env_attr.values())
 
     args = struct(
         process_wrapper_flags = process_wrapper_flags,
         rustc_path = rustc_path,
         rustc_flags = rustc_flags,
         extra_rustc_flags = rust_flags_args,
-        supports_path_mapping = not has_location_expansion,
+        supports_path_mapping = not target_has_location_expansion,
         all = all_args,
     )
 
diff --git a/test/unit/location_expansion/location_expansion_test.bzl b/test/unit/location_expansion/location_expansion_test.bzl
index 3c63355..7ca5d0b 100644
--- a/test/unit/location_expansion/location_expansion_test.bzl
+++ b/test/unit/location_expansion/location_expansion_test.bzl
@@ -3,7 +3,7 @@
 load("@bazel_skylib//lib:unittest.bzl", "analysistest")
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
 load("//cargo:defs.bzl", "cargo_build_script")
-load("//rust:defs.bzl", "rust_library")
+load("//rust:defs.bzl", "rust_library", "rust_test")
 load("//test/unit:common.bzl", "assert_action_mnemonic", "assert_argv_contains", "assert_env_value")
 
 def _find_action(tut, mnemonic):
@@ -35,6 +35,27 @@
 
 location_expansion_rustc_flags_test = analysistest.make(_location_expansion_rustc_flags_test)
 
+def _location_expansion_rustc_env_test(ctx):
+    env = analysistest.begin(ctx)
+    tut = analysistest.target_under_test(env)
+    action = _find_action(tut, "Rustc")
+    if not action:
+        fail("No Rustc action found")
+
+    # Sanity-check: `$(execpaths ...)` in `rustc_env` is expanded at
+    # analysis time and surfaces as a configuration-specific path in
+    # the env value. The build-time guard against path-mapping
+    # mismatches lives in `mylibrary_env` itself: it pulls the env
+    # path in via `include_bytes!(env!("MY_DATA"))`, so any path under
+    # `--experimental_output_paths=strip` that doesn't match the
+    # sandbox layout fails the build — see the comment in
+    # `mylibrary_env.rs`.
+    expected = "${pwd}/" + ctx.bin_dir.path + "/test/unit/location_expansion/flag_execpaths.data"
+    assert_env_value(env, action, "MY_DATA", expected)
+    return analysistest.end(env)
+
+location_expansion_rustc_env_test = analysistest.make(_location_expansion_rustc_env_test)
+
 def _location_expansion_build_script_env_test(ctx):
     env = analysistest.begin(ctx)
     tut = analysistest.target_under_test(env)
@@ -75,6 +96,36 @@
         ],
     )
 
+    # A library whose location expansion lives ONLY in `rustc_env` (not
+    # in `rustc_flags`). Exercises the path-mapping interaction with
+    # rustc_env specifically.
+    rust_library(
+        name = "mylibrary_env",
+        srcs = ["mylibrary_env.rs"],
+        edition = "2018",
+        rustc_env = {
+            "MY_DATA": "$(execpaths :flag_generator_execpaths)",
+        },
+        compile_data = [
+            ":flag_generator_execpaths",
+        ],
+    )
+
+    # Same shape as `mylibrary_env` but built through `rust_test`, which
+    # takes a different `crate_info.rustc_env` code path — see
+    # `mytest_env.rs` for the details of why PR #4117 doesn't cover it.
+    rust_test(
+        name = "mytest_env",
+        srcs = ["mytest_env.rs"],
+        edition = "2018",
+        rustc_env = {
+            "MY_DATA": "$(execpaths :flag_generator_execpaths)",
+        },
+        compile_data = [
+            ":flag_generator_execpaths",
+        ],
+    )
+
     cargo_build_script(
         name = "mybuildscript",
         srcs = ["build_script.rs"],
@@ -90,6 +141,11 @@
         target_under_test = ":mylibrary",
     )
 
+    location_expansion_rustc_env_test(
+        name = "location_expansion_rustc_env_test",
+        target_under_test = ":mylibrary_env",
+    )
+
     location_expansion_build_script_env_test(
         name = "location_expansion_build_script_env_test",
         target_under_test = ":mybuildscript",
@@ -107,6 +163,7 @@
         name = name,
         tests = [
             ":location_expansion_rustc_flags_test",
+            ":location_expansion_rustc_env_test",
             ":location_expansion_build_script_env_test",
         ],
     )
diff --git a/test/unit/location_expansion/mylibrary_env.rs b/test/unit/location_expansion/mylibrary_env.rs
new file mode 100644
index 0000000..7513641
--- /dev/null
+++ b/test/unit/location_expansion/mylibrary_env.rs
@@ -0,0 +1,16 @@
+// The byte content of `MY_DATA` is included at compile time. The
+// path is plumbed via a `$(execpaths ...)` location expansion in
+// `rustc_env`, so it points at a generated file under `bazel-out`.
+//
+// When Bazel path mapping (`--experimental_output_paths=strip`) is
+// active, the rustc action's sandbox sees files at
+// `bazel-out/cfg/bin/...` only. The expanded env value, however, is
+// a plain string set at analysis time and is NOT rewritten by path
+// mapping. So if the action wrongly advertises
+// `supports-path-mapping`, the env path stays at
+// `bazel-out/<config>/bin/...` while the file only exists at
+// `bazel-out/cfg/bin/...` inside the sandbox, and this
+// `include_bytes!` fails at compile time. PR #4117 disables path
+// mapping for actions whose `rustc_env` contains location
+// expansions; this `const` exercises that contract.
+pub const MY_DATA: &[u8] = include_bytes!(env!("MY_DATA"));
diff --git a/test/unit/location_expansion/mytest_env.rs b/test/unit/location_expansion/mytest_env.rs
new file mode 100644
index 0000000..8c7f508
--- /dev/null
+++ b/test/unit/location_expansion/mytest_env.rs
@@ -0,0 +1,18 @@
+// Same contract as `mylibrary_env.rs`, but exercised through
+// `rust_test` rather than `rust_library`. This surfaces a code path
+// PR #4117 missed: for `rust_test`, location expansion in `rustc_env`
+// is performed inside the rule implementation (see `rust.bzl`)
+// BEFORE the values reach `CrateInfo.rustc_env`. By the time
+// `construct_arguments` in `rustc.bzl` inspects
+// `crate_info.rustc_env.values()` looking for `$(location ...)`
+// markers, they've already been replaced with concrete paths — so
+// the check finds nothing and `supports-path-mapping` stays enabled.
+// Under `--experimental_output_paths=strip`, that leaves the env
+// value pointing at the un-mapped `bazel-out/<config>/bin/...` path
+// while the sandbox only has the file at `bazel-out/cfg/bin/...`.
+//
+// The bug is caught at compile time by `include_bytes!(env!(...))`:
+// rustc has to read the file at the env path inside the sandbox, so
+// a path-mapping mismatch fails the build outright. No runtime
+// assertion is needed — declaring the const is the whole test.
+const _MY_DATA: &[u8] = include_bytes!(env!("MY_DATA"));