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