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