-
Notifications
You must be signed in to change notification settings - Fork 13.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
doctests: fix merging on stable #137899
doctests: fix merging on stable #137899
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,7 +96,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}")); | ||
|
@@ -513,12 +513,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() | ||
} | ||
} | ||
|
||
|
@@ -537,91 +543,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![]; | ||
notriddle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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()); | ||
|
@@ -630,8 +653,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"); | ||
notriddle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
} | ||
Comment on lines
+675
to
+686
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering, is this actually necessary? Did you have a concrete case where this was needed? Locally I've experimented with the consequences of removing it, couldn't find any problems and all tests pass without it, too. It's possible that I missed something. Everything seems to work out just fine without this extra logic because any And if the (non-Cargo) user / Cargo itself were to pass too few
pub const VALUE: bool = true;
//! ```
//! assert!(foreign::VALUE);
//! ```
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly for more complex crate graphs (
|
||
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"); | ||
|
@@ -708,15 +788,15 @@ 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); | ||
} | ||
} | ||
if let Some(run_directory) = &rustdoc_options.test_run_directory { | ||
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(), | ||
|
@@ -1003,7 +1083,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); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(definitely not blocking) With some work it might be possible to generate a single
doctest_runner.rs
for all editions unless I'm missing something glaringly obviousThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Theoretically, yes, but that would require a bunch of refactoring to generate the runner, and doesn't seem worth it for a niche feature like writing doctests with a different edition than the crate.