blob: 663c65f12bb9401294773470aea30030d6582fad [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001# 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
17load("//rust/private:common.bzl", "rust_common")
18load("//rust/private:rustdoc.bzl", "rustdoc_compile_action")
19load("//rust/private:utils.bzl", "dedent", "find_toolchain")
20
21def _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
81def _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
145rust_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)