From 507a90b79bc14efe0880c67601de9773aeea043f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 13 Jul 2025 20:38:12 +0200 Subject: [PATCH 1/9] 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 2ab4052fedff9..285c3d7ed9c3d 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -351,7 +351,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 630702bfa13e7a8de1546e70243143cf49d57d66 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:23:15 +0200 Subject: [PATCH 2/9] 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 285c3d7ed9c3d..62e67c39ef148 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; @@ -446,25 +447,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), @@ -554,7 +536,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); @@ -643,7 +625,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.nocapture { // If `nocapture` is disabled, then we don't display rustc's output when compiling @@ -720,7 +702,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.nocapture { // If `nocapture` is disabled, then we don't display rustc's output when compiling @@ -773,7 +755,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) => { @@ -789,12 +771,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)); } } @@ -832,17 +817,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 @@ -1136,54 +1113,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 b292354589f1539e5b2082850858e66deafe459a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:37:31 +0200 Subject: [PATCH 3/9] 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 a47e09f3903d22393c2f4bb5627a41c5089d426d Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 14:22:30 +0200 Subject: [PATCH 4/9] 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 62e67c39ef148..117f9999281b1 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 adaf3e087754dab61c44cb5c33e294708c0653df Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 23:04:54 +0200 Subject: [PATCH 5/9] 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 0bffe4fa179d68a613699f3b366a198dbcfed527 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 16 Oct 2025 15:06:00 +0200 Subject: [PATCH 6/9] 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 117f9999281b1..c7416beddec37 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1124,6 +1124,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 1d719a49a436ff0b98641cbd000c8065b0fcb7bf Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 17 Oct 2025 20:41:35 +0200 Subject: [PATCH 7/9] 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 c7416beddec37..522d2315ba072 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -560,6 +560,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.nocapture { + // If `nocapture` is disabled, then we don't display rustc's output when compiling + // the merged doctests. + runner_compiler.stderr(Stdio::null()); + } + runner_compiler.arg("--error-format=short"); + debug!("compiler invocation for doctest runner: {runner_compiler:?}"); + + let status = if !status.success() { + status + } else { + let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process"); + child_runner.wait().expect("Failed to wait") + }; + + 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 @@ -675,7 +752,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", @@ -690,71 +766,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.nocapture { - // If `nocapture` 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 0ae80cc0eac254932cb54321245e19476ee2c76a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 18 Oct 2025 02:18:55 +0200 Subject: [PATCH 8/9] 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 522d2315ba072..1d8ad4dfbff59 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -407,7 +407,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, @@ -420,6 +420,7 @@ pub(crate) fn run_tests( opts.clone(), Arc::clone(rustdoc_options), unused_extern_reports.clone(), + pos, )); } } @@ -549,11 +550,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() @@ -569,13 +580,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); @@ -584,10 +596,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). @@ -606,11 +622,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); @@ -648,6 +665,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. @@ -728,12 +746,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. @@ -752,11 +777,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()); @@ -775,6 +795,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, @@ -1091,6 +1142,7 @@ impl CreateRunnableDocTests { self.opts.clone(), Arc::clone(&self.rustdoc_options), self.unused_extern_reports.clone(), + self.standalone_tests.len(), ) } } @@ -1101,6 +1153,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 = @@ -1135,6 +1188,7 @@ fn generate_test_desc_and_fn( scraped_test, rustdoc_options, unused_externs, + doctest_id, ) })), } @@ -1147,6 +1201,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() { @@ -1172,8 +1227,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 5eaadc9eb4510..7bd39a9c83207 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 bd3fe054f045cef9c50d9027e3b420f73e7da3a4 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 18 Oct 2025 23:18:39 +0200 Subject: [PATCH 9/9] 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 | 236 +++++++++--------- 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, 240 insertions(+), 166 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 1d8ad4dfbff59..acc599057d5a4 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -407,7 +407,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, @@ -420,7 +420,6 @@ pub(crate) fn run_tests( opts.clone(), Arc::clone(rustdoc_options), unused_extern_reports.clone(), - pos, )); } } @@ -550,21 +549,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() @@ -572,7 +561,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, @@ -580,14 +569,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); @@ -596,13 +584,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 @@ -622,36 +607,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.nocapture { - // If `nocapture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. - runner_compiler.stderr(Stdio::null()); + runner_compiler.arg(&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.nocapture { + // If `nocapture` is disabled, then we don't display rustc's output when compiling + // the merged doctests. + runner_compiler.stderr(Stdio::null()); + runner_compiler.arg("--error-format=short"); + } } - 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`. @@ -665,7 +672,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. @@ -696,11 +702,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 { @@ -746,40 +747,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.nocapture { - // If `nocapture` 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.nocapture { + // If `nocapture` 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:?}"); @@ -795,21 +803,25 @@ 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; @@ -819,21 +831,20 @@ fn main() -> ExitCode {{ if test::cannot_handle_should_panic() {{ ExitCode::SUCCESS }} 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); @@ -1142,7 +1153,6 @@ impl CreateRunnableDocTests { self.opts.clone(), Arc::clone(&self.rustdoc_options), self.unused_extern_reports.clone(), - self.standalone_tests.len(), ) } } @@ -1153,7 +1163,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 = @@ -1188,7 +1197,6 @@ fn generate_test_desc_and_fn( scraped_test, rustdoc_options, unused_externs, - doctest_id, ) })), } @@ -1201,7 +1209,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() { @@ -1227,13 +1234,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 7bd39a9c83207..862cc257a7b1f 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::{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"]' ""