Have rust_test put its compilation outputs in a subdirectory (#1434)

This is supposed to fix the long standing Windows flakiness as described in https://github.com/bazelbuild/rules_rust/pull/1427#issuecomment-1171940333

Initially I thought it's an issue with `rustdoc_test`, however the actual issue is that `rust_binary` and its `rust_test` have the same crate name and generate the same intermediary `.o` files. For sandboxed builds this is not an issue, as the actions are isolated, however, we have a race condition in non-sandboxed builds (Windows):

Let's consider the following build:
```
bazel build --spawn_strategy=standalone \
    //test/unit/rustdoc:bin_with_transitive_proc_macro \
    //test/unit/rustdoc:bin_with_transitive_proc_macro_test
```

The `bin_with_transitive_proc_macro` compile action will create the following intermediate file: `bazel-out/k8-fastbuild/bin/test/unit/rustdoc/bin_with_transitive_proc_macro.bin_with_transitive_proc_macro.573369dc-cgu.2.rcgu.o`, as will the `bin_with_transitive_proc_macro_test` action. These two files differ slightly (as the second action is for a test), namely the `bin_with_transitive_proc_macro` output has the following symbols:

```
0000000000000000 T main
0000000000000000 t _ZN30bin_with_transitive_proc_macro4main17hfc292cc42314e131E
                 U _ZN3std2rt10lang_start17h1dbc829c47cd61d9E
```

while the `bin_with_transitive_proc_macro_test` `.o` output looks like this:
```
0000000000000000 T main
0000000000000000 t _ZN30bin_with_transitive_proc_macro4main17h28726504dc060f8dE
                 U _ZN3std2rt10lang_start17h1dbc829c47cd61d9E
                 U _ZN4test16test_main_static17h37e3d88407f5b40fE
```

Now, when the two actions run in parallel, it can happen that `bin_with_transitive_proc_macro` creates the output, and `bin_with_transitive_proc_macro_test` overwrites it. Then, at linking time for `bin_with_transitive_proc_macro`, `rustc` will complain:
```
note: ld.lld: error: undefined symbol: test::test_main_static::h37e3d88407f5b40f
```
This is because `bin_with_transitive_proc_macro` is not a test and as such is not linked against `libtest`.

This PR fixes the issue by directing the compilation outputs of `rust_test` to be under a `test-{hash}` directory.
diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl
index b189525..9af3a32 100644
--- a/rust/private/rust.bzl
+++ b/rust/private/rust.bzl
@@ -384,13 +384,11 @@
         ),
     )
 
-def _rust_test_common(ctx, toolchain, output):
-    """Builds a Rust test binary.
+def _rust_test_impl(ctx):
+    """The implementation of the `rust_test` rule.
 
     Args:
         ctx (ctx): The ctx object for the current target.
-        toolchain (rust_toolchain): The current `rust_toolchain`
-        output (File): The output File that will be produced, depends on crate type.
 
     Returns:
         list: The list of providers. See `rustc_compile_action`
@@ -398,6 +396,8 @@
     _assert_no_deprecated_attributes(ctx)
     _assert_correct_dep_mapping(ctx)
 
+    toolchain = find_toolchain(ctx)
+
     srcs, crate_root = _transform_sources(ctx, ctx.files.srcs, getattr(ctx.file, "crate_root", None))
     crate_type = "bin"
 
@@ -408,6 +408,15 @@
         # Target is building the crate in `test` config
         crate = ctx.attr.crate[rust_common.crate_info] if rust_common.crate_info in ctx.attr.crate else ctx.attr.crate[rust_common.test_crate_info].crate
 
+        output_hash = determine_output_hash(crate.root, ctx.label)
+        output = ctx.actions.declare_file(
+            "test-%s/%s%s" % (
+                output_hash,
+                ctx.label.name,
+                toolchain.binary_ext,
+            ),
+        )
+
         # Optionally join compile data
         if crate.compile_data:
             compile_data = depset(ctx.files.compile_data, transitive = [crate.compile_data])
@@ -435,6 +444,15 @@
         if not crate_root:
             crate_root = crate_root_src(ctx.attr.name, ctx.files.srcs, "lib")
 
+        output_hash = determine_output_hash(crate_root, ctx.label)
+        output = ctx.actions.declare_file(
+            "test-%s/%s%s" % (
+                output_hash,
+                ctx.label.name,
+                toolchain.binary_ext,
+            ),
+        )
+
         # Target is a standalone crate. Build the test binary as its own crate.
         crate_info = rust_common.create_crate_info(
             name = compute_crate_name(ctx.workspace_name, ctx.label, toolchain, ctx.attr.crate_name),
@@ -476,23 +494,6 @@
 
     return providers
 
-def _rust_test_impl(ctx):
-    """The implementation of the `rust_test` rule
-
-    Args:
-        ctx (ctx): The rule's context object
-
-    Returns:
-        list: A list of providers. See `_rust_test_common`
-    """
-    toolchain = find_toolchain(ctx)
-
-    output = ctx.actions.declare_file(
-        ctx.label.name + toolchain.binary_ext,
-    )
-
-    return _rust_test_common(ctx, toolchain, output)
-
 _common_attrs = {
     "aliases": attr.label_keyed_string_dict(
         doc = dedent("""\
diff --git a/test/rust_test_suite/BUILD.bazel b/test/rust_test_suite/BUILD.bazel
index 8490f8b..df8ed74 100644
--- a/test/rust_test_suite/BUILD.bazel
+++ b/test/rust_test_suite/BUILD.bazel
@@ -7,7 +7,7 @@
 )
 
 rust_test_suite(
-    name = "integrated_tests_suite",
+    name = "tests_suite",
     srcs = glob(["tests/**"]),
     edition = "2018",
     deps = [":math_lib"],
diff --git a/test/unit/linkstamps/linkstamps_test.bzl b/test/unit/linkstamps/linkstamps_test.bzl
index 39fb048..a0ae6fe 100644
--- a/test/unit/linkstamps/linkstamps_test.bzl
+++ b/test/unit/linkstamps/linkstamps_test.bzl
@@ -2,7 +2,7 @@
 
 load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
 load("@rules_cc//cc:defs.bzl", "cc_library")
-load("//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
+load("//rust:defs.bzl", "rust_binary", "rust_common", "rust_library", "rust_test")
 load("//test/unit:common.bzl", "assert_action_mnemonic")
 
 def _is_running_on_linux(ctx):
@@ -20,8 +20,13 @@
     linkstamp_out = linkstamp_action.outputs.to_list()[0]
     asserts.equals(env, linkstamp_out.basename, "linkstamp.o")
     tut_out = tut.files.to_list()[0]
+    is_test = tut[rust_common.crate_info].is_test
     workspace_prefix = "" if ctx.workspace_name == "rules_rust" else "/external/rules_rust"
-    expected_linkstamp_path = tut_out.dirname + "/_objs/" + tut_out.basename + workspace_prefix + "/test/unit/linkstamps/linkstamp.o"
+
+    # Rust compilation outputs coming from a test are put in test-{hash} directory
+    # which we need to remove in order to obtain the linkstamp file path.
+    dirname = "/".join(tut_out.dirname.split("/")[:-1]) if is_test else tut_out.dirname
+    expected_linkstamp_path = dirname + "/_objs/" + tut_out.basename + workspace_prefix + "/test/unit/linkstamps/linkstamp.o"
     asserts.equals(
         env,
         linkstamp_out.path,
diff --git a/test/unit/rust_test_outputs_are_in_subdirectory/BUILD.bazel b/test/unit/rust_test_outputs_are_in_subdirectory/BUILD.bazel
new file mode 100644
index 0000000..bf00a3a
--- /dev/null
+++ b/test/unit/rust_test_outputs_are_in_subdirectory/BUILD.bazel
@@ -0,0 +1,4 @@
+load(":rust_test_outputs.bzl", "rust_test_outputs_test_suite")
+
+############################ UNIT TESTS #############################
+rust_test_outputs_test_suite(name = "rust_test_outputs_test_suite")
diff --git a/test/unit/rust_test_outputs_are_in_subdirectory/foo.rs b/test/unit/rust_test_outputs_are_in_subdirectory/foo.rs
new file mode 100644
index 0000000..da0f5d9
--- /dev/null
+++ b/test/unit/rust_test_outputs_are_in_subdirectory/foo.rs
@@ -0,0 +1 @@
+pub fn main() {}
diff --git a/test/unit/rust_test_outputs_are_in_subdirectory/rust_test_outputs.bzl b/test/unit/rust_test_outputs_are_in_subdirectory/rust_test_outputs.bzl
new file mode 100644
index 0000000..8c0f480
--- /dev/null
+++ b/test/unit/rust_test_outputs_are_in_subdirectory/rust_test_outputs.bzl
@@ -0,0 +1,64 @@
+"""Tests for rust_test outputs directory."""
+
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+load("//rust:defs.bzl", "rust_binary", "rust_common", "rust_test")
+
+def _rust_test_outputs_test(ctx):
+    env = analysistest.begin(ctx)
+    tut = analysistest.target_under_test(env)
+
+    output = tut[rust_common.crate_info].output
+
+    asserts.true(env, output.dirname.split("/")[-1].startswith("test-"))
+
+    return analysistest.end(env)
+
+rust_test_outputs_test = analysistest.make(
+    _rust_test_outputs_test,
+)
+
+def _rust_test_outputs_targets():
+    rust_binary(
+        name = "bin_outputs",
+        srcs = ["foo.rs"],
+        edition = "2018",
+    )
+
+    rust_test(
+        name = "test_outputs_with_srcs",
+        srcs = ["foo.rs"],
+        edition = "2018",
+    )
+
+    rust_test_outputs_test(
+        name = "rust_test_outputs_using_srcs_attr",
+        target_under_test = ":test_outputs_with_srcs",
+    )
+
+    rust_test(
+        name = "test_outputs_with_crate",
+        crate = "bin_outputs",
+        edition = "2018",
+    )
+
+    rust_test_outputs_test(
+        name = "rust_test_outputs_using_crate_attr",
+        target_under_test = ":test_outputs_with_crate",
+    )
+
+def rust_test_outputs_test_suite(name):
+    """Entry-point macro called from the BUILD file.
+
+    Args:
+      name: Name of the macro.
+    """
+
+    _rust_test_outputs_targets()
+
+    native.test_suite(
+        name = name,
+        tests = [
+            ":rust_test_outputs_using_srcs_attr",
+            ":rust_test_outputs_using_crate_attr",
+        ],
+    )