Skip to content

Commit 630702b

Browse files
Fix should_panic on merged doctests
1 parent 507a90b commit 630702b

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;
@@ -446,25 +447,6 @@ fn scrape_test_config(
446447
opts
447448
}
448449

449-
/// Documentation test failure modes.
450-
enum TestFailure {
451-
/// The test failed to compile.
452-
CompileError,
453-
/// The test is marked `compile_fail` but compiled successfully.
454-
UnexpectedCompilePass,
455-
/// The test failed to compile (as expected) but the compiler output did not contain all
456-
/// expected error codes.
457-
MissingErrorCodes(Vec<String>),
458-
/// The test binary was unable to be executed.
459-
ExecutionError(io::Error),
460-
/// The test binary exited with a non-zero exit code.
461-
///
462-
/// This typically means an assertion in the test failed or another form of panic occurred.
463-
ExecutionFailure(process::Output),
464-
/// The test is marked `should_panic` but the test binary executed successfully.
465-
UnexpectedRunPass,
466-
}
467-
468450
enum DirState {
469451
Temp(TempDir),
470452
Perm(PathBuf),
@@ -554,7 +536,7 @@ fn run_test(
554536
rustdoc_options: &RustdocOptions,
555537
supports_color: bool,
556538
report_unused_externs: impl Fn(UnusedExterns),
557-
) -> (Duration, Result<(), TestFailure>) {
539+
) -> (Duration, Result<(), RustdocResult>) {
558540
let langstr = &doctest.langstr;
559541
// Make sure we emit well-formed executable names for our target.
560542
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
@@ -643,7 +625,7 @@ fn run_test(
643625
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
644626
// If we cannot write this file for any reason, we leave. All combined tests will be
645627
// tested as standalone tests.
646-
return (Duration::default(), Err(TestFailure::CompileError));
628+
return (Duration::default(), Err(RustdocResult::CompileError));
647629
}
648630
if !rustdoc_options.nocapture {
649631
// If `nocapture` is disabled, then we don't display rustc's output when compiling
@@ -720,7 +702,7 @@ fn run_test(
720702
if std::fs::write(&runner_input_file, merged_test_code).is_err() {
721703
// If we cannot write this file for any reason, we leave. All combined tests will be
722704
// tested as standalone tests.
723-
return (instant.elapsed(), Err(TestFailure::CompileError));
705+
return (instant.elapsed(), Err(RustdocResult::CompileError));
724706
}
725707
if !rustdoc_options.nocapture {
726708
// If `nocapture` is disabled, then we don't display rustc's output when compiling
@@ -773,7 +755,7 @@ fn run_test(
773755
let _bomb = Bomb(&out);
774756
match (output.status.success(), langstr.compile_fail) {
775757
(true, true) => {
776-
return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass));
758+
return (instant.elapsed(), Err(RustdocResult::UnexpectedCompilePass));
777759
}
778760
(true, false) => {}
779761
(false, true) => {
@@ -789,12 +771,15 @@ fn run_test(
789771
.collect();
790772

791773
if !missing_codes.is_empty() {
792-
return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes)));
774+
return (
775+
instant.elapsed(),
776+
Err(RustdocResult::MissingErrorCodes(missing_codes)),
777+
);
793778
}
794779
}
795780
}
796781
(false, false) => {
797-
return (instant.elapsed(), Err(TestFailure::CompileError));
782+
return (instant.elapsed(), Err(RustdocResult::CompileError));
798783
}
799784
}
800785

@@ -832,17 +817,9 @@ fn run_test(
832817
cmd.output()
833818
};
834819
match result {
835-
Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
836-
Ok(out) => {
837-
if langstr.should_panic && out.status.success() {
838-
return (duration, Err(TestFailure::UnexpectedRunPass));
839-
} else if !langstr.should_panic && !out.status.success() {
840-
return (duration, Err(TestFailure::ExecutionFailure(out)));
841-
}
842-
}
820+
Err(e) => (duration, Err(RustdocResult::ExecutionError(e))),
821+
Ok(output) => (duration, get_rustdoc_result(output, langstr.should_panic)),
843822
}
844-
845-
(duration, Ok(()))
846823
}
847824

848825
/// Converts a path intended to use as a command to absolute if it is
@@ -1136,54 +1113,7 @@ fn doctest_run_fn(
11361113
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
11371114

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

0 commit comments

Comments
 (0)