diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl index 777def8e6e..3700978297 100644 --- a/rust/private/rustdoc_test.bzl +++ b/rust/private/rustdoc_test.bzl @@ -171,13 +171,32 @@ def _compiled_rust_doc_test_impl(ctx, toolchain, crate_info): is_executable = True, ) + # An rlib's runfiles need not include its native shared libraries. The + # persisted doctest executables still need those libraries at test time. + runtime_libraries = [] + for target in [ctx.attr.crate] + ctx.attr.deps: + if CcInfo not in target: + continue + for linker_input in target[CcInfo].linking_context.linker_inputs.to_list(): + for lib in linker_input.libraries: + if lib.static_library or lib.pic_static_library: + continue + if lib.dynamic_library: + runtime_libraries.append(lib.dynamic_library) + if lib.resolved_symlink_dynamic_library: + runtime_libraries.append(lib.resolved_symlink_dynamic_library) + return [ DefaultInfo( files = depset([test_runner]), runfiles = ctx.runfiles( - files = [doctest_dir, test_metadata, ctx.executable._test_runner_bin], - transitive_files = action.inputs, - ), + files = [doctest_dir, test_metadata, ctx.executable._test_runner_bin] + runtime_libraries, + transitive_files = crate_info.compile_data, + ).merge_all([ + target[DefaultInfo].default_runfiles + for target in [ctx.attr.crate] + ctx.attr.deps + [ctx.attr._test_runner_bin] + if target[DefaultInfo].default_runfiles != None + ]), executable = test_runner, ), RunEnvironmentInfo( diff --git a/test/rustdoc_runfiles/BUILD.bazel b/test/rustdoc_runfiles/BUILD.bazel new file mode 100644 index 0000000000..2c4ad0eb90 --- /dev/null +++ b/test/rustdoc_runfiles/BUILD.bazel @@ -0,0 +1,97 @@ +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("@rules_cc//cc:defs.bzl", "cc_import", "cc_library") +load("//rust:defs.bzl", "rust_doc_test", "rust_library") +load(":runfiles_test.bzl", "compiled_runfiles_test", "without_runfiles") + +# Compiled doctest launchers do not yet keep their executable suffix on Windows. +NOT_WINDOWS = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], +}) + +rust_doc_test( + name = "runtime_data_test", + crate = ":lib", + rustdoc_flags = ["--test-args=--nocapture"], + target_compatible_with = NOT_WINDOWS + select({ + ":compiled_doctests": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + ":extra", + ":native_without_runfiles", + ], +) + +compiled_runfiles_test( + name = "compiled_runfiles_test", + target_compatible_with = NOT_WINDOWS + select({ + ":compiled_doctests": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + target_under_test = ":runtime_data_test", +) + +rust_library( + name = "lib", + srcs = ["lib.rs"], + compile_data = ["compile.txt"], + crate_name = "doctest_runfiles", + data = ["crate.txt"], + edition = "2021", + link_deps = [":native_import"], + target_compatible_with = NOT_WINDOWS, + deps = [":transitive"], +) + +rust_library( + name = "transitive", + srcs = ["empty.rs"], + data = ["transitive.txt"], + edition = "2021", +) + +rust_library( + name = "extra", + srcs = ["empty.rs"], + data = ["extra.txt"], + edition = "2021", +) + +cc_import( + name = "native_import", + shared_library = ":native_shared", + target_compatible_with = NOT_WINDOWS, +) + +without_runfiles( + name = "native_without_runfiles", + dep = ":native_import", +) + +cc_shared_library( + name = "native_shared", + target_compatible_with = NOT_WINDOWS, + # Use the declared runtime path so this tests runfiles propagation without + # depending on rustdoc's executable-relative rpaths for persisted tests. + user_link_flags = select({ + "@platforms//os:linux": ["-Wl,-soname,test/rustdoc_runfiles/libnative_shared.so"], + "@platforms//os:macos": ["-Wl,-install_name,test/rustdoc_runfiles/libnative_shared.dylib"], + "//conditions:default": [], + }), + deps = [":native"], +) + +cc_library( + name = "native", + srcs = ["native.c"], + target_compatible_with = NOT_WINDOWS, +) + +config_setting( + name = "compiled_doctests", + flag_values = { + "//rust/settings:experimental_compile_rustdoc_tests": "true", + "//rust/toolchain/channel:channel": "nightly", + }, +) diff --git a/test/rustdoc_runfiles/compile.txt b/test/rustdoc_runfiles/compile.txt new file mode 100644 index 0000000000..9698f3839a --- /dev/null +++ b/test/rustdoc_runfiles/compile.txt @@ -0,0 +1 @@ +compile data diff --git a/test/rustdoc_runfiles/crate.txt b/test/rustdoc_runfiles/crate.txt new file mode 100644 index 0000000000..5e25ab2769 --- /dev/null +++ b/test/rustdoc_runfiles/crate.txt @@ -0,0 +1 @@ +crate data diff --git a/test/rustdoc_runfiles/empty.rs b/test/rustdoc_runfiles/empty.rs new file mode 100644 index 0000000000..a01339c9a2 --- /dev/null +++ b/test/rustdoc_runfiles/empty.rs @@ -0,0 +1 @@ +//! A dependency with declared runtime data. diff --git a/test/rustdoc_runfiles/extra.txt b/test/rustdoc_runfiles/extra.txt new file mode 100644 index 0000000000..3887180cee --- /dev/null +++ b/test/rustdoc_runfiles/extra.txt @@ -0,0 +1 @@ +extra dependency data diff --git a/test/rustdoc_runfiles/lib.rs b/test/rustdoc_runfiles/lib.rs new file mode 100644 index 0000000000..95692e8b2b --- /dev/null +++ b/test/rustdoc_runfiles/lib.rs @@ -0,0 +1,28 @@ +//! Read declared runtime files and call a shared library from a persisted doctest. +//! +//! ``` +//! use std::path::PathBuf; +//! +//! let root = PathBuf::from(std::env::var("TEST_SRCDIR")?) +//! .join(std::env::var("TEST_WORKSPACE")?) +//! .join("test/rustdoc_runfiles"); +//! assert_eq!(std::fs::read_to_string(root.join("crate.txt"))?, "crate data\n"); +//! assert_eq!(std::fs::read_to_string(root.join("transitive.txt"))?, "transitive data\n"); +//! assert_eq!(std::fs::read_to_string(root.join("extra.txt"))?, "extra dependency data\n"); +//! assert_eq!(include_str!("compile.txt"), "compile data\n"); +//! assert_eq!(doctest_runfiles::answer(), 42); +//! # Ok::<(), Box>(()) +//! ``` + +#![warn(rust_2018_idioms)] + +#[link(name = "native_shared")] +extern "C" { + fn native_answer() -> i32; +} + +/// Return the value exported by the native runtime dependency. +pub fn answer() -> i32 { + // SAFETY: native_answer takes no pointers and always returns a valid i32. + unsafe { native_answer() } +} diff --git a/test/rustdoc_runfiles/native.c b/test/rustdoc_runfiles/native.c new file mode 100644 index 0000000000..ad8cbc7780 --- /dev/null +++ b/test/rustdoc_runfiles/native.c @@ -0,0 +1 @@ +int native_answer(void) { return 42; } diff --git a/test/rustdoc_runfiles/runfiles_test.bzl b/test/rustdoc_runfiles/runfiles_test.bzl new file mode 100644 index 0000000000..3d92e2be57 --- /dev/null +++ b/test/rustdoc_runfiles/runfiles_test.bzl @@ -0,0 +1,42 @@ +"""Compiled doctests need runtime dependencies, not their compilation tools.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") + +def _without_runfiles_impl(ctx): + return [ctx.attr.dep[CcInfo]] + +# A custom CcInfo dependency need not provide its own runtime runfiles. +without_runfiles = rule( + implementation = _without_runfiles_impl, + attrs = {"dep": attr.label(providers = [CcInfo])}, +) + +def _compiled_runfiles_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + action = [action for action in target.actions if action.mnemonic == "RustdocTestCompile"][0] + inputs = action.inputs.to_list() + runfiles = target[DefaultInfo].default_runfiles.files.to_list() + paths = [file.short_path for file in runfiles] + + for name in ["crate.txt", "transitive.txt", "extra.txt", "compile.txt"]: + asserts.true(env, "test/rustdoc_runfiles/" + name in paths, "Missing runfile: " + name) + + compilers = [file for file in inputs if file.basename in ["rustc", "rustdoc", "rustc.exe", "rustdoc.exe"]] + asserts.true(env, bool(compilers), "Compilation must retain its compiler inputs") + for file in compilers: + asserts.false(env, file in runfiles, "Compiler should not be a test input: " + file.path) + for file in runfiles: + asserts.false(env, "/rust_toolchain/" in file.path, "Compiler SDK should not be a test input: " + file.path) + + libraries = [file for file in inputs if "native_shared" in file.basename and file.extension in ["so", "dylib", "dll"]] + asserts.true(env, bool(libraries), "Fixture must link a shared native library") + for file in libraries: + asserts.true(env, file in runfiles, "Missing shared library: " + file.path) + + return analysistest.end(env) + +compiled_runfiles_test = analysistest.make( + _compiled_runfiles_test_impl, +) diff --git a/test/rustdoc_runfiles/transitive.txt b/test/rustdoc_runfiles/transitive.txt new file mode 100644 index 0000000000..bae365a145 --- /dev/null +++ b/test/rustdoc_runfiles/transitive.txt @@ -0,0 +1 @@ +transitive data