diff options
Diffstat (limited to 'src/librustdoc/doctest.rs')
| -rw-r--r-- | src/librustdoc/doctest.rs | 170 |
1 files changed, 125 insertions, 45 deletions
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 009e9662933..881d0b6dcbe 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -95,7 +95,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> .map_err(|error| format!("failed to create args file: {error:?}"))?; // We now put the common arguments into the file we created. - let mut content = vec!["--crate-type=bin".to_string()]; + let mut content = vec![]; for cfg in &options.cfgs { content.push(format!("--cfg={cfg}")); @@ -488,12 +488,18 @@ pub(crate) struct RunnableDocTest { line: usize, edition: Edition, no_run: bool, - is_multiple_tests: bool, + merged_test_code: Option<String>, } impl RunnableDocTest { - fn path_for_merged_doctest(&self) -> PathBuf { - self.test_opts.outdir.path().join(format!("doctest_{}.rs", self.edition)) + fn path_for_merged_doctest_bundle(&self) -> PathBuf { + self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition)) + } + fn path_for_merged_doctest_runner(&self) -> PathBuf { + self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition)) + } + fn is_multiple_tests(&self) -> bool { + self.merged_test_code.is_some() } } @@ -512,91 +518,108 @@ fn run_test( let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target); let output_file = doctest.test_opts.outdir.path().join(rust_out); - let rustc_binary = rustdoc_options - .test_builder - .as_deref() - .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc")); - let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); + // Common arguments used for compiling the doctest runner. + // On merged doctests, the compiler is invoked twice: once for the test code itself, + // and once for the runner wrapper (which needs to use `#![feature]` on stable). + let mut compiler_args = vec![]; - compiler.arg(format!("@{}", doctest.global_opts.args_file.display())); + compiler_args.push(format!("@{}", doctest.global_opts.args_file.display())); if let Some(sysroot) = &rustdoc_options.maybe_sysroot { - compiler.arg(format!("--sysroot={}", sysroot.display())); + compiler_args.push(format!("--sysroot={}", sysroot.display())); } - compiler.arg("--edition").arg(doctest.edition.to_string()); - if !doctest.is_multiple_tests { - // Setting these environment variables is unneeded if this is a merged doctest. - compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path); - compiler.env( - "UNSTABLE_RUSTDOC_TEST_LINE", - format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize), - ); - } - compiler.arg("-o").arg(&output_file); + compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]); if langstr.test_harness { - compiler.arg("--test"); + compiler_args.push("--test".to_owned()); } if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail { - compiler.arg("--error-format=json"); - compiler.arg("--json").arg("unused-externs"); - compiler.arg("-W").arg("unused_crate_dependencies"); - compiler.arg("-Z").arg("unstable-options"); + compiler_args.push("--error-format=json".to_owned()); + compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]); + compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]); + compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]); } if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() { // FIXME: why does this code check if it *shouldn't* persist doctests // -- shouldn't it be the negation? - compiler.arg("--emit=metadata"); + compiler_args.push("--emit=metadata".to_owned()); } - compiler.arg("--target").arg(match &rustdoc_options.target { - TargetTuple::TargetTuple(s) => s, - TargetTuple::TargetJson { path_for_rustdoc, .. } => { - path_for_rustdoc.to_str().expect("target path must be valid unicode") - } - }); + compiler_args.extend_from_slice(&[ + "--target".to_owned(), + match &rustdoc_options.target { + TargetTuple::TargetTuple(s) => s.clone(), + TargetTuple::TargetJson { path_for_rustdoc, .. } => { + path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned() + } + }, + ]); if let ErrorOutputType::HumanReadable(kind, color_config) = rustdoc_options.error_format { let short = kind.short(); let unicode = kind == HumanReadableErrorType::Unicode; if short { - compiler.arg("--error-format").arg("short"); + compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]); } if unicode { - compiler.arg("--error-format").arg("human-unicode"); + compiler_args + .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]); } match color_config { ColorConfig::Never => { - compiler.arg("--color").arg("never"); + compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]); } ColorConfig::Always => { - compiler.arg("--color").arg("always"); + compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]); } ColorConfig::Auto => { - compiler.arg("--color").arg(if supports_color { "always" } else { "never" }); + compiler_args.extend_from_slice(&[ + "--color".to_owned(), + if supports_color { "always" } else { "never" }.to_owned(), + ]); } } } + let rustc_binary = rustdoc_options + .test_builder + .as_deref() + .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc")); + let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); + + compiler.args(&compiler_args); + // If this is a merged doctest, we need to write it into a file instead of using stdin // because if the size of the merged doctests is too big, it'll simply break stdin. - if doctest.is_multiple_tests { + if doctest.is_multiple_tests() { // It makes the compilation failure much faster if it is for a combined doctest. compiler.arg("--error-format=short"); - let input_file = doctest.path_for_merged_doctest(); + let input_file = doctest.path_for_merged_doctest_bundle(); if std::fs::write(&input_file, &doctest.full_test_code).is_err() { // If we cannot write this file for any reason, we leave. All combined tests will be // tested as standalone tests. return Err(TestFailure::CompileError); } - compiler.arg(input_file); if !rustdoc_options.nocapture { // If `nocapture` is disabled, then we don't display rustc's output when compiling // the merged doctests. compiler.stderr(Stdio::null()); } + // bundled tests are an rlib, loaded by a separate runner executable + compiler + .arg("--crate-type=lib") + .arg("--out-dir") + .arg(doctest.test_opts.outdir.path()) + .arg(input_file); } else { + compiler.arg("--crate-type=bin").arg("-o").arg(&output_file); + // Setting these environment variables is unneeded if this is a merged doctest. + compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path); + compiler.env( + "UNSTABLE_RUSTDOC_TEST_LINE", + format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize), + ); compiler.arg("-"); compiler.stdin(Stdio::piped()); compiler.stderr(Stdio::piped()); @@ -605,8 +628,65 @@ fn run_test( debug!("compiler invocation for doctest: {compiler:?}"); let mut child = compiler.spawn().expect("Failed to spawn rustc process"); - let output = if doctest.is_multiple_tests { + let output = if let Some(merged_test_code) = &doctest.merged_test_code { + // compile-fail tests never get merged, so this should always pass let status = child.wait().expect("Failed to wait"); + + // the actual test runner is a separate component, built with nightly-only features; + // build it now + let runner_input_file = doctest.path_for_merged_doctest_runner(); + + let mut runner_compiler = + wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); + // the test runner does not contain any user-written code, so this doesn't allow + // the user to exploit nightly-only features on stable + runner_compiler.env("RUSTC_BOOTSTRAP", "1"); + runner_compiler.args(compiler_args); + runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file); + let mut extern_path = std::ffi::OsString::from(format!( + "--extern=doctest_bundle_{edition}=", + edition = doctest.edition + )); + for extern_str in &rustdoc_options.extern_strs { + if let Some((_cratename, path)) = extern_str.split_once('=') { + // Direct dependencies of the tests themselves are + // indirect dependencies of the test runner. + // They need to be in the library search path. + let dir = Path::new(path) + .parent() + .filter(|x| x.components().count() > 0) + .unwrap_or(Path::new(".")); + runner_compiler.arg("-L").arg(dir); + } + } + let output_bundle_file = doctest + .test_opts + .outdir + .path() + .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition)); + extern_path.push(&output_bundle_file); + runner_compiler.arg(extern_path); + runner_compiler.arg(&runner_input_file); + if std::fs::write(&runner_input_file, &merged_test_code).is_err() { + // If we cannot write this file for any reason, we leave. All combined tests will be + // tested as standalone tests. + return Err(TestFailure::CompileError); + } + if !rustdoc_options.nocapture { + // If `nocapture` is disabled, then we don't display rustc's output when compiling + // the merged doctests. + runner_compiler.stderr(Stdio::null()); + } + runner_compiler.arg("--error-format=short"); + debug!("compiler invocation for doctest runner: {runner_compiler:?}"); + + let status = if !status.success() { + status + } else { + let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process"); + child_runner.wait().expect("Failed to wait") + }; + process::Output { status, stdout: Vec::new(), stderr: Vec::new() } } else { let stdin = child.stdin.as_mut().expect("Failed to open stdin"); @@ -683,7 +763,7 @@ fn run_test( cmd.arg(&output_file); } else { cmd = Command::new(&output_file); - if doctest.is_multiple_tests { + if doctest.is_multiple_tests() { cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file); } } @@ -691,7 +771,7 @@ fn run_test( cmd.current_dir(run_directory); } - let result = if doctest.is_multiple_tests || rustdoc_options.nocapture { + let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture { cmd.status().map(|status| process::Output { status, stdout: Vec::new(), @@ -977,7 +1057,7 @@ fn doctest_run_fn( line: scraped_test.line, edition: scraped_test.edition(&rustdoc_options), no_run: scraped_test.no_run(&rustdoc_options), - is_multiple_tests: false, + merged_test_code: None, }; let res = run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs); |
