Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | # Copyright 2018 The Bazel Authors. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Rules for performing `rustdoc --test` on Bazel built crates""" |
| 16 | |
| 17 | load("//rust/private:common.bzl", "rust_common") |
| 18 | load("//rust/private:rustdoc.bzl", "rustdoc_compile_action") |
| 19 | load("//rust/private:utils.bzl", "dedent", "find_toolchain") |
| 20 | |
| 21 | def _construct_writer_arguments(ctx, test_runner, action, crate_info): |
| 22 | """Construct arguments and environment variables specific to `rustdoc_test_writer`. |
| 23 | |
| 24 | This is largely solving for the fact that tests run from a runfiles directory |
| 25 | where actions run in an execroot. But it also tracks what environment variables |
| 26 | were explicitly added to the action. |
| 27 | |
| 28 | Args: |
| 29 | ctx (ctx): The rule's context object. |
| 30 | test_runner (File): The test_runner output file declared by `rustdoc_test`. |
| 31 | action (struct): Action arguments generated by `rustdoc_compile_action`. |
| 32 | crate_info (CrateInfo): The provider of the crate who's docs are being tested. |
| 33 | |
| 34 | Returns: |
| 35 | tuple: A tuple of `rustdoc_test_writer` specific inputs |
| 36 | - Args: Arguments for the test writer |
| 37 | - dict: Required environment variables |
| 38 | """ |
| 39 | |
| 40 | writer_args = ctx.actions.args() |
| 41 | |
| 42 | # Track the output path where the test writer should write the test |
| 43 | writer_args.add("--output={}".format(test_runner.path)) |
| 44 | |
| 45 | # Track what environment variables should be written to the test runner |
| 46 | writer_args.add("--action_env=DEVELOPER_DIR") |
| 47 | writer_args.add("--action_env=PATHEXT") |
| 48 | writer_args.add("--action_env=SDKROOT") |
| 49 | writer_args.add("--action_env=SYSROOT") |
| 50 | for var in action.env.keys(): |
| 51 | writer_args.add("--action_env={}".format(var)) |
| 52 | |
| 53 | # Since the test runner will be running from a runfiles directory, the |
| 54 | # paths originally generated for the build action will not map to any |
| 55 | # files. To ensure rustdoc can find the appropriate dependencies, the |
| 56 | # file roots are identified and tracked for each dependency so it can be |
| 57 | # stripped from the test runner. |
| 58 | for dep in crate_info.deps.to_list(): |
| 59 | dep_crate_info = getattr(dep, "crate_info", None) |
| 60 | dep_dep_info = getattr(dep, "dep_info", None) |
| 61 | if dep_crate_info: |
| 62 | root = dep_crate_info.output.root.path |
| 63 | writer_args.add("--strip_substring={}/".format(root)) |
| 64 | if dep_dep_info: |
| 65 | for direct_dep in dep_dep_info.direct_crates.to_list(): |
| 66 | root = direct_dep.dep.output.root.path |
| 67 | writer_args.add("--strip_substring={}/".format(root)) |
| 68 | for transitive_dep in dep_dep_info.transitive_crates.to_list(): |
| 69 | root = transitive_dep.output.root.path |
| 70 | writer_args.add("--strip_substring={}/".format(root)) |
| 71 | |
| 72 | # Indicate that the rustdoc_test args are over. |
| 73 | writer_args.add("--") |
| 74 | |
| 75 | # Prepare for the process runner to ingest the rest of the arguments |
| 76 | # to match the expectations of `rustc_compile_action`. |
| 77 | writer_args.add(ctx.executable._process_wrapper.short_path) |
| 78 | |
| 79 | return (writer_args, action.env) |
| 80 | |
| 81 | def _rust_doc_test_impl(ctx): |
| 82 | """The implementation for the `rust_doc_test` rule |
| 83 | |
| 84 | Args: |
| 85 | ctx (ctx): The rule's context object |
| 86 | |
| 87 | Returns: |
| 88 | list: A list containing a DefaultInfo provider |
| 89 | """ |
| 90 | |
| 91 | toolchain = find_toolchain(ctx) |
| 92 | |
| 93 | crate = ctx.attr.crate |
| 94 | crate_info = crate[rust_common.crate_info] |
| 95 | |
| 96 | if toolchain.os == "windows": |
| 97 | test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat") |
| 98 | else: |
| 99 | test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh") |
| 100 | |
| 101 | # Add the current crate as an extern for the compile action |
| 102 | rustdoc_flags = [ |
| 103 | "--extern", |
| 104 | "{}={}".format(crate_info.name, crate_info.output.short_path), |
| 105 | "--test", |
| 106 | ] |
| 107 | |
| 108 | action = rustdoc_compile_action( |
| 109 | ctx = ctx, |
| 110 | toolchain = toolchain, |
| 111 | crate_info = crate_info, |
| 112 | rustdoc_flags = rustdoc_flags, |
| 113 | is_test = True, |
| 114 | ) |
| 115 | |
| 116 | tools = action.tools + [ctx.executable._process_wrapper] |
| 117 | |
| 118 | writer_args, env = _construct_writer_arguments( |
| 119 | ctx = ctx, |
| 120 | test_runner = test_runner, |
| 121 | action = action, |
| 122 | crate_info = crate_info, |
| 123 | ) |
| 124 | |
| 125 | # Allow writer environment variables to override those from the action. |
| 126 | action.env.update(env) |
| 127 | |
| 128 | ctx.actions.run( |
| 129 | mnemonic = "RustdocTestWriter", |
| 130 | progress_message = "Generating Rustdoc test runner for {}".format(crate.label), |
| 131 | executable = ctx.executable._test_writer, |
| 132 | inputs = action.inputs, |
| 133 | tools = tools, |
| 134 | arguments = [writer_args] + action.arguments, |
| 135 | env = action.env, |
| 136 | outputs = [test_runner], |
| 137 | ) |
| 138 | |
| 139 | return [DefaultInfo( |
| 140 | files = depset([test_runner]), |
| 141 | runfiles = ctx.runfiles(files = tools, transitive_files = action.inputs), |
| 142 | executable = test_runner, |
| 143 | )] |
| 144 | |
| 145 | rust_doc_test = rule( |
| 146 | implementation = _rust_doc_test_impl, |
| 147 | attrs = { |
| 148 | "crate": attr.label( |
| 149 | doc = ( |
| 150 | "The label of the target to generate code documentation for. " + |
| 151 | "`rust_doc_test` can generate HTML code documentation for the " + |
| 152 | "source files of `rust_library` or `rust_binary` targets." |
| 153 | ), |
| 154 | providers = [rust_common.crate_info], |
| 155 | mandatory = True, |
| 156 | ), |
| 157 | "_cc_toolchain": attr.label( |
| 158 | doc = ( |
| 159 | "In order to use find_cc_toolchain, your rule has to depend " + |
| 160 | "on C++ toolchain. See @rules_cc//cc:find_cc_toolchain.bzl " + |
| 161 | "docs for details." |
| 162 | ), |
| 163 | default = "@bazel_tools//tools/cpp:current_cc_toolchain", |
| 164 | ), |
| 165 | "_process_wrapper": attr.label( |
| 166 | doc = "A process wrapper for running rustdoc on all platforms", |
| 167 | cfg = "exec", |
| 168 | default = Label("//util/process_wrapper"), |
| 169 | executable = True, |
| 170 | ), |
| 171 | "_test_writer": attr.label( |
| 172 | doc = "A binary used for writing script for use as the test executable.", |
| 173 | cfg = "exec", |
| 174 | default = Label("//tools/rustdoc:rustdoc_test_writer"), |
| 175 | executable = True, |
| 176 | ), |
| 177 | }, |
| 178 | test = True, |
| 179 | fragments = ["cpp"], |
| 180 | host_fragments = ["cpp"], |
| 181 | toolchains = [ |
| 182 | str(Label("//rust:toolchain")), |
| 183 | "@bazel_tools//tools/cpp:toolchain_type", |
| 184 | ], |
| 185 | incompatible_use_toolchain_transition = True, |
| 186 | doc = dedent("""\ |
| 187 | Runs Rust documentation tests. |
| 188 | |
| 189 | Example: |
| 190 | |
| 191 | Suppose you have the following directory structure for a Rust library crate: |
| 192 | |
| 193 | ```output |
| 194 | [workspace]/ |
| 195 | WORKSPACE |
| 196 | hello_lib/ |
| 197 | BUILD |
| 198 | src/ |
| 199 | lib.rs |
| 200 | ``` |
| 201 | |
| 202 | To run [documentation tests][doc-test] for the `hello_lib` crate, define a `rust_doc_test` \ |
| 203 | target that depends on the `hello_lib` `rust_library` target: |
| 204 | |
| 205 | [doc-test]: https://doc.rust-lang.org/book/documentation.html#documentation-as-tests |
| 206 | |
| 207 | ```python |
| 208 | package(default_visibility = ["//visibility:public"]) |
| 209 | |
| 210 | load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc_test") |
| 211 | |
| 212 | rust_library( |
| 213 | name = "hello_lib", |
| 214 | srcs = ["src/lib.rs"], |
| 215 | ) |
| 216 | |
| 217 | rust_doc_test( |
| 218 | name = "hello_lib_doc_test", |
| 219 | crate = ":hello_lib", |
| 220 | ) |
| 221 | ``` |
| 222 | |
| 223 | Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation tests for the `hello_lib` library crate. |
| 224 | """), |
| 225 | ) |