Skip to content

Commit c20d6b7

Browse files
Fix should_panic on merged doctests
1 parent 6818777 commit c20d6b7

File tree

7 files changed

+139
-120
lines changed

7 files changed

+139
-120
lines changed

library/test/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ pub mod test {
4545
pub use crate::cli::{TestOpts, parse_opts};
4646
pub use crate::helpers::metrics::{Metric, MetricMap};
4747
pub use crate::options::{Options, RunIgnored, RunStrategy, ShouldPanic};
48-
pub use crate::test_result::{TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk};
48+
pub use crate::test_result::{
49+
RustdocResult, TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk, get_rustdoc_result,
50+
};
4951
pub use crate::time::{TestExecTime, TestTimeOptions};
5052
pub use crate::types::{
5153
DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc,

library/test/src/test_result.rs

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::any::Any;
22
#[cfg(unix)]
33
use std::os::unix::process::ExitStatusExt;
4-
use std::process::ExitStatus;
4+
use std::process::{ExitStatus, Output};
5+
use std::{fmt, io};
56

67
pub use self::TestResult::*;
78
use super::bench::BenchSamples;
@@ -103,15 +104,14 @@ pub(crate) fn calc_result(
103104
result
104105
}
105106

106-
/// Creates a `TestResult` depending on the exit code of test subprocess.
107-
pub(crate) fn get_result_from_exit_code(
108-
desc: &TestDesc,
107+
/// Creates a `TestResult` depending on the exit code of test subprocess
108+
pub(crate) fn get_result_from_exit_code_inner(
109109
status: ExitStatus,
110-
time_opts: Option<&time::TestTimeOptions>,
111-
exec_time: Option<&time::TestExecTime>,
110+
success_error_code: i32,
112111
) -> TestResult {
113-
let result = match status.code() {
114-
Some(TR_OK) => TestResult::TrOk,
112+
match status.code() {
113+
Some(error_code) if error_code == success_error_code => TestResult::TrOk,
114+
Some(crate::ERROR_EXIT_CODE) => TestResult::TrFailed,
115115
#[cfg(windows)]
116116
Some(STATUS_FAIL_FAST_EXCEPTION) => TestResult::TrFailed,
117117
#[cfg(unix)]
@@ -131,7 +131,17 @@ pub(crate) fn get_result_from_exit_code(
131131
Some(code) => TestResult::TrFailedMsg(format!("got unexpected return code {code}")),
132132
#[cfg(not(any(windows, unix)))]
133133
Some(_) => TestResult::TrFailed,
134-
};
134+
}
135+
}
136+
137+
/// Creates a `TestResult` depending on the exit code of test subprocess and on its runtime.
138+
pub(crate) fn get_result_from_exit_code(
139+
desc: &TestDesc,
140+
status: ExitStatus,
141+
time_opts: Option<&time::TestTimeOptions>,
142+
exec_time: Option<&time::TestExecTime>,
143+
) -> TestResult {
144+
let result = get_result_from_exit_code_inner(status, TR_OK);
135145

136146
// If test is already failed (or allowed to fail), do not change the result.
137147
if result != TestResult::TrOk {
@@ -147,3 +157,92 @@ pub(crate) fn get_result_from_exit_code(
147157

148158
result
149159
}
160+
161+
pub enum RustdocResult {
162+
/// The test failed to compile.
163+
CompileError,
164+
/// The test is marked `compile_fail` but compiled successfully.
165+
UnexpectedCompilePass,
166+
/// The test failed to compile (as expected) but the compiler output did not contain all
167+
/// expected error codes.
168+
MissingErrorCodes(Vec<String>),
169+
/// The test binary was unable to be executed.
170+
ExecutionError(io::Error),
171+
/// The test binary exited with a non-zero exit code.
172+
///
173+
/// This typically means an assertion in the test failed or another form of panic occurred.
174+
ExecutionFailure(Output),
175+
/// The test is marked `should_panic` but the test binary executed successfully.
176+
NoPanic(Option<String>),
177+
}
178+
179+
impl fmt::Display for RustdocResult {
180+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181+
match self {
182+
Self::CompileError => {
183+
write!(f, "Couldn't compile the test.")
184+
}
185+
Self::UnexpectedCompilePass => {
186+
write!(f, "Test compiled successfully, but it's marked `compile_fail`.")
187+
}
188+
Self::NoPanic(msg) => {
189+
write!(f, "Test didn't panic, but it's marked `should_panic`")?;
190+
if let Some(msg) = msg {
191+
write!(f, " ({msg})")?;
192+
}
193+
f.write_str(".")
194+
}
195+
Self::MissingErrorCodes(codes) => {
196+
write!(f, "Some expected error codes were not found: {codes:?}")
197+
}
198+
Self::ExecutionError(err) => {
199+
write!(f, "Couldn't run the test: {err}")?;
200+
if err.kind() == io::ErrorKind::PermissionDenied {
201+
f.write_str(" - maybe your tempdir is mounted with noexec?")?;
202+
}
203+
Ok(())
204+
}
205+
Self::ExecutionFailure(out) => {
206+
writeln!(f, "Test executable failed ({reason}).", reason = out.status)?;
207+
208+
// FIXME(#12309): An unfortunate side-effect of capturing the test
209+
// executable's output is that the relative ordering between the test's
210+
// stdout and stderr is lost. However, this is better than the
211+
// alternative: if the test executable inherited the parent's I/O
212+
// handles the output wouldn't be captured at all, even on success.
213+
//
214+
// The ordering could be preserved if the test process' stderr was
215+
// redirected to stdout, but that functionality does not exist in the
216+
// standard library, so it may not be portable enough.
217+
let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
218+
let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
219+
220+
if !stdout.is_empty() || !stderr.is_empty() {
221+
writeln!(f)?;
222+
223+
if !stdout.is_empty() {
224+
writeln!(f, "stdout:\n{stdout}")?;
225+
}
226+
227+
if !stderr.is_empty() {
228+
writeln!(f, "stderr:\n{stderr}")?;
229+
}
230+
}
231+
Ok(())
232+
}
233+
}
234+
}
235+
}
236+
237+
pub fn get_rustdoc_result(output: Output, should_panic: bool) -> Result<(), RustdocResult> {
238+
let result = get_result_from_exit_code_inner(output.status, 0);
239+
match (result, should_panic) {
240+
(TestResult::TrFailed, true) | (TestResult::TrOk, false) => Ok(()),
241+
(TestResult::TrOk, true) => Err(RustdocResult::NoPanic(None)),
242+
(TestResult::TrFailedMsg(msg), true) => Err(RustdocResult::NoPanic(Some(msg))),
243+
(TestResult::TrFailedMsg(_) | TestResult::TrFailed, false) => {
244+
Err(RustdocResult::ExecutionFailure(output))
245+
}
246+
_ => unreachable!("unexpected status for rustdoc test output"),
247+
}
248+
}

src/librustdoc/doctest.rs

Lines changed: 13 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use rustc_span::symbol::sym;
3030
use rustc_span::{FileName, Span};
3131
use rustc_target::spec::{Target, TargetTuple};
3232
use tempfile::{Builder as TempFileBuilder, TempDir};
33+
use test::test::{RustdocResult, get_rustdoc_result};
3334
use tracing::debug;
3435

3536
use self::rust::HirCollector;
@@ -445,25 +446,6 @@ fn scrape_test_config(
445446
opts
446447
}
447448

448-
/// Documentation test failure modes.
449-
enum TestFailure {
450-
/// The test failed to compile.
451-
CompileError,
452-
/// The test is marked `compile_fail` but compiled successfully.
453-
UnexpectedCompilePass,
454-
/// The test failed to compile (as expected) but the compiler output did not contain all
455-
/// expected error codes.
456-
MissingErrorCodes(Vec<String>),
457-
/// The test binary was unable to be executed.
458-
ExecutionError(io::Error),
459-
/// The test binary exited with a non-zero exit code.
460-
///
461-
/// This typically means an assertion in the test failed or another form of panic occurred.
462-
ExecutionFailure(process::Output),
463-
/// The test is marked `should_panic` but the test binary executed successfully.
464-
UnexpectedRunPass,
465-
}
466-
467449
enum DirState {
468450
Temp(TempDir),
469451
Perm(PathBuf),
@@ -553,7 +535,7 @@ fn run_test(
553535
rustdoc_options: &RustdocOptions,
554536
supports_color: bool,
555537
report_unused_externs: impl Fn(UnusedExterns),
556-
) -> (Duration, Result<(), TestFailure>) {
538+
) -> (Duration, Result<(), RustdocResult>) {
557539
let langstr = &doctest.langstr;
558540
// Make sure we emit well-formed executable names for our target.
559541
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
@@ -642,7 +624,7 @@ fn run_test(
642624
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
643625
// If we cannot write this file for any reason, we leave. All combined tests will be
644626
// tested as standalone tests.
645-
return (Duration::default(), Err(TestFailure::CompileError));
627+
return (Duration::default(), Err(RustdocResult::CompileError));
646628
}
647629
if !rustdoc_options.no_capture {
648630
// If `no_capture` is disabled, then we don't display rustc's output when compiling
@@ -719,7 +701,7 @@ fn run_test(
719701
if std::fs::write(&runner_input_file, merged_test_code).is_err() {
720702
// If we cannot write this file for any reason, we leave. All combined tests will be
721703
// tested as standalone tests.
722-
return (instant.elapsed(), Err(TestFailure::CompileError));
704+
return (instant.elapsed(), Err(RustdocResult::CompileError));
723705
}
724706
if !rustdoc_options.no_capture {
725707
// If `no_capture` is disabled, then we don't display rustc's output when compiling
@@ -772,7 +754,7 @@ fn run_test(
772754
let _bomb = Bomb(&out);
773755
match (output.status.success(), langstr.compile_fail) {
774756
(true, true) => {
775-
return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass));
757+
return (instant.elapsed(), Err(RustdocResult::UnexpectedCompilePass));
776758
}
777759
(true, false) => {}
778760
(false, true) => {
@@ -788,12 +770,15 @@ fn run_test(
788770
.collect();
789771

790772
if !missing_codes.is_empty() {
791-
return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes)));
773+
return (
774+
instant.elapsed(),
775+
Err(RustdocResult::MissingErrorCodes(missing_codes)),
776+
);
792777
}
793778
}
794779
}
795780
(false, false) => {
796-
return (instant.elapsed(), Err(TestFailure::CompileError));
781+
return (instant.elapsed(), Err(RustdocResult::CompileError));
797782
}
798783
}
799784

@@ -831,17 +816,9 @@ fn run_test(
831816
cmd.output()
832817
};
833818
match result {
834-
Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
835-
Ok(out) => {
836-
if langstr.should_panic && out.status.success() {
837-
return (duration, Err(TestFailure::UnexpectedRunPass));
838-
} else if !langstr.should_panic && !out.status.success() {
839-
return (duration, Err(TestFailure::ExecutionFailure(out)));
840-
}
841-
}
819+
Err(e) => (duration, Err(RustdocResult::ExecutionError(e))),
820+
Ok(output) => (duration, get_rustdoc_result(output, langstr.should_panic)),
842821
}
843-
844-
(duration, Ok(()))
845822
}
846823

847824
/// Converts a path intended to use as a command to absolute if it is
@@ -1132,54 +1109,7 @@ fn doctest_run_fn(
11321109
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
11331110

11341111
if let Err(err) = res {
1135-
match err {
1136-
TestFailure::CompileError => {
1137-
eprint!("Couldn't compile the test.");
1138-
}
1139-
TestFailure::UnexpectedCompilePass => {
1140-
eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1141-
}
1142-
TestFailure::UnexpectedRunPass => {
1143-
eprint!("Test executable succeeded, but it's marked `should_panic`.");
1144-
}
1145-
TestFailure::MissingErrorCodes(codes) => {
1146-
eprint!("Some expected error codes were not found: {codes:?}");
1147-
}
1148-
TestFailure::ExecutionError(err) => {
1149-
eprint!("Couldn't run the test: {err}");
1150-
if err.kind() == io::ErrorKind::PermissionDenied {
1151-
eprint!(" - maybe your tempdir is mounted with noexec?");
1152-
}
1153-
}
1154-
TestFailure::ExecutionFailure(out) => {
1155-
eprintln!("Test executable failed ({reason}).", reason = out.status);
1156-
1157-
// FIXME(#12309): An unfortunate side-effect of capturing the test
1158-
// executable's output is that the relative ordering between the test's
1159-
// stdout and stderr is lost. However, this is better than the
1160-
// alternative: if the test executable inherited the parent's I/O
1161-
// handles the output wouldn't be captured at all, even on success.
1162-
//
1163-
// The ordering could be preserved if the test process' stderr was
1164-
// redirected to stdout, but that functionality does not exist in the
1165-
// standard library, so it may not be portable enough.
1166-
let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1167-
let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1168-
1169-
if !stdout.is_empty() || !stderr.is_empty() {
1170-
eprintln!();
1171-
1172-
if !stdout.is_empty() {
1173-
eprintln!("stdout:\n{stdout}");
1174-
}
1175-
1176-
if !stderr.is_empty() {
1177-
eprintln!("stderr:\n{stderr}");
1178-
}
1179-
}
1180-
}
1181-
}
1182-
1112+
eprint!("{err}");
11831113
panic::resume_unwind(Box::new(()));
11841114
}
11851115
Ok(())

0 commit comments

Comments
 (0)