From 6818777b8470a7bc03ea16f4011aa4dcfe78c4ed Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 13 Jul 2025 20:38:12 +0200 Subject: [PATCH 01/11] Correctly handle `--no-run` rustdoc test option --- src/librustdoc/doctest.rs | 2 +- src/librustdoc/doctest/runner.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index c9cd9f7fd4b11..bc26ba82c08b9 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -350,7 +350,7 @@ pub(crate) fn run_tests( ); for (doctest, scraped_test) in &doctests { - tests_runner.add_test(doctest, scraped_test, &target_str); + tests_runner.add_test(doctest, scraped_test, &target_str, rustdoc_options); } let (duration, ret) = tests_runner.run_merged_tests( rustdoc_test_options, diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index fcfa424968e48..5493d56456872 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -39,6 +39,7 @@ impl DocTestRunner { doctest: &DocTestBuilder, scraped_test: &ScrapedDocTest, target_str: &str, + opts: &RustdocOptions, ) { let ignore = match scraped_test.langstr.ignore { Ignore::All => true, @@ -62,6 +63,7 @@ impl DocTestRunner { self.nb_tests, &mut self.output, &mut self.output_merged_tests, + opts, ), )); self.supports_color &= doctest.supports_color; @@ -223,6 +225,7 @@ fn generate_mergeable_doctest( id: usize, output: &mut String, output_merged_tests: &mut String, + opts: &RustdocOptions, ) -> String { let test_id = format!("__doctest_{id}"); @@ -256,7 +259,7 @@ fn main() {returns_result} {{ ) .unwrap(); } - let not_running = ignore || scraped_test.langstr.no_run; + let not_running = ignore || scraped_test.no_run(opts); writeln!( output_merged_tests, " @@ -270,7 +273,7 @@ test::StaticTestFn( test_name = scraped_test.name, file = scraped_test.path(), line = scraped_test.line, - no_run = scraped_test.langstr.no_run, + no_run = scraped_test.no_run(opts), should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic, // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply // don't give it the function to run. From c20d6b713989eab2750359fb910c614f5b335d7c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:23:15 +0200 Subject: [PATCH 02/11] Fix `should_panic` on merged doctests --- library/test/src/lib.rs | 4 +- library/test/src/test_result.rs | 117 ++++++++++++++++-- src/librustdoc/doctest.rs | 96 ++------------ src/librustdoc/doctest/runner.rs | 34 ++--- .../failed-doctest-should-panic-2021.stdout | 2 +- .../failed-doctest-should-panic.stdout | 4 +- .../rustdoc-ui/doctest/wrong-ast-2024.stdout | 2 +- 7 files changed, 139 insertions(+), 120 deletions(-) diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index d554807bbde70..8aaf579422f61 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -45,7 +45,9 @@ pub mod test { pub use crate::cli::{TestOpts, parse_opts}; pub use crate::helpers::metrics::{Metric, MetricMap}; pub use crate::options::{Options, RunIgnored, RunStrategy, ShouldPanic}; - pub use crate::test_result::{TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk}; + pub use crate::test_result::{ + RustdocResult, TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk, get_rustdoc_result, + }; pub use crate::time::{TestExecTime, TestTimeOptions}; pub use crate::types::{ DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, diff --git a/library/test/src/test_result.rs b/library/test/src/test_result.rs index 4cb43fc45fd6c..dea1831db0509 100644 --- a/library/test/src/test_result.rs +++ b/library/test/src/test_result.rs @@ -1,7 +1,8 @@ use std::any::Any; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; -use std::process::ExitStatus; +use std::process::{ExitStatus, Output}; +use std::{fmt, io}; pub use self::TestResult::*; use super::bench::BenchSamples; @@ -103,15 +104,14 @@ pub(crate) fn calc_result( result } -/// Creates a `TestResult` depending on the exit code of test subprocess. -pub(crate) fn get_result_from_exit_code( - desc: &TestDesc, +/// Creates a `TestResult` depending on the exit code of test subprocess +pub(crate) fn get_result_from_exit_code_inner( status: ExitStatus, - time_opts: Option<&time::TestTimeOptions>, - exec_time: Option<&time::TestExecTime>, + success_error_code: i32, ) -> TestResult { - let result = match status.code() { - Some(TR_OK) => TestResult::TrOk, + match status.code() { + Some(error_code) if error_code == success_error_code => TestResult::TrOk, + Some(crate::ERROR_EXIT_CODE) => TestResult::TrFailed, #[cfg(windows)] Some(STATUS_FAIL_FAST_EXCEPTION) => TestResult::TrFailed, #[cfg(unix)] @@ -131,7 +131,17 @@ pub(crate) fn get_result_from_exit_code( Some(code) => TestResult::TrFailedMsg(format!("got unexpected return code {code}")), #[cfg(not(any(windows, unix)))] Some(_) => TestResult::TrFailed, - }; + } +} + +/// Creates a `TestResult` depending on the exit code of test subprocess and on its runtime. +pub(crate) fn get_result_from_exit_code( + desc: &TestDesc, + status: ExitStatus, + time_opts: Option<&time::TestTimeOptions>, + exec_time: Option<&time::TestExecTime>, +) -> TestResult { + let result = get_result_from_exit_code_inner(status, TR_OK); // If test is already failed (or allowed to fail), do not change the result. if result != TestResult::TrOk { @@ -147,3 +157,92 @@ pub(crate) fn get_result_from_exit_code( result } + +pub enum RustdocResult { + /// The test failed to compile. + CompileError, + /// The test is marked `compile_fail` but compiled successfully. + UnexpectedCompilePass, + /// The test failed to compile (as expected) but the compiler output did not contain all + /// expected error codes. + MissingErrorCodes(Vec), + /// The test binary was unable to be executed. + ExecutionError(io::Error), + /// The test binary exited with a non-zero exit code. + /// + /// This typically means an assertion in the test failed or another form of panic occurred. + ExecutionFailure(Output), + /// The test is marked `should_panic` but the test binary executed successfully. + NoPanic(Option), +} + +impl fmt::Display for RustdocResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CompileError => { + write!(f, "Couldn't compile the test.") + } + Self::UnexpectedCompilePass => { + write!(f, "Test compiled successfully, but it's marked `compile_fail`.") + } + Self::NoPanic(msg) => { + write!(f, "Test didn't panic, but it's marked `should_panic`")?; + if let Some(msg) = msg { + write!(f, " ({msg})")?; + } + f.write_str(".") + } + Self::MissingErrorCodes(codes) => { + write!(f, "Some expected error codes were not found: {codes:?}") + } + Self::ExecutionError(err) => { + write!(f, "Couldn't run the test: {err}")?; + if err.kind() == io::ErrorKind::PermissionDenied { + f.write_str(" - maybe your tempdir is mounted with noexec?")?; + } + Ok(()) + } + Self::ExecutionFailure(out) => { + writeln!(f, "Test executable failed ({reason}).", reason = out.status)?; + + // FIXME(#12309): An unfortunate side-effect of capturing the test + // executable's output is that the relative ordering between the test's + // stdout and stderr is lost. However, this is better than the + // alternative: if the test executable inherited the parent's I/O + // handles the output wouldn't be captured at all, even on success. + // + // The ordering could be preserved if the test process' stderr was + // redirected to stdout, but that functionality does not exist in the + // standard library, so it may not be portable enough. + let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); + let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); + + if !stdout.is_empty() || !stderr.is_empty() { + writeln!(f)?; + + if !stdout.is_empty() { + writeln!(f, "stdout:\n{stdout}")?; + } + + if !stderr.is_empty() { + writeln!(f, "stderr:\n{stderr}")?; + } + } + Ok(()) + } + } + } +} + +pub fn get_rustdoc_result(output: Output, should_panic: bool) -> Result<(), RustdocResult> { + let result = get_result_from_exit_code_inner(output.status, 0); + match (result, should_panic) { + (TestResult::TrFailed, true) | (TestResult::TrOk, false) => Ok(()), + (TestResult::TrOk, true) => Err(RustdocResult::NoPanic(None)), + (TestResult::TrFailedMsg(msg), true) => Err(RustdocResult::NoPanic(Some(msg))), + (TestResult::TrFailedMsg(_) | TestResult::TrFailed, false) => { + Err(RustdocResult::ExecutionFailure(output)) + } + _ => unreachable!("unexpected status for rustdoc test output"), + } +} diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index bc26ba82c08b9..b35dd5937905b 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -30,6 +30,7 @@ use rustc_span::symbol::sym; use rustc_span::{FileName, Span}; use rustc_target::spec::{Target, TargetTuple}; use tempfile::{Builder as TempFileBuilder, TempDir}; +use test::test::{RustdocResult, get_rustdoc_result}; use tracing::debug; use self::rust::HirCollector; @@ -445,25 +446,6 @@ fn scrape_test_config( opts } -/// Documentation test failure modes. -enum TestFailure { - /// The test failed to compile. - CompileError, - /// The test is marked `compile_fail` but compiled successfully. - UnexpectedCompilePass, - /// The test failed to compile (as expected) but the compiler output did not contain all - /// expected error codes. - MissingErrorCodes(Vec), - /// The test binary was unable to be executed. - ExecutionError(io::Error), - /// The test binary exited with a non-zero exit code. - /// - /// This typically means an assertion in the test failed or another form of panic occurred. - ExecutionFailure(process::Output), - /// The test is marked `should_panic` but the test binary executed successfully. - UnexpectedRunPass, -} - enum DirState { Temp(TempDir), Perm(PathBuf), @@ -553,7 +535,7 @@ fn run_test( rustdoc_options: &RustdocOptions, supports_color: bool, report_unused_externs: impl Fn(UnusedExterns), -) -> (Duration, Result<(), TestFailure>) { +) -> (Duration, Result<(), RustdocResult>) { let langstr = &doctest.langstr; // Make sure we emit well-formed executable names for our target. let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target); @@ -642,7 +624,7 @@ fn run_test( 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 (Duration::default(), Err(TestFailure::CompileError)); + return (Duration::default(), Err(RustdocResult::CompileError)); } if !rustdoc_options.no_capture { // If `no_capture` is disabled, then we don't display rustc's output when compiling @@ -719,7 +701,7 @@ fn run_test( 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 (instant.elapsed(), Err(TestFailure::CompileError)); + return (instant.elapsed(), Err(RustdocResult::CompileError)); } if !rustdoc_options.no_capture { // If `no_capture` is disabled, then we don't display rustc's output when compiling @@ -772,7 +754,7 @@ fn run_test( let _bomb = Bomb(&out); match (output.status.success(), langstr.compile_fail) { (true, true) => { - return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass)); + return (instant.elapsed(), Err(RustdocResult::UnexpectedCompilePass)); } (true, false) => {} (false, true) => { @@ -788,12 +770,15 @@ fn run_test( .collect(); if !missing_codes.is_empty() { - return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes))); + return ( + instant.elapsed(), + Err(RustdocResult::MissingErrorCodes(missing_codes)), + ); } } } (false, false) => { - return (instant.elapsed(), Err(TestFailure::CompileError)); + return (instant.elapsed(), Err(RustdocResult::CompileError)); } } @@ -831,17 +816,9 @@ fn run_test( cmd.output() }; match result { - Err(e) => return (duration, Err(TestFailure::ExecutionError(e))), - Ok(out) => { - if langstr.should_panic && out.status.success() { - return (duration, Err(TestFailure::UnexpectedRunPass)); - } else if !langstr.should_panic && !out.status.success() { - return (duration, Err(TestFailure::ExecutionFailure(out))); - } - } + Err(e) => (duration, Err(RustdocResult::ExecutionError(e))), + Ok(output) => (duration, get_rustdoc_result(output, langstr.should_panic)), } - - (duration, Ok(())) } /// Converts a path intended to use as a command to absolute if it is @@ -1132,54 +1109,7 @@ fn doctest_run_fn( run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs); if let Err(err) = res { - match err { - TestFailure::CompileError => { - eprint!("Couldn't compile the test."); - } - TestFailure::UnexpectedCompilePass => { - eprint!("Test compiled successfully, but it's marked `compile_fail`."); - } - TestFailure::UnexpectedRunPass => { - eprint!("Test executable succeeded, but it's marked `should_panic`."); - } - TestFailure::MissingErrorCodes(codes) => { - eprint!("Some expected error codes were not found: {codes:?}"); - } - TestFailure::ExecutionError(err) => { - eprint!("Couldn't run the test: {err}"); - if err.kind() == io::ErrorKind::PermissionDenied { - eprint!(" - maybe your tempdir is mounted with noexec?"); - } - } - TestFailure::ExecutionFailure(out) => { - eprintln!("Test executable failed ({reason}).", reason = out.status); - - // FIXME(#12309): An unfortunate side-effect of capturing the test - // executable's output is that the relative ordering between the test's - // stdout and stderr is lost. However, this is better than the - // alternative: if the test executable inherited the parent's I/O - // handles the output wouldn't be captured at all, even on success. - // - // The ordering could be preserved if the test process' stderr was - // redirected to stdout, but that functionality does not exist in the - // standard library, so it may not be portable enough. - let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); - let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); - - if !stdout.is_empty() || !stderr.is_empty() { - eprintln!(); - - if !stdout.is_empty() { - eprintln!("stdout:\n{stdout}"); - } - - if !stderr.is_empty() { - eprintln!("stderr:\n{stderr}"); - } - } - } - } - + eprint!("{err}"); panic::resume_unwind(Box::new(())); } Ok(()) diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index 5493d56456872..0a4ca67966242 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -6,7 +6,7 @@ use rustc_span::edition::Edition; use crate::doctest::{ DocTestBuilder, GlobalTestOptions, IndividualTestOptions, RunnableDocTest, RustdocOptions, - ScrapedDocTest, TestFailure, UnusedExterns, run_test, + RustdocResult, ScrapedDocTest, UnusedExterns, run_test, }; use crate::html::markdown::{Ignore, LangString}; @@ -136,29 +136,14 @@ mod __doctest_mod {{ }} #[allow(unused)] - pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{ + pub fn doctest_runner(bin: &std::path::Path, test_nb: usize, should_panic: bool) -> ExitCode {{ let out = std::process::Command::new(bin) .env(self::RUN_OPTION, test_nb.to_string()) .args(std::env::args().skip(1).collect::>()) .output() .expect(\"failed to run command\"); - if !out.status.success() {{ - if let Some(code) = out.status.code() {{ - eprintln!(\"Test executable failed (exit status: {{code}}).\"); - }} else {{ - eprintln!(\"Test executable failed (terminated by signal).\"); - }} - if !out.stdout.is_empty() || !out.stderr.is_empty() {{ - eprintln!(); - }} - if !out.stdout.is_empty() {{ - eprintln!(\"stdout:\"); - eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stdout)); - }} - if !out.stderr.is_empty() {{ - eprintln!(\"stderr:\"); - eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stderr)); - }} + if let Err(err) = test::test::get_rustdoc_result(out, should_panic) {{ + eprint!(\"{{err}}\"); ExitCode::FAILURE }} else {{ ExitCode::SUCCESS @@ -213,7 +198,10 @@ std::process::Termination::report(test::test_main(test_args, tests, None)) }; let (duration, ret) = run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); - (duration, if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }) + ( + duration, + if let Err(RustdocResult::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }, + ) } } @@ -265,7 +253,7 @@ fn main() {returns_result} {{ " mod {test_id} {{ pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest( -{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic}, +{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, false, test::StaticTestFn( || {{{runner}}}, )); @@ -274,7 +262,6 @@ test::StaticTestFn( file = scraped_test.path(), line = scraped_test.line, no_run = scraped_test.no_run(opts), - should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic, // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply // don't give it the function to run. runner = if not_running { @@ -283,11 +270,12 @@ test::StaticTestFn( format!( " if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{ - test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id})) + test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}, {should_panic})) }} else {{ test::assert_test_result(doctest_bundle::{test_id}::__main_fn()) }} ", + should_panic = scraped_test.langstr.should_panic, ) }, ) diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout index 9f4d60e6f4de5..f8413756e3d6d 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout @@ -5,7 +5,7 @@ test $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) ... FAILED failures: ---- $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) stdout ---- -Test executable succeeded, but it's marked `should_panic`. +Test didn't panic, but it's marked `should_panic`. failures: $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout index 9047fe0dcdd93..8865fb4e40425 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout @@ -1,11 +1,11 @@ running 1 test -test $DIR/failed-doctest-should-panic.rs - Foo (line 12) - should panic ... FAILED +test $DIR/failed-doctest-should-panic.rs - Foo (line 12) ... FAILED failures: ---- $DIR/failed-doctest-should-panic.rs - Foo (line 12) stdout ---- -note: test did not panic as expected at $DIR/failed-doctest-should-panic.rs:12:0 +Test didn't panic, but it's marked `should_panic`. failures: $DIR/failed-doctest-should-panic.rs - Foo (line 12) diff --git a/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout b/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout index 13567b41e51f5..27f9a0157a6cc 100644 --- a/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout +++ b/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout @@ -1,6 +1,6 @@ running 1 test -test $DIR/wrong-ast-2024.rs - three (line 20) - should panic ... ok +test $DIR/wrong-ast-2024.rs - three (line 20) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME From b5c9833b6e2b8707978ab43fd0cf186e974d087f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:37:31 +0200 Subject: [PATCH 03/11] Add regression tests for `no_run` and `compile_fail` --- tests/run-make/rustdoc-should-panic/rmake.rs | 43 +++++++++++++++++ tests/run-make/rustdoc-should-panic/test.rs | 14 ++++++ .../doctest/failed-doctest-should-panic.rs | 13 ++++-- .../failed-doctest-should-panic.stdout | 14 ++++-- .../doctest/no-run.edition2021.stdout | 12 +++++ .../doctest/no-run.edition2024.stdout | 18 ++++++++ tests/rustdoc-ui/doctest/no-run.rs | 46 +++++++++++++++++++ 7 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 tests/run-make/rustdoc-should-panic/rmake.rs create mode 100644 tests/run-make/rustdoc-should-panic/test.rs create mode 100644 tests/rustdoc-ui/doctest/no-run.edition2021.stdout create mode 100644 tests/rustdoc-ui/doctest/no-run.edition2024.stdout create mode 100644 tests/rustdoc-ui/doctest/no-run.rs diff --git a/tests/run-make/rustdoc-should-panic/rmake.rs b/tests/run-make/rustdoc-should-panic/rmake.rs new file mode 100644 index 0000000000000..07826768b88db --- /dev/null +++ b/tests/run-make/rustdoc-should-panic/rmake.rs @@ -0,0 +1,43 @@ +// Ensure that `should_panic` doctests only succeed if the test actually panicked. +// Regression test for . + +//@ ignore-cross-compile + +use run_make_support::rustdoc; + +fn check_output(edition: &str, panic_abort: bool) { + let mut rustdoc_cmd = rustdoc(); + rustdoc_cmd.input("test.rs").arg("--test").edition(edition); + if panic_abort { + rustdoc_cmd.args(["-C", "panic=abort"]); + } + let output = rustdoc_cmd.run_fail().stdout_utf8(); + let should_contain = &[ + "test test.rs - bad_exit_code (line 1) ... FAILED", + "test test.rs - did_not_panic (line 6) ... FAILED", + "test test.rs - did_panic (line 11) ... ok", + "---- test.rs - bad_exit_code (line 1) stdout ---- +Test executable failed (exit status: 1).", + "---- test.rs - did_not_panic (line 6) stdout ---- +Test didn't panic, but it's marked `should_panic` (got unexpected return code 1).", + "test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out;", + ]; + for text in should_contain { + assert!( + output.contains(text), + "output (edition: {edition}) doesn't contain {:?}\nfull output: {output}", + text + ); + } +} + +fn main() { + check_output("2015", false); + + // Same check with the merged doctest feature (enabled with the 2024 edition). + check_output("2024", false); + + // Checking that `-C panic=abort` is working too. + check_output("2015", true); + check_output("2024", true); +} diff --git a/tests/run-make/rustdoc-should-panic/test.rs b/tests/run-make/rustdoc-should-panic/test.rs new file mode 100644 index 0000000000000..1eea8e1e1958c --- /dev/null +++ b/tests/run-make/rustdoc-should-panic/test.rs @@ -0,0 +1,14 @@ +/// ``` +/// std::process::exit(1); +/// ``` +fn bad_exit_code() {} + +/// ```should_panic +/// std::process::exit(1); +/// ``` +fn did_not_panic() {} + +/// ```should_panic +/// panic!("yeay"); +/// ``` +fn did_panic() {} diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs index 0504c3dc73033..b95e23715175f 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs @@ -2,14 +2,17 @@ // adapted to use that, and that normalize line can go away //@ edition: 2024 -//@ compile-flags:--test +//@ compile-flags:--test --test-args=--test-threads=1 //@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" //@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" //@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" //@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" //@ failure-status: 101 -/// ```should_panic -/// println!("Hello, world!"); -/// ``` -pub struct Foo; +//! ```should_panic +//! println!("Hello, world!"); +//! ``` +//! +//! ```should_panic +//! std::process::exit(2); +//! ``` diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout index 8865fb4e40425..10172ea79226d 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout @@ -1,15 +1,19 @@ -running 1 test -test $DIR/failed-doctest-should-panic.rs - Foo (line 12) ... FAILED +running 2 tests +test $DIR/failed-doctest-should-panic.rs - (line 12) ... FAILED +test $DIR/failed-doctest-should-panic.rs - (line 16) ... FAILED failures: ----- $DIR/failed-doctest-should-panic.rs - Foo (line 12) stdout ---- +---- $DIR/failed-doctest-should-panic.rs - (line 12) stdout ---- Test didn't panic, but it's marked `should_panic`. +---- $DIR/failed-doctest-should-panic.rs - (line 16) stdout ---- +Test didn't panic, but it's marked `should_panic` (got unexpected return code 2). failures: - $DIR/failed-doctest-should-panic.rs - Foo (line 12) + $DIR/failed-doctest-should-panic.rs - (line 12) + $DIR/failed-doctest-should-panic.rs - (line 16) -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME +test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME all doctests ran in $TIME; merged doctests compilation took $TIME diff --git a/tests/rustdoc-ui/doctest/no-run.edition2021.stdout b/tests/rustdoc-ui/doctest/no-run.edition2021.stdout new file mode 100644 index 0000000000000..2b8232d18eba8 --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.edition2021.stdout @@ -0,0 +1,12 @@ + +running 7 tests +test $DIR/no-run.rs - f (line 14) - compile ... ok +test $DIR/no-run.rs - f (line 17) - compile ... ok +test $DIR/no-run.rs - f (line 20) ... ignored +test $DIR/no-run.rs - f (line 23) - compile ... ok +test $DIR/no-run.rs - f (line 31) - compile fail ... ok +test $DIR/no-run.rs - f (line 36) - compile ... ok +test $DIR/no-run.rs - f (line 40) - compile ... ok + +test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/rustdoc-ui/doctest/no-run.edition2024.stdout b/tests/rustdoc-ui/doctest/no-run.edition2024.stdout new file mode 100644 index 0000000000000..30d9c5d5fc769 --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.edition2024.stdout @@ -0,0 +1,18 @@ + +running 5 tests +test $DIR/no-run.rs - f (line 14) - compile ... ok +test $DIR/no-run.rs - f (line 17) - compile ... ok +test $DIR/no-run.rs - f (line 23) - compile ... ok +test $DIR/no-run.rs - f (line 36) - compile ... ok +test $DIR/no-run.rs - f (line 40) - compile ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 2 tests +test $DIR/no-run.rs - f (line 20) ... ignored +test $DIR/no-run.rs - f (line 31) - compile fail ... ok + +test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME + +all doctests ran in $TIME; merged doctests compilation took $TIME diff --git a/tests/rustdoc-ui/doctest/no-run.rs b/tests/rustdoc-ui/doctest/no-run.rs new file mode 100644 index 0000000000000..7b8f0ddc3f07a --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.rs @@ -0,0 +1,46 @@ +// This test ensures that the `--no-run` flag works the same between normal and merged doctests. +// Regression test for . + +//@ check-pass +//@ revisions: edition2021 edition2024 +//@ [edition2021]edition:2021 +//@ [edition2024]edition:2024 +//@ compile-flags:-Z unstable-options --test --no-run --test-args=--test-threads=1 +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" +//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" + +/// ``` +/// let a = true; +/// ``` +/// ```should_panic +/// panic!() +/// ``` +/// ```ignore (incomplete-code) +/// fn foo() { +/// ``` +/// ```no_run +/// loop { +/// println!("Hello, world"); +/// } +/// ``` +/// +/// fails to compile +/// +/// ```compile_fail +/// let x = 5; +/// x += 2; // shouldn't compile! +/// ``` +/// Ok the test does not run +/// ``` +/// panic!() +/// ``` +/// Ok the test does not run +/// ```should_panic +/// loop { +/// println!("Hello, world"); +/// panic!() +/// } +/// ``` +pub fn f() {} From 6dea913b48d130a3b4714ea1758e0d5e60777ec6 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 14:22:30 +0200 Subject: [PATCH 04/11] Fix stage 1 build --- src/librustdoc/doctest.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index b35dd5937905b..d0fe4c17431aa 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -4,6 +4,8 @@ mod markdown; mod runner; mod rust; +#[cfg(bootstrap)] +use std::fmt; use std::fs::File; use std::hash::{Hash, Hasher}; use std::io::{self, Write}; @@ -30,6 +32,7 @@ use rustc_span::symbol::sym; use rustc_span::{FileName, Span}; use rustc_target::spec::{Target, TargetTuple}; use tempfile::{Builder as TempFileBuilder, TempDir}; +#[cfg(not(bootstrap))] use test::test::{RustdocResult, get_rustdoc_result}; use tracing::debug; @@ -38,6 +41,38 @@ use crate::config::{Options as RustdocOptions, OutputFormat}; use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine}; use crate::lint::init_lints; +#[cfg(bootstrap)] +#[allow(dead_code)] +pub enum RustdocResult { + /// The test failed to compile. + CompileError, + /// The test is marked `compile_fail` but compiled successfully. + UnexpectedCompilePass, + /// The test failed to compile (as expected) but the compiler output did not contain all + /// expected error codes. + MissingErrorCodes(Vec), + /// The test binary was unable to be executed. + ExecutionError(io::Error), + /// The test binary exited with a non-zero exit code. + /// + /// This typically means an assertion in the test failed or another form of panic occurred. + ExecutionFailure(process::Output), + /// The test is marked `should_panic` but the test binary executed successfully. + NoPanic(Option), +} + +#[cfg(bootstrap)] +impl fmt::Display for RustdocResult { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } +} + +#[cfg(bootstrap)] +fn get_rustdoc_result(_: process::Output, _: bool) -> Result<(), RustdocResult> { + Ok(()) +} + /// Type used to display times (compilation and total) information for merged doctests. struct MergedDoctestTimes { total_time: Instant, From 01f4aa013c5c5994794e035e67cd056b3a82e736 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 23:04:54 +0200 Subject: [PATCH 05/11] Update std doctests --- library/std/src/error.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/library/std/src/error.rs b/library/std/src/error.rs index def5f984c88e4..09bfc83ebca6c 100644 --- a/library/std/src/error.rs +++ b/library/std/src/error.rs @@ -123,7 +123,7 @@ use crate::fmt::{self, Write}; /// the `Debug` output means `Report` is an ideal starting place for formatting errors returned /// from `main`. /// -/// ```should_panic +/// ``` /// #![feature(error_reporter)] /// use std::error::Report; /// # use std::error::Error; @@ -154,10 +154,14 @@ use crate::fmt::{self, Write}; /// # Err(SuperError { source: SuperErrorSideKick }) /// # } /// -/// fn main() -> Result<(), Report> { +/// fn run() -> Result<(), Report> { /// get_super_error()?; /// Ok(()) /// } +/// +/// fn main() { +/// assert!(run().is_err()); +/// } /// ``` /// /// This example produces the following output: @@ -170,7 +174,7 @@ use crate::fmt::{self, Write}; /// output format. If you want to make sure your `Report`s are pretty printed and include backtrace /// you will need to manually convert and enable those flags. /// -/// ```should_panic +/// ``` /// #![feature(error_reporter)] /// use std::error::Report; /// # use std::error::Error; @@ -201,12 +205,16 @@ use crate::fmt::{self, Write}; /// # Err(SuperError { source: SuperErrorSideKick }) /// # } /// -/// fn main() -> Result<(), Report> { +/// fn run() -> Result<(), Report> { /// get_super_error() /// .map_err(Report::from) /// .map_err(|r| r.pretty(true).show_backtrace(true))?; /// Ok(()) /// } +/// +/// fn main() { +/// assert!(run().is_err()); +/// } /// ``` /// /// This example produces the following output: From 3a5e078aa8d3274f0766a0fe579a69d1d87ee57e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 16 Oct 2025 15:06:00 +0200 Subject: [PATCH 06/11] Correcty handle `should_panic` on unsupported targets --- library/test/src/lib.rs | 9 ++++++--- src/librustdoc/doctest.rs | 4 ++++ src/librustdoc/doctest/runner.rs | 6 +++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index 8aaf579422f61..b81bdab3e2e35 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -570,6 +570,10 @@ pub fn convert_benchmarks_to_tests(tests: Vec) -> Vec bool { + (cfg!(target_family = "wasm") || cfg!(target_os = "zkvm")) && !cfg!(target_os = "emscripten") +} + pub fn run_test( opts: &TestOpts, force_ignore: bool, @@ -581,9 +585,8 @@ pub fn run_test( let TestDescAndFn { desc, testfn } = test; // Emscripten can catch panics but other wasm targets cannot - let ignore_because_no_process_support = desc.should_panic != ShouldPanic::No - && (cfg!(target_family = "wasm") || cfg!(target_os = "zkvm")) - && !cfg!(target_os = "emscripten"); + let ignore_because_no_process_support = + desc.should_panic != ShouldPanic::No && cannot_handle_should_panic(); if force_ignore || desc.ignore || ignore_because_no_process_support { let message = CompletedTest::new(id, desc, TrIgnored, None, Vec::new()); diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index d0fe4c17431aa..da532be34b7c5 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1120,6 +1120,10 @@ fn doctest_run_fn( rustdoc_options: Arc, unused_externs: Arc>>, ) -> Result<(), String> { + #[cfg(not(bootstrap))] + if scraped_test.langstr.should_panic && test::cannot_handle_should_panic() { + return Ok(()); + } let report_unused_externs = |uext| { unused_externs.lock().unwrap().push(uext); }; diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index 0a4ca67966242..428389b040ca5 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -267,9 +267,13 @@ test::StaticTestFn( runner = if not_running { "test::assert_test_result(Ok::<(), String>(()))".to_string() } else { + // One case to consider: if this is a `should_panic` doctest, on some targets, libtest + // ignores such tests because it's not supported. The first `if` checks exactly that. format!( " -if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{ +if {should_panic} && test::cannot_handle_should_panic() {{ + test::assert_test_result(Ok::<(), String>(())) +}} else if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{ test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}, {should_panic})) }} else {{ test::assert_test_result(doctest_bundle::{test_id}::__main_fn()) From 5b5e21991818b9bf51ea76263dbf16939035857e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 17 Oct 2025 20:41:35 +0200 Subject: [PATCH 07/11] Move code to compile merged doctest and its caller into its own function --- src/librustdoc/doctest.rs | 154 ++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index da532be34b7c5..29fb850ee305f 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -559,6 +559,83 @@ impl RunnableDocTest { } } +fn compile_merged_doctest_and_caller_binary( + mut child: process::Child, + doctest: &RunnableDocTest, + rustdoc_options: &RustdocOptions, + rustc_binary: &Path, + output_file: &Path, + compiler_args: Vec, + test_code: &str, + instant: Instant, +) -> Result)> { + // 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 + )); + + // Deduplicate passed -L directory paths, since usually all dependencies will be in the + // same directory (e.g. target/debug/deps from Cargo). + let mut seen_search_dirs = FxHashSet::default(); + 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(".")); + if seen_search_dirs.insert(dir) { + 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, 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((instant.elapsed(), Err(RustdocResult::CompileError))); + } + if !rustdoc_options.no_capture { + // If `no_capture` 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") + }; + + Ok(process::Output { status, stdout: Vec::new(), stderr: Vec::new() }) +} + /// Execute a `RunnableDoctest`. /// /// This is the function that calculates the compiler command line, invokes the compiler, then @@ -674,7 +751,6 @@ fn run_test( .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", @@ -689,71 +765,19 @@ fn run_test( let mut child = compiler.spawn().expect("Failed to spawn rustc process"); 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 - )); - - // Deduplicate passed -L directory paths, since usually all dependencies will be in the - // same directory (e.g. target/debug/deps from Cargo). - let mut seen_search_dirs = FxHashSet::default(); - 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(".")); - if seen_search_dirs.insert(dir) { - 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 (instant.elapsed(), Err(RustdocResult::CompileError)); - } - if !rustdoc_options.no_capture { - // If `no_capture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. - runner_compiler.stderr(Stdio::null()); + match compile_merged_doctest_and_caller_binary( + child, + &doctest, + rustdoc_options, + rustc_binary, + &output_file, + compiler_args, + merged_test_code, + instant, + ) { + Ok(out) => out, + Err(err) => return err, } - 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"); stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources"); From fc90106892df996833c5527f652231ccce728674 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 18 Oct 2025 02:18:55 +0200 Subject: [PATCH 08/11] Fix `should_panic` in doctest --- src/librustdoc/doctest.rs | 108 ++++++++++++++++++++++++------- src/librustdoc/doctest/make.rs | 6 +- src/librustdoc/doctest/runner.rs | 2 +- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 29fb850ee305f..d68eef179a5d7 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -406,7 +406,7 @@ pub(crate) fn run_tests( // We failed to compile all compatible tests as one so we push them into the // `standalone_tests` doctests. debug!("Failed to compile compatible doctests for edition {} all at once", edition); - for (doctest, scraped_test) in doctests { + for (pos, (doctest, scraped_test)) in doctests.into_iter().enumerate() { doctest.generate_unique_doctest( &scraped_test.text, scraped_test.langstr.test_harness, @@ -419,6 +419,7 @@ pub(crate) fn run_tests( opts.clone(), Arc::clone(rustdoc_options), unused_extern_reports.clone(), + pos, )); } } @@ -548,11 +549,21 @@ pub(crate) struct RunnableDocTest { } impl RunnableDocTest { - 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_bundle(&self, id: Option) -> PathBuf { + let name = if let Some(id) = id { + format!("doctest_bundle_id_{id}.rs") + } else { + format!("doctest_bundle_{}.rs", self.edition) + }; + self.test_opts.outdir.path().join(name) } - fn path_for_merged_doctest_runner(&self) -> PathBuf { - self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition)) + fn path_for_merged_doctest_runner(&self, id: Option) -> PathBuf { + let name = if let Some(id) = id { + format!("doctest_runner_id_{id}.rs") + } else { + format!("doctest_runner_{}.rs", self.edition) + }; + self.test_opts.outdir.path().join(name) } fn is_multiple_tests(&self) -> bool { self.merged_test_code.is_some() @@ -568,13 +579,14 @@ fn compile_merged_doctest_and_caller_binary( compiler_args: Vec, test_code: &str, instant: Instant, + id: Option, ) -> Result)> { // 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 runner_input_file = doctest.path_for_merged_doctest_runner(id); let mut runner_compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); @@ -583,10 +595,14 @@ fn compile_merged_doctest_and_caller_binary( 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 - )); + let mut extern_path = if let Some(id) = id { + std::ffi::OsString::from(format!("--extern=doctest_bundle_id_{id}=")) + } else { + std::ffi::OsString::from(format!( + "--extern=doctest_bundle_{edition}=", + edition = doctest.edition + )) + }; // Deduplicate passed -L directory paths, since usually all dependencies will be in the // same directory (e.g. target/debug/deps from Cargo). @@ -605,11 +621,12 @@ fn compile_merged_doctest_and_caller_binary( } } } - let output_bundle_file = doctest - .test_opts - .outdir - .path() - .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition)); + let filename = if let Some(id) = id { + format!("libdoctest_bundle_id_{id}.rlib") + } else { + format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition) + }; + let output_bundle_file = doctest.test_opts.outdir.path().join(filename); extern_path.push(&output_bundle_file); runner_compiler.arg(extern_path); runner_compiler.arg(&runner_input_file); @@ -647,6 +664,7 @@ fn run_test( rustdoc_options: &RustdocOptions, supports_color: bool, report_unused_externs: impl Fn(UnusedExterns), + doctest_id: usize, ) -> (Duration, Result<(), RustdocResult>) { let langstr = &doctest.langstr; // Make sure we emit well-formed executable names for our target. @@ -727,12 +745,19 @@ fn run_test( compiler.args(&compiler_args); + 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), + ); // 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() || (!langstr.compile_fail && langstr.should_panic) { // 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_bundle(); + let input_file = doctest.path_for_merged_doctest_bundle( + if !langstr.compile_fail && langstr.should_panic { Some(doctest_id) } else { None }, + ); 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. @@ -751,11 +776,6 @@ fn run_test( .arg(input_file); } else { compiler.arg("--crate-type=bin").arg("-o").arg(&output_file); - 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()); @@ -774,6 +794,37 @@ fn run_test( compiler_args, merged_test_code, instant, + None, + ) { + Ok(out) => out, + Err(err) => return err, + } + } else if !langstr.compile_fail && langstr.should_panic { + match compile_merged_doctest_and_caller_binary( + child, + &doctest, + rustdoc_options, + rustc_binary, + &output_file, + compiler_args, + &format!( + "\ +#![feature(test)] +extern crate test; + +use std::process::{{ExitCode, Termination}}; + +fn main() -> ExitCode {{ + if test::cannot_handle_should_panic() {{ + ExitCode::SUCCESS + }} else {{ + extern crate doctest_bundle_id_{doctest_id} as doctest_bundle; + doctest_bundle::main().report() + }} +}}" + ), + instant, + Some(doctest_id), ) { Ok(out) => out, Err(err) => return err, @@ -1087,6 +1138,7 @@ impl CreateRunnableDocTests { self.opts.clone(), Arc::clone(&self.rustdoc_options), self.unused_extern_reports.clone(), + self.standalone_tests.len(), ) } } @@ -1097,6 +1149,7 @@ fn generate_test_desc_and_fn( opts: GlobalTestOptions, rustdoc_options: Arc, unused_externs: Arc>>, + doctest_id: usize, ) -> test::TestDescAndFn { let target_str = rustdoc_options.target.to_string(); let rustdoc_test_options = @@ -1131,6 +1184,7 @@ fn generate_test_desc_and_fn( scraped_test, rustdoc_options, unused_externs, + doctest_id, ) })), } @@ -1143,6 +1197,7 @@ fn doctest_run_fn( scraped_test: ScrapedDocTest, rustdoc_options: Arc, unused_externs: Arc>>, + doctest_id: usize, ) -> Result<(), String> { #[cfg(not(bootstrap))] if scraped_test.langstr.should_panic && test::cannot_handle_should_panic() { @@ -1168,8 +1223,13 @@ fn doctest_run_fn( no_run: scraped_test.no_run(&rustdoc_options), merged_test_code: None, }; - let (_, res) = - run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs); + let (_, res) = run_test( + runnable_test, + &rustdoc_options, + doctest.supports_color, + report_unused_externs, + doctest_id, + ); if let Err(err) = res { eprint!("{err}"); diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 6c177f942fb05..9eb2528bcf6c7 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -388,17 +388,17 @@ impl DocTestBuilder { let (main_pre, main_post) = if returns_result { ( format!( - "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n", + "pub fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n", ), format!("\n}} {inner_fn_name}().unwrap() }}"), ) } else if self.test_id.is_some() { ( - format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), + format!("pub fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), format!("\n}} {inner_fn_name}() }}"), ) } else { - ("fn main() {\n".into(), "\n}".into()) + ("pub fn main() {\n".into(), "\n}".into()) }; // Note on newlines: We insert a line/newline *before*, and *after* // the doctest and adjust the `line_offset` accordingly. diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index 428389b040ca5..45c344cada035 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -197,7 +197,7 @@ std::process::Termination::report(test::test_main(test_args, tests, None)) merged_test_code: Some(code), }; let (duration, ret) = - run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); + run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}, 0); ( duration, if let Err(RustdocResult::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }, From 0f51bb2bc0010d2d2514116ef787a104752441e1 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 18 Oct 2025 23:18:39 +0200 Subject: [PATCH 09/11] Split `should_panic` doctests like merged doctests so we can use `libtest` API directly --- library/test/src/test_result.rs | 1 + src/librustdoc/doctest.rs | 240 +++++++++--------- src/librustdoc/doctest/make.rs | 125 +++++++-- src/librustdoc/doctest/runner.rs | 2 +- src/librustdoc/doctest/tests.rs | 36 +-- .../rustdoc-ui/extract-doctests-result.stdout | 2 +- tests/rustdoc-ui/extract-doctests.stdout | 2 +- tests/rustdoc/playground-arg.rs | 2 +- 8 files changed, 242 insertions(+), 168 deletions(-) diff --git a/library/test/src/test_result.rs b/library/test/src/test_result.rs index dea1831db0509..62cacd9ece972 100644 --- a/library/test/src/test_result.rs +++ b/library/test/src/test_result.rs @@ -158,6 +158,7 @@ pub(crate) fn get_result_from_exit_code( result } +#[derive(Debug)] pub enum RustdocResult { /// The test failed to compile. CompileError, diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index d68eef179a5d7..e438a524a7055 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -406,7 +406,7 @@ pub(crate) fn run_tests( // We failed to compile all compatible tests as one so we push them into the // `standalone_tests` doctests. debug!("Failed to compile compatible doctests for edition {} all at once", edition); - for (pos, (doctest, scraped_test)) in doctests.into_iter().enumerate() { + for (doctest, scraped_test) in doctests { doctest.generate_unique_doctest( &scraped_test.text, scraped_test.langstr.test_harness, @@ -419,7 +419,6 @@ pub(crate) fn run_tests( opts.clone(), Arc::clone(rustdoc_options), unused_extern_reports.clone(), - pos, )); } } @@ -549,21 +548,11 @@ pub(crate) struct RunnableDocTest { } impl RunnableDocTest { - fn path_for_merged_doctest_bundle(&self, id: Option) -> PathBuf { - let name = if let Some(id) = id { - format!("doctest_bundle_id_{id}.rs") - } else { - format!("doctest_bundle_{}.rs", self.edition) - }; - self.test_opts.outdir.path().join(name) + 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, id: Option) -> PathBuf { - let name = if let Some(id) = id { - format!("doctest_runner_id_{id}.rs") - } else { - format!("doctest_runner_{}.rs", self.edition) - }; - self.test_opts.outdir.path().join(name) + 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() @@ -571,7 +560,7 @@ impl RunnableDocTest { } fn compile_merged_doctest_and_caller_binary( - mut child: process::Child, + child: process::Child, doctest: &RunnableDocTest, rustdoc_options: &RustdocOptions, rustc_binary: &Path, @@ -579,14 +568,13 @@ fn compile_merged_doctest_and_caller_binary( compiler_args: Vec, test_code: &str, instant: Instant, - id: Option, + is_compile_fail: bool, ) -> Result)> { // 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(id); + let output = child.wait_with_output().expect("Failed to wait"); + if is_compile_fail && !output.status.success() { + return Ok(output); + } let mut runner_compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); @@ -595,13 +583,10 @@ fn compile_merged_doctest_and_caller_binary( 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 = if let Some(id) = id { - std::ffi::OsString::from(format!("--extern=doctest_bundle_id_{id}=")) + let base_name = if is_compile_fail { + format!("rust_out") } else { - std::ffi::OsString::from(format!( - "--extern=doctest_bundle_{edition}=", - edition = doctest.edition - )) + format!("doctest_bundle_{edition}", edition = doctest.edition) }; // Deduplicate passed -L directory paths, since usually all dependencies will be in the @@ -621,36 +606,58 @@ fn compile_merged_doctest_and_caller_binary( } } } - let filename = if let Some(id) = id { - format!("libdoctest_bundle_id_{id}.rlib") - } else { - format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition) - }; - let output_bundle_file = doctest.test_opts.outdir.path().join(filename); + let output_bundle_file = doctest.test_opts.outdir.path().join(format!("lib{base_name}.rlib")); + let mut extern_path = std::ffi::OsString::from(format!("--extern={base_name}=")); extern_path.push(&output_bundle_file); - runner_compiler.arg(extern_path); - runner_compiler.arg(&runner_input_file); - if std::fs::write(&runner_input_file, 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((instant.elapsed(), Err(RustdocResult::CompileError))); - } - if !rustdoc_options.no_capture { - // If `no_capture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. - runner_compiler.stderr(Stdio::null()); + runner_compiler.arg(&extern_path); + + if is_compile_fail { + add_rustdoc_env_vars(&mut runner_compiler, doctest); + runner_compiler.stderr(Stdio::piped()); + runner_compiler.stdin(Stdio::piped()); + runner_compiler.arg("-"); + } else { + // 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(); + runner_compiler.arg(&runner_input_file); + if std::fs::write(&runner_input_file, 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((instant.elapsed(), Err(RustdocResult::CompileError))); + } + if !rustdoc_options.no_capture { + // If `no_capture` 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"); + } } - runner_compiler.arg("--error-format=short"); debug!("compiler invocation for doctest runner: {runner_compiler:?}"); - let status = if !status.success() { - status + let output = if !output.status.success() { + output } else { let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process"); - child_runner.wait().expect("Failed to wait") + if is_compile_fail { + let stdin = child_runner.stdin.as_mut().expect("Failed to open stdin"); + stdin.write_all(test_code.as_bytes()).expect("could write out test sources"); + } + child_runner.wait_with_output().expect("Failed to wait") }; + if is_compile_fail { + Ok(output) + } else { + Ok(process::Output { status: output.status, stdout: Vec::new(), stderr: Vec::new() }) + } +} - Ok(process::Output { status, stdout: Vec::new(), stderr: Vec::new() }) +fn add_rustdoc_env_vars(compiler: &mut Command, doctest: &RunnableDocTest) { + 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), + ); } /// Execute a `RunnableDoctest`. @@ -664,7 +671,6 @@ fn run_test( rustdoc_options: &RustdocOptions, supports_color: bool, report_unused_externs: impl Fn(UnusedExterns), - doctest_id: usize, ) -> (Duration, Result<(), RustdocResult>) { let langstr = &doctest.langstr; // Make sure we emit well-formed executable names for our target. @@ -695,11 +701,6 @@ fn run_test( 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_args.push("--emit=metadata".to_owned()); - } compiler_args.extend_from_slice(&[ "--target".to_owned(), match &rustdoc_options.target { @@ -745,40 +746,47 @@ fn run_test( compiler.args(&compiler_args); - 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), - ); + let is_should_panic = !langstr.compile_fail && langstr.should_panic; // 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() || (!langstr.compile_fail && langstr.should_panic) { - // 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_bundle( - if !langstr.compile_fail && langstr.should_panic { Some(doctest_id) } else { None }, - ); - 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 (Duration::default(), Err(RustdocResult::CompileError)); - } - if !rustdoc_options.no_capture { - // If `no_capture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. - compiler.stderr(Stdio::null()); - } + if doctest.is_multiple_tests() || is_should_panic { // 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); + compiler.arg("--crate-type=lib").arg("--out-dir").arg(doctest.test_opts.outdir.path()); + + if !is_should_panic { + compiler.arg("--error-format=short"); + if !rustdoc_options.no_capture { + // If `no_capture` is disabled, then we don't display rustc's output when compiling + // the merged doctests. + compiler.stderr(Stdio::null()); + compiler.stdout(Stdio::null()); + } + // It makes the compilation failure much faster if it is for a combined 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 (Duration::default(), Err(RustdocResult::CompileError)); + } + compiler.arg(input_file); + } else { + compiler.stdin(Stdio::piped()); + compiler.stderr(Stdio::piped()); + add_rustdoc_env_vars(&mut compiler, &doctest); + compiler.arg("-"); + } } else { + add_rustdoc_env_vars(&mut compiler, &doctest); compiler.arg("--crate-type=bin").arg("-o").arg(&output_file); compiler.arg("-"); compiler.stdin(Stdio::piped()); compiler.stderr(Stdio::piped()); + + 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_args.push("--emit=metadata".to_owned()); + } } debug!("compiler invocation for doctest: {compiler:?}"); @@ -794,45 +802,48 @@ fn run_test( compiler_args, merged_test_code, instant, - None, + false, ) { Ok(out) => out, Err(err) => return err, } - } else if !langstr.compile_fail && langstr.should_panic { - match compile_merged_doctest_and_caller_binary( - child, - &doctest, - rustdoc_options, - rustc_binary, - &output_file, - compiler_args, - &format!( - "\ + } else { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources"); + + if !langstr.compile_fail && langstr.should_panic { + match compile_merged_doctest_and_caller_binary( + child, + &doctest, + rustdoc_options, + rustc_binary, + &output_file, + compiler_args, + &format!( + "\ #![feature(test)] extern crate test; -use std::process::{{ExitCode, Termination}}; +use std::process::{{ExitCode, Termination, exit}}; fn main() -> ExitCode {{ if test::cannot_handle_should_panic() {{ - ExitCode::SUCCESS + exit(test::ERROR_EXIT_CODE); }} else {{ - extern crate doctest_bundle_id_{doctest_id} as doctest_bundle; + extern crate rust_out as doctest_bundle; doctest_bundle::main().report() }} -}}" - ), - instant, - Some(doctest_id), - ) { - Ok(out) => out, - Err(err) => return err, +}}", + ), + instant, + true, + ) { + Ok(out) => out, + Err(err) => return err, + } + } else { + child.wait_with_output().expect("Failed to read stdout") } - } else { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources"); - child.wait_with_output().expect("Failed to read stdout") }; struct Bomb<'a>(&'a str); @@ -1138,7 +1149,6 @@ impl CreateRunnableDocTests { self.opts.clone(), Arc::clone(&self.rustdoc_options), self.unused_extern_reports.clone(), - self.standalone_tests.len(), ) } } @@ -1149,7 +1159,6 @@ fn generate_test_desc_and_fn( opts: GlobalTestOptions, rustdoc_options: Arc, unused_externs: Arc>>, - doctest_id: usize, ) -> test::TestDescAndFn { let target_str = rustdoc_options.target.to_string(); let rustdoc_test_options = @@ -1184,7 +1193,6 @@ fn generate_test_desc_and_fn( scraped_test, rustdoc_options, unused_externs, - doctest_id, ) })), } @@ -1197,7 +1205,6 @@ fn doctest_run_fn( scraped_test: ScrapedDocTest, rustdoc_options: Arc, unused_externs: Arc>>, - doctest_id: usize, ) -> Result<(), String> { #[cfg(not(bootstrap))] if scraped_test.langstr.should_panic && test::cannot_handle_should_panic() { @@ -1223,13 +1230,8 @@ fn doctest_run_fn( no_run: scraped_test.no_run(&rustdoc_options), merged_test_code: None, }; - let (_, res) = run_test( - runnable_test, - &rustdoc_options, - doctest.supports_color, - report_unused_externs, - doctest_id, - ); + let (_, res) = + run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs); if let Err(err) = res { eprint!("{err}"); diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 9eb2528bcf6c7..cb8b5a37f1961 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use rustc_ast::token::{Delimiter, TokenKind}; use rustc_ast::tokenstream::TokenTree; -use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind}; +use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind, VisibilityKind}; use rustc_errors::emitter::stderr_destination; use rustc_errors::{AutoStream, ColorConfig, DiagCtxtHandle}; use rustc_parse::lexer::StripTokens; @@ -124,7 +124,16 @@ impl<'a> BuildDocTestBuilder<'a> { let result = rustc_driver::catch_fatal_errors(|| { rustc_span::create_session_if_not_set_then(edition, |_| { - parse_source(source, &crate_name, dcx, span) + parse_source( + source, + &crate_name, + dcx, + span, + !can_merge_doctests + && lang_str.is_some_and(|lang_str| { + !lang_str.compile_fail && lang_str.should_panic + }), + ) }) }); @@ -444,6 +453,7 @@ fn parse_source( crate_name: &Option<&str>, parent_dcx: Option>, span: Span, + should_panic: bool, ) -> Result { use rustc_errors::DiagCtxt; use rustc_errors::emitter::{Emitter, HumanEmitter}; @@ -492,8 +502,13 @@ fn parse_source( *prev_span_hi = hi; } - fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool { + fn check_item( + item: &ast::Item, + info: &mut ParseSourceInfo, + crate_name: &Option<&str>, + ) -> (bool, bool) { let mut is_extern_crate = false; + let mut found_main = false; if !info.has_global_allocator && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator)) { @@ -503,6 +518,7 @@ fn parse_source( ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => { if fn_item.ident.name == sym::main { info.has_main_fn = true; + found_main = true; } } ast::ItemKind::ExternCrate(original, ident) => { @@ -521,7 +537,39 @@ fn parse_source( } _ => {} } - is_extern_crate + (is_extern_crate, found_main) + } + + fn push_code( + stmt: &ast::Stmt, + is_extern_crate: bool, + info: &mut ParseSourceInfo, + source: &str, + prev_span_hi: &mut usize, + cut_to_hi: Option, + ) { + // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to + // tweak the span to include the attributes as well. + let mut span = stmt.span; + if let Some(attr) = stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer) { + span = span.with_lo(attr.span.lo()); + } + if let Some(cut_to_hi) = cut_to_hi { + span = span.with_hi(cut_to_hi); + } + if info.everything_else.is_empty() + && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty()) + { + // To keep the doctest code "as close as possible" to the original, we insert + // all the code located between this new span and the previous span which + // might contain code comments and backlines. + push_to_s(&mut info.crates, source, span.shrink_to_lo(), prev_span_hi); + } + if !is_extern_crate { + push_to_s(&mut info.everything_else, source, span, prev_span_hi); + } else { + push_to_s(&mut info.crates, source, span, prev_span_hi); + } } let mut prev_span_hi = 0; @@ -560,7 +608,51 @@ fn parse_source( let mut is_extern_crate = false; match stmt.kind { StmtKind::Item(ref item) => { - is_extern_crate = check_item(item, &mut info, crate_name); + let (found_is_extern_crate, found_main) = + check_item(item, &mut info, crate_name); + is_extern_crate = found_is_extern_crate; + if found_main + && should_panic + && !matches!(item.vis.kind, VisibilityKind::Public) + { + if matches!(item.vis.kind, VisibilityKind::Inherited) { + push_code( + stmt, + is_extern_crate, + &mut info, + source, + &mut prev_span_hi, + Some(item.span.lo()), + ); + } else { + push_code( + stmt, + is_extern_crate, + &mut info, + source, + &mut prev_span_hi, + Some(item.vis.span.lo()), + ); + prev_span_hi += + (item.vis.span.hi().0 - item.vis.span.lo().0) as usize; + }; + if !info + .everything_else + .chars() + .last() + .is_some_and(|c| c.is_whitespace()) + { + info.everything_else.push(' '); + } + info.everything_else.push_str("pub "); + push_to_s( + &mut info.everything_else, + source, + item.span, + &mut prev_span_hi, + ); + continue; + } } // We assume that the macro calls will expand to item(s) even though they could // expand to statements and expressions. @@ -596,28 +688,7 @@ fn parse_source( } StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true, } - - // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to - // tweak the span to include the attributes as well. - let mut span = stmt.span; - if let Some(attr) = - stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer) - { - span = span.with_lo(attr.span.lo()); - } - if info.everything_else.is_empty() - && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty()) - { - // To keep the doctest code "as close as possible" to the original, we insert - // all the code located between this new span and the previous span which - // might contain code comments and backlines. - push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi); - } - if !is_extern_crate { - push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi); - } else { - push_to_s(&mut info.crates, source, span, &mut prev_span_hi); - } + push_code(stmt, is_extern_crate, &mut info, source, &mut prev_span_hi, None); } if has_non_items { if info.has_main_fn diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index 45c344cada035..428389b040ca5 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -197,7 +197,7 @@ std::process::Termination::report(test::test_main(test_args, tests, None)) merged_test_code: Some(code), }; let (duration, ret) = - run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}, 0); + run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); ( duration, if let Err(RustdocResult::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }, diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index ccc3e55a33122..6517b71051ac5 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -45,7 +45,7 @@ fn make_test_basic() { let opts = default_global_opts(""); let input = "assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { assert_eq!(2+2, 4); }" .to_string(); @@ -60,7 +60,7 @@ fn make_test_crate_name_no_use() { let opts = default_global_opts("asdf"); let input = "assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { assert_eq!(2+2, 4); }" .to_string(); @@ -78,7 +78,7 @@ assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] #[allow(unused_extern_crates)] extern crate r#asdf; -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -95,7 +95,7 @@ fn make_test_no_crate_inject() { let input = "use asdf::qwop; assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -113,7 +113,7 @@ fn make_test_ignore_std() { let input = "use std::*; assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { use std::*; assert_eq!(2+2, 4); }" @@ -132,7 +132,7 @@ use asdf::qwop; assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] extern crate asdf; -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -149,7 +149,7 @@ use asdf::qwop; assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] #[macro_use] extern crate asdf; -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -168,7 +168,7 @@ assert_eq!(2+2, 4);"; let expected = "#![feature(sick_rad)] #[allow(unused_extern_crates)] extern crate r#asdf; -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -181,7 +181,7 @@ assert_eq!(2+2, 4); #![feature(hella_dope)] #[allow(unused_extern_crates)] extern crate r#asdf; -fn main() { +pub fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" @@ -211,7 +211,7 @@ assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] #![feature(sick_rad)] -fn main() { +pub fn main() { assert_eq!(2+2, 4); }" .to_string(); @@ -242,7 +242,7 @@ fn make_test_fake_main() { let input = "//Ceci n'est pas une `fn main` assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { //Ceci n'est pas une `fn main` assert_eq!(2+2, 4); }" @@ -273,7 +273,7 @@ fn make_test_issues_21299() { assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { +pub fn main() { // fn main assert_eq!(2+2, 4); }" @@ -294,7 +294,7 @@ assert_eq!(asdf::foo, 4);"; extern crate hella_qwop; #[allow(unused_extern_crates)] extern crate r#asdf; -fn main() { +pub fn main() { assert_eq!(asdf::foo, 4); }" .to_string(); @@ -330,7 +330,7 @@ let mut input = String::new(); io::stdin().read_line(&mut input)?; Ok::<(), io:Error>(())"; let expected = "#![allow(unused)] -fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> { +pub fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> { use std::io; let mut input = String::new(); io::stdin().read_line(&mut input)?; @@ -347,7 +347,7 @@ fn make_test_named_wrapper() { let opts = default_global_opts(""); let input = "assert_eq!(2+2, 4);"; let expected = "#![allow(unused)] -fn main() { #[allow(non_snake_case)] fn _doctest_main__some_unique_name() { +pub fn main() { #[allow(non_snake_case)] fn _doctest_main__some_unique_name() { assert_eq!(2+2, 4); } _doctest_main__some_unique_name() }" .to_string(); @@ -364,7 +364,7 @@ assert_eq!(2+2, 4); eprintln!(\"hello anan\"); "; let expected = "#![allow(unused)] -fn main() { +pub fn main() { use std::*; assert_eq!(2+2, 4); eprintln!(\"hello anan\"); @@ -411,7 +411,7 @@ fn comment_in_attrs() { #![allow(internal_features)] #![doc(rust_logo)] //! This crate has the Rust(tm) branding on it. -fn main() { +pub fn main() { }" .to_string(); @@ -457,7 +457,7 @@ pub mod outer_module { //! A doc comment that applies to the implicit anonymous module of this crate -fn main() { +pub fn main() { pub mod outer_module { //!! - Still an inner line doc (but with a bang at the beginning) } diff --git a/tests/rustdoc-ui/extract-doctests-result.stdout b/tests/rustdoc-ui/extract-doctests-result.stdout index 44e6d33c66268..96d1a0bcb9fd8 100644 --- a/tests/rustdoc-ui/extract-doctests-result.stdout +++ b/tests/rustdoc-ui/extract-doctests-result.stdout @@ -1 +1 @@ -{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests-result.rs","line":8,"doctest_attributes":{"original":"","should_panic":false,"no_run":false,"ignore":"None","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nOk(())","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nOk(())","wrapper":{"before":"fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> {\n","after":"\n} _inner().unwrap() }","returns_result":true}},"name":"$DIR/extract-doctests-result.rs - (line 8)"}]} \ No newline at end of file +{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests-result.rs","line":8,"doctest_attributes":{"original":"","should_panic":false,"no_run":false,"ignore":"None","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nOk(())","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nOk(())","wrapper":{"before":"pub fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> {\n","after":"\n} _inner().unwrap() }","returns_result":true}},"name":"$DIR/extract-doctests-result.rs - (line 8)"}]} \ No newline at end of file diff --git a/tests/rustdoc-ui/extract-doctests.stdout b/tests/rustdoc-ui/extract-doctests.stdout index 796ecd82f1c93..411e5749ba2e2 100644 --- a/tests/rustdoc-ui/extract-doctests.stdout +++ b/tests/rustdoc-ui/extract-doctests.stdout @@ -1 +1 @@ -{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nlet y = 14;","wrapper":{"before":"fn main() {\n","after":"\n}","returns_result":false}},"name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":null,"name":"$DIR/extract-doctests.rs - (line 13)"}]} \ No newline at end of file +{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nlet y = 14;","wrapper":{"before":"pub fn main() {\n","after":"\n}","returns_result":false}},"name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":null,"name":"$DIR/extract-doctests.rs - (line 13)"}]} \ No newline at end of file diff --git a/tests/rustdoc/playground-arg.rs b/tests/rustdoc/playground-arg.rs index e10a31017efc0..02beeb8e4a88a 100644 --- a/tests/rustdoc/playground-arg.rs +++ b/tests/rustdoc/playground-arg.rs @@ -10,4 +10,4 @@ pub fn dummy() {} // ensure that `extern crate foo;` was inserted into code snips automatically: -//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://example.com/?code=%23!%5Ballow(unused)%5D%0A%23%5Ballow(unused_extern_crates)%5D%0Aextern+crate+r%23foo;%0Afn+main()+%7B%0A++++use+foo::dummy;%0A++++dummy();%0A%7D&edition=2015"]' "" +//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://example.com/?code=%23!%5Ballow(unused)%5D%0A%23%5Ballow(unused_extern_crates)%5D%0Aextern+crate+r%23foo;%0Apub+fn+main()+%7B%0A++++use+foo::dummy;%0A++++dummy();%0A%7D&edition=2015"]' "" From 696141377e8d09f8dd8ebc90c33dfb03f50cc93a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 19 Oct 2025 15:06:44 +0200 Subject: [PATCH 10/11] Correctly handle stability when `#![feature(staged_api)]` is used --- src/librustdoc/doctest.rs | 4 +- src/librustdoc/doctest/make.rs | 206 ++++++++++++++++++++------------- 2 files changed, 126 insertions(+), 84 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index e438a524a7055..c958fbecdaaf5 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -785,7 +785,7 @@ fn run_test( 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_args.push("--emit=metadata".to_owned()); + compiler.arg("--emit=metadata".to_owned()); } } @@ -811,7 +811,7 @@ fn run_test( let stdin = child.stdin.as_mut().expect("Failed to open stdin"); stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources"); - if !langstr.compile_fail && langstr.should_panic { + if is_should_panic { match compile_merged_doctest_and_caller_binary( child, &doctest, diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index cb8b5a37f1961..b32501032d22c 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -34,6 +34,7 @@ struct ParseSourceInfo { crates: String, crate_attrs: String, maybe_crate_attrs: String, + need_stability_attr: bool, } /// Builder type for `DocTestBuilder`. @@ -147,6 +148,7 @@ impl<'a> BuildDocTestBuilder<'a> { crates, crate_attrs, maybe_crate_attrs, + need_stability_attr, })) = result else { // If the AST returned an error, we don't want this doctest to be merged with the @@ -184,6 +186,7 @@ impl<'a> BuildDocTestBuilder<'a> { test_id, invalid_ast: false, can_be_merged, + need_stability_attr, } } } @@ -204,6 +207,7 @@ pub(crate) struct DocTestBuilder { pub(crate) test_id: Option, pub(crate) invalid_ast: bool, pub(crate) can_be_merged: bool, + need_stability_attr: bool, } /// Contains needed information for doctest to be correctly generated with expected "wrapping". @@ -301,6 +305,7 @@ impl DocTestBuilder { test_id, invalid_ast: true, can_be_merged: false, + need_stability_attr: false, } } @@ -379,53 +384,58 @@ impl DocTestBuilder { } // FIXME: This code cannot yet handle no_std test cases yet - let wrapper = if dont_insert_main - || self.has_main_fn - || crate_level_code.contains("![no_std]") - { - None - } else { - let returns_result = processed_code.ends_with("(())"); - // Give each doctest main function a unique name. - // This is for example needed for the tooling around `-C instrument-coverage`. - let inner_fn_name = if let Some(ref test_id) = self.test_id { - format!("_doctest_main_{test_id}") + let wrapper = + if dont_insert_main || self.has_main_fn || crate_level_code.contains("![no_std]") { + None } else { - "_inner".into() - }; - let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; - let (main_pre, main_post) = if returns_result { - ( - format!( - "pub fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n", - ), - format!("\n}} {inner_fn_name}().unwrap() }}"), - ) - } else if self.test_id.is_some() { - ( - format!("pub fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), - format!("\n}} {inner_fn_name}() }}"), - ) - } else { - ("pub fn main() {\n".into(), "\n}".into()) + let extra = if self.need_stability_attr { + "#[stable(feature = \"doctest\", since = \"1.0.0\")]\n" + } else { + "" + }; + let returns_result = processed_code.ends_with("(())"); + // Give each doctest main function a unique name. + // This is for example needed for the tooling around `-C instrument-coverage`. + let inner_fn_name = if let Some(ref test_id) = self.test_id { + format!("_doctest_main_{test_id}") + } else { + "_inner".into() + }; + let inner_attr = + if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; + let (main_pre, main_post) = if returns_result { + ( + format!( + "{extra}pub fn main() {{ {inner_attr}fn {inner_fn_name}() \ + -> core::result::Result<(), impl core::fmt::Debug> {{\n", + ), + format!("\n}} {inner_fn_name}().unwrap() }}"), + ) + } else if self.test_id.is_some() { + ( + format!("{extra}pub fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), + format!("\n}} {inner_fn_name}() }}"), + ) + } else { + (format!("{extra}pub fn main() {{\n"), "\n}".into()) + }; + // Note on newlines: We insert a line/newline *before*, and *after* + // the doctest and adjust the `line_offset` accordingly. + // In the case of `-C instrument-coverage`, this means that the generated + // inner `main` function spans from the doctest opening codeblock to the + // closing one. For example + // /// ``` <- start of the inner main + // /// <- code under doctest + // /// ``` <- end of the inner main + line_offset += 1; + + Some(WrapperInfo { + before: main_pre, + after: main_post, + returns_result, + insert_indent_space: opts.insert_indent_space, + }) }; - // Note on newlines: We insert a line/newline *before*, and *after* - // the doctest and adjust the `line_offset` accordingly. - // In the case of `-C instrument-coverage`, this means that the generated - // inner `main` function spans from the doctest opening codeblock to the - // closing one. For example - // /// ``` <- start of the inner main - // /// <- code under doctest - // /// ``` <- end of the inner main - line_offset += 1; - - Some(WrapperInfo { - before: main_pre, - after: main_post, - returns_result, - insert_indent_space: opts.insert_indent_space, - }) - }; ( DocTestWrapResult::Valid { @@ -575,6 +585,7 @@ fn parse_source( let mut prev_span_hi = 0; let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect]; let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No); + let mut need_crate_stability_attr = false; let result = match parsed { Ok(Some(ref item)) @@ -600,10 +611,28 @@ fn parse_source( ); } } else { + if !info.need_stability_attr + && attr.has_name(sym::feature) + && let Some(sub_attrs) = attr.meta_item_list() + && sub_attrs.iter().any(|attr| { + if let ast::MetaItemInner::MetaItem(attr) = attr { + matches!(attr.kind, ast::MetaItemKind::Word) + && attr.has_name(sym::staged_api) + } else { + false + } + }) + { + info.need_stability_attr = true; + } + if attr.has_any_name(&[sym::stable, sym::unstable]) { + need_crate_stability_attr = false; + } push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi); } } let mut has_non_items = false; + let mut fn_main_pos = None; for stmt in &body.stmts { let mut is_extern_crate = false; match stmt.kind { @@ -611,47 +640,47 @@ fn parse_source( let (found_is_extern_crate, found_main) = check_item(item, &mut info, crate_name); is_extern_crate = found_is_extern_crate; - if found_main - && should_panic - && !matches!(item.vis.kind, VisibilityKind::Public) - { - if matches!(item.vis.kind, VisibilityKind::Inherited) { - push_code( - stmt, - is_extern_crate, - &mut info, - source, - &mut prev_span_hi, - Some(item.span.lo()), - ); - } else { - push_code( - stmt, - is_extern_crate, - &mut info, + if found_main { + fn_main_pos = Some(info.everything_else.len()); + if !matches!(item.vis.kind, VisibilityKind::Public) && should_panic { + if matches!(item.vis.kind, VisibilityKind::Inherited) { + push_code( + stmt, + is_extern_crate, + &mut info, + source, + &mut prev_span_hi, + Some(item.span.lo()), + ); + } else { + push_code( + stmt, + is_extern_crate, + &mut info, + source, + &mut prev_span_hi, + Some(item.vis.span.lo()), + ); + prev_span_hi += + (item.vis.span.hi().0 - item.vis.span.lo().0) as usize; + }; + if !info + .everything_else + .chars() + .last() + .is_some_and(|c| c.is_whitespace()) + { + info.everything_else.push(' '); + } + info.everything_else.push_str("pub "); + push_to_s( + &mut info.everything_else, source, + item.span, &mut prev_span_hi, - Some(item.vis.span.lo()), ); - prev_span_hi += - (item.vis.span.hi().0 - item.vis.span.lo().0) as usize; - }; - if !info - .everything_else - .chars() - .last() - .is_some_and(|c| c.is_whitespace()) - { - info.everything_else.push(' '); + continue; } - info.everything_else.push_str("pub "); - push_to_s( - &mut info.everything_else, - source, - item.span, - &mut prev_span_hi, - ); - continue; } } // We assume that the macro calls will expand to item(s) even though they could @@ -690,6 +719,19 @@ fn parse_source( } push_code(stmt, is_extern_crate, &mut info, source, &mut prev_span_hi, None); } + if info.need_stability_attr { + if let Some(fn_main_pos) = fn_main_pos { + info.everything_else.insert_str( + fn_main_pos, + "#[stable(feature = \"doctest\", since = \"1.0.0\")]\n", + ); + info.need_stability_attr = false; + } + if need_crate_stability_attr { + info.crate_attrs + .push_str("#![stable(feature = \"doctest\", since = \"1.0.0\")]\n"); + } + } if has_non_items { if info.has_main_fn && let Some(dcx) = parent_dcx From 20bebfd8337ea8778cc435f32b351f8ecd95ede9 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 30 Oct 2025 22:58:47 +0100 Subject: [PATCH 11/11] Try fixing multiple std found error --- src/librustdoc/doctest.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index c958fbecdaaf5..4a9d62461dc87 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -568,11 +568,11 @@ fn compile_merged_doctest_and_caller_binary( compiler_args: Vec, test_code: &str, instant: Instant, - is_compile_fail: bool, + should_panic: bool, ) -> Result)> { // compile-fail tests never get merged, so this should always pass let output = child.wait_with_output().expect("Failed to wait"); - if is_compile_fail && !output.status.success() { + if !output.status.success() { return Ok(output); } @@ -583,7 +583,7 @@ fn compile_merged_doctest_and_caller_binary( runner_compiler.env("RUSTC_BOOTSTRAP", "1"); runner_compiler.args(compiler_args); runner_compiler.args(["--crate-type=bin", "-o"]).arg(output_file); - let base_name = if is_compile_fail { + let base_name = if should_panic { format!("rust_out") } else { format!("doctest_bundle_{edition}", edition = doctest.edition) @@ -611,7 +611,12 @@ fn compile_merged_doctest_and_caller_binary( extern_path.push(&output_bundle_file); runner_compiler.arg(&extern_path); - if is_compile_fail { + let sysroot = &rustdoc_options.sysroot; + if let Some(explicit_sysroot) = &sysroot.explicit { + runner_compiler.arg(format!("--sysroot={}", explicit_sysroot.display())); + } + + if should_panic { add_rustdoc_env_vars(&mut runner_compiler, doctest); runner_compiler.stderr(Stdio::piped()); runner_compiler.stdin(Stdio::piped()); @@ -635,17 +640,13 @@ fn compile_merged_doctest_and_caller_binary( } debug!("compiler invocation for doctest runner: {runner_compiler:?}"); - let output = if !output.status.success() { - output - } else { - let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process"); - if is_compile_fail { - let stdin = child_runner.stdin.as_mut().expect("Failed to open stdin"); - stdin.write_all(test_code.as_bytes()).expect("could write out test sources"); - } - child_runner.wait_with_output().expect("Failed to wait") - }; - if is_compile_fail { + let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process"); + if should_panic { + let stdin = child_runner.stdin.as_mut().expect("Failed to open stdin"); + stdin.write_all(test_code.as_bytes()).expect("could write out test sources"); + } + let output = child_runner.wait_with_output().expect("Failed to wait"); + if should_panic { Ok(output) } else { Ok(process::Output { status: output.status, stdout: Vec::new(), stderr: Vec::new() })