xref: /aosp_15_r20/external/bazelbuild-rules_rust/rust/private/rustdoc_test.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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
17load("//rust/private:common.bzl", "rust_common")
18load("//rust/private:providers.bzl", "CrateInfo")
19load("//rust/private:rustdoc.bzl", "rustdoc_compile_action")
20load("//rust/private:utils.bzl", "dedent", "find_toolchain", "transform_deps")
21
22def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate_info):
23    """Construct arguments and environment variables specific to `rustdoc_test_writer`.
24
25    This is largely solving for the fact that tests run from a runfiles directory
26    where actions run in an execroot. But it also tracks what environment variables
27    were explicitly added to the action.
28
29    Args:
30        ctx (ctx): The rule's context object.
31        test_runner (File): The test_runner output file declared by `rustdoc_test`.
32        opt_test_params (File): An output file we can optionally use to store params for `rustdoc`.
33        action (struct): Action arguments generated by `rustdoc_compile_action`.
34        crate_info (CrateInfo): The provider of the crate who's docs are being tested.
35
36    Returns:
37        tuple: A tuple of `rustdoc_test_writer` specific inputs
38            - Args: Arguments for the test writer
39            - dict: Required environment variables
40    """
41
42    writer_args = ctx.actions.args()
43
44    # Track the output path where the test writer should write the test
45    writer_args.add("--output={}".format(test_runner.path))
46
47    # Track where the test writer should move "spilled" Args to
48    writer_args.add("--optional_test_params={}".format(opt_test_params.path))
49
50    # Track what environment variables should be written to the test runner
51    writer_args.add("--action_env=DEVELOPER_DIR")
52    writer_args.add("--action_env=PATHEXT")
53    writer_args.add("--action_env=SDKROOT")
54    writer_args.add("--action_env=SYSROOT")
55    for var in action.env.keys():
56        writer_args.add("--action_env={}".format(var))
57
58    # Since the test runner will be running from a runfiles directory, the
59    # paths originally generated for the build action will not map to any
60    # files. To ensure rustdoc can find the appropriate dependencies, the
61    # file roots are identified and tracked for each dependency so it can be
62    # stripped from the test runner.
63
64    # Collect and dedupe all of the file roots in a list before appending
65    # them to args to prevent generating a large amount of identical args
66    roots = []
67    root = crate_info.output.root.path
68    if not root in roots:
69        roots.append(root)
70    for dep in crate_info.deps.to_list():
71        dep_crate_info = getattr(dep, "crate_info", None)
72        dep_dep_info = getattr(dep, "dep_info", None)
73        if dep_crate_info:
74            root = dep_crate_info.output.root.path
75            if not root in roots:
76                roots.append(root)
77        if dep_dep_info:
78            for direct_dep in dep_dep_info.direct_crates.to_list():
79                root = direct_dep.dep.output.root.path
80                if not root in roots:
81                    roots.append(root)
82            for transitive_dep in dep_dep_info.transitive_crates.to_list():
83                root = transitive_dep.output.root.path
84                if not root in roots:
85                    roots.append(root)
86
87    for root in roots:
88        writer_args.add("--strip_substring={}/".format(root))
89
90    # Indicate that the rustdoc_test args are over.
91    writer_args.add("--")
92
93    # Prepare for the process runner to ingest the rest of the arguments
94    # to match the expectations of `rustc_compile_action`.
95    writer_args.add(ctx.executable._process_wrapper.short_path)
96
97    return (writer_args, action.env)
98
99def _rust_doc_test_impl(ctx):
100    """The implementation for the `rust_doc_test` rule
101
102    Args:
103        ctx (ctx): The rule's context object
104
105    Returns:
106        list: A list containing a DefaultInfo provider
107    """
108
109    toolchain = find_toolchain(ctx)
110
111    crate = ctx.attr.crate[rust_common.crate_info]
112    deps = transform_deps(ctx.attr.deps)
113
114    crate_info = rust_common.create_crate_info(
115        name = crate.name,
116        type = crate.type,
117        root = crate.root,
118        srcs = crate.srcs,
119        deps = depset(deps, transitive = [crate.deps]),
120        proc_macro_deps = crate.proc_macro_deps,
121        aliases = crate.aliases,
122        output = crate.output,
123        edition = crate.edition,
124        rustc_env = crate.rustc_env,
125        rustc_env_files = crate.rustc_env_files,
126        is_test = True,
127        compile_data = crate.compile_data,
128        compile_data_targets = crate.compile_data_targets,
129        wrapped_crate_type = crate.type,
130        owner = ctx.label,
131    )
132
133    if toolchain.target_os == "windows":
134        test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat")
135    else:
136        test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh")
137
138    # Bazel will auto-magically spill params to a file, if they are too many for a given OSes shell
139    # (e.g. Windows ~32k, Linux ~2M). The executable script (aka test_runner) that gets generated,
140    # is run from the runfiles, which is separate from the params_file Bazel generates. To handle
141    # this case, we declare our own params file, that the test_writer will populate, if necessary
142    opt_test_params = ctx.actions.declare_file(ctx.label.name + ".rustdoc_opt_params", sibling = test_runner)
143
144    # Add the current crate as an extern for the compile action
145    rustdoc_flags = [
146        "--extern",
147        "{}={}".format(crate_info.name, crate_info.output.short_path),
148        "--test",
149    ]
150
151    action = rustdoc_compile_action(
152        ctx = ctx,
153        toolchain = toolchain,
154        crate_info = crate_info,
155        rustdoc_flags = rustdoc_flags,
156        is_test = True,
157    )
158
159    tools = action.tools + [ctx.executable._process_wrapper]
160
161    writer_args, env = _construct_writer_arguments(
162        ctx = ctx,
163        test_runner = test_runner,
164        opt_test_params = opt_test_params,
165        action = action,
166        crate_info = crate_info,
167    )
168
169    # Allow writer environment variables to override those from the action.
170    action.env.update(env)
171
172    ctx.actions.run(
173        mnemonic = "RustdocTestWriter",
174        progress_message = "Generating Rustdoc test runner for {}".format(ctx.attr.crate.label),
175        executable = ctx.executable._test_writer,
176        inputs = action.inputs,
177        tools = tools,
178        arguments = [writer_args] + action.arguments,
179        env = action.env,
180        outputs = [test_runner, opt_test_params],
181    )
182
183    return [DefaultInfo(
184        files = depset([test_runner]),
185        runfiles = ctx.runfiles(files = tools + [opt_test_params], transitive_files = action.inputs),
186        executable = test_runner,
187    )]
188
189rust_doc_test = rule(
190    implementation = _rust_doc_test_impl,
191    attrs = {
192        "crate": attr.label(
193            doc = (
194                "The label of the target to generate code documentation for. " +
195                "`rust_doc_test` can generate HTML code documentation for the " +
196                "source files of `rust_library` or `rust_binary` targets."
197            ),
198            providers = [rust_common.crate_info],
199            mandatory = True,
200        ),
201        "deps": attr.label_list(
202            doc = dedent("""\
203                List of other libraries to be linked to this library target.
204
205                These can be either other `rust_library` targets or `cc_library` targets if
206                linking a native library.
207            """),
208            providers = [[CrateInfo], [CcInfo]],
209        ),
210        "_cc_toolchain": attr.label(
211            doc = (
212                "In order to use find_cc_toolchain, your rule has to depend " +
213                "on C++ toolchain. See @rules_cc//cc:find_cc_toolchain.bzl " +
214                "docs for details."
215            ),
216            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
217        ),
218        "_process_wrapper": attr.label(
219            doc = "A process wrapper for running rustdoc on all platforms",
220            cfg = "exec",
221            default = Label("//util/process_wrapper"),
222            executable = True,
223        ),
224        "_test_writer": attr.label(
225            doc = "A binary used for writing script for use as the test executable.",
226            cfg = "exec",
227            default = Label("//tools/rustdoc:rustdoc_test_writer"),
228            executable = True,
229        ),
230    },
231    test = True,
232    fragments = ["cpp"],
233    toolchains = [
234        str(Label("//rust:toolchain_type")),
235        "@bazel_tools//tools/cpp:toolchain_type",
236    ],
237    doc = dedent("""\
238        Runs Rust documentation tests.
239
240        Example:
241
242        Suppose you have the following directory structure for a Rust library crate:
243
244        ```output
245        [workspace]/
246        WORKSPACE
247        hello_lib/
248            BUILD
249            src/
250                lib.rs
251        ```
252
253        To run [documentation tests][doc-test] for the `hello_lib` crate, define a `rust_doc_test` \
254        target that depends on the `hello_lib` `rust_library` target:
255
256        [doc-test]: https://doc.rust-lang.org/book/documentation.html#documentation-as-tests
257
258        ```python
259        package(default_visibility = ["//visibility:public"])
260
261        load("@rules_rust//rust:defs.bzl", "rust_library", "rust_doc_test")
262
263        rust_library(
264            name = "hello_lib",
265            srcs = ["src/lib.rs"],
266        )
267
268        rust_doc_test(
269            name = "hello_lib_doc_test",
270            crate = ":hello_lib",
271        )
272        ```
273
274        Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation tests for the `hello_lib` library crate.
275    """),
276)
277