@@ -39,6 +39,7 @@ impl DocTestRunner {
3939 doctest : & DocTestBuilder ,
4040 scraped_test : & ScrapedDocTest ,
4141 target_str : & str ,
42+ opts : & RustdocOptions ,
4243 ) {
4344 let ignore = match scraped_test. langstr . ignore {
4445 Ignore :: All => true ,
@@ -62,6 +63,7 @@ impl DocTestRunner {
6263 self . nb_tests,
6364 & mut self . output,
6465 & mut self . output_merged_tests,
66+ opts,
6567 ) ,
6668 ) ) ;
6769 self . supports_color &= doctest. supports_color ;
@@ -121,26 +123,81 @@ impl DocTestRunner {
121123 {output}
122124
123125mod __doctest_mod {{
124- use std::sync::OnceLock;
126+ #[cfg(unix)]
127+ use std::os::unix::process::ExitStatusExt;
125128 use std::path::PathBuf;
126129 use std::process::ExitCode;
130+ use std::sync::OnceLock;
127131
128132 pub static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
129133 pub const RUN_OPTION: &str = \" RUSTDOC_DOCTEST_RUN_NB_TEST\" ;
134+ pub const SHOULD_PANIC_DISABLED: bool = (
135+ cfg!(target_family = \" wasm\" ) || cfg!(target_os = \" zkvm\" )
136+ ) && !cfg!(target_os = \" emscripten\" );
130137
131138 #[allow(unused)]
132139 pub fn doctest_path() -> Option<&'static PathBuf> {{
133140 self::BINARY_PATH.get()
134141 }}
135142
136143 #[allow(unused)]
137- pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{
144+ pub fn doctest_runner(bin: &std::path::Path, test_nb: usize, should_panic: bool ) -> ExitCode {{
138145 let out = std::process::Command::new(bin)
139146 .env(self::RUN_OPTION, test_nb.to_string())
140147 .args(std::env::args().skip(1).collect::<Vec<_>>())
141148 .output()
142149 .expect(\" failed to run command\" );
143- if !out.status.success() {{
150+ if should_panic {{
151+ // FIXME: Make `test::get_result_from_exit_code` public and use this code instead of this.
152+ //
153+ // On Zircon (the Fuchsia kernel), an abort from userspace calls the
154+ // LLVM implementation of __builtin_trap(), e.g., ud2 on x86, which
155+ // raises a kernel exception. If a userspace process does not
156+ // otherwise arrange exception handling, the kernel kills the process
157+ // with this return code.
158+ #[cfg(target_os = \" fuchsia\" )]
159+ const ZX_TASK_RETCODE_EXCEPTION_KILL: i32 = -1028;
160+ // On Windows we use __fastfail to abort, which is documented to use this
161+ // exception code.
162+ #[cfg(windows)]
163+ const STATUS_FAIL_FAST_EXCEPTION: i32 = 0xC0000409u32 as i32;
164+ #[cfg(unix)]
165+ const SIGABRT: std::ffi::c_int = 6;
166+
167+ match out.status.code() {{
168+ Some(test::ERROR_EXIT_CODE) => ExitCode::SUCCESS,
169+ #[cfg(windows)]
170+ Some(STATUS_FAIL_FAST_EXCEPTION) => ExitCode::SUCCESS,
171+ #[cfg(unix)]
172+ None => match out.status.signal() {{
173+ Some(SIGABRT) => ExitCode::SUCCESS,
174+ Some(signal) => {{
175+ eprintln!(\" Test didn't panic, but it's marked `should_panic` (exit signal: {{signal}}).\" );
176+ ExitCode::FAILURE
177+ }}
178+ None => {{
179+ eprintln!(\" Test didn't panic, but it's marked `should_panic` and exited with no error code and no signal.\" );
180+ ExitCode::FAILURE
181+ }}
182+ }},
183+ #[cfg(not(unix))]
184+ None => {{
185+ eprintln!(\" Test didn't panic, but it's marked `should_panic`.\" );
186+ ExitCode::FAILURE
187+ }}
188+ // Upon an abort, Fuchsia returns the status code ZX_TASK_RETCODE_EXCEPTION_KILL.
189+ #[cfg(target_os = \" fuchsia\" )]
190+ Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => ExitCode::SUCCESS,
191+ Some(exit_code) => {{
192+ if !out.status.success() {{
193+ eprintln!(\" Test didn't panic, but it's marked `should_panic` (exit status: {{exit_code}}).\" );
194+ }} else {{
195+ eprintln!(\" Test didn't panic, but it's marked `should_panic`.\" );
196+ }}
197+ ExitCode::FAILURE
198+ }}
199+ }}
200+ }} else if !out.status.success() {{
144201 if let Some(code) = out.status.code() {{
145202 eprintln!(\" Test executable failed (exit status: {{code}}).\" );
146203 }} else {{
@@ -223,6 +280,7 @@ fn generate_mergeable_doctest(
223280 id : usize ,
224281 output : & mut String ,
225282 output_merged_tests : & mut String ,
283+ opts : & RustdocOptions ,
226284) -> String {
227285 let test_id = format ! ( "__doctest_{id}" ) ;
228286
@@ -256,31 +314,33 @@ fn main() {returns_result} {{
256314 )
257315 . unwrap ( ) ;
258316 }
259- let not_running = ignore || scraped_test. langstr . no_run ;
317+ let should_panic = scraped_test. langstr . should_panic ;
318+ let not_running = ignore || scraped_test. no_run ( opts) ;
260319 writeln ! (
261320 output_merged_tests,
262321 "
263322mod {test_id} {{
264323pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
265- {test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic} ,
324+ {test_name:?}, {ignore} || ({should_panic} && crate::__doctest_mod::SHOULD_PANIC_DISABLED) , {file:?}, {line}, {no_run}, false ,
266325test::StaticTestFn(
267326 || {{{runner}}},
268327));
269328}}" ,
270329 test_name = scraped_test. name,
271330 file = scraped_test. path( ) ,
272331 line = scraped_test. line,
273- no_run = scraped_test. langstr. no_run,
274- should_panic = !scraped_test. langstr. no_run && scraped_test. langstr. should_panic,
332+ no_run = scraped_test. no_run( opts) ,
275333 // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
276334 // don't give it the function to run.
277335 runner = if not_running {
278336 "test::assert_test_result(Ok::<(), String>(()))" . to_string( )
279337 } else {
280338 format!(
281339 "
282- if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
283- test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
340+ if {should_panic} && crate::__doctest_mod::SHOULD_PANIC_DISABLED {{
341+ test::assert_test_result(Ok::<(), String>(()))
342+ }} else if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
343+ test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}, {should_panic}))
284344}} else {{
285345 test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
286346}}
0 commit comments