| 1 | use std::any::Any; |
| 2 | #[cfg (unix)] |
| 3 | use std::os::unix::process::ExitStatusExt; |
| 4 | use std::process::ExitStatus; |
| 5 | |
| 6 | pub use self::TestResult::*; |
| 7 | use super::bench::BenchSamples; |
| 8 | use super::options::ShouldPanic; |
| 9 | use super::time; |
| 10 | use super::types::TestDesc; |
| 11 | |
| 12 | // Return code for secondary process. |
| 13 | // Start somewhere other than 0 so we know the return code means what we think |
| 14 | // it means. |
| 15 | pub(crate) const TR_OK: i32 = 50; |
| 16 | |
| 17 | // On Windows we use __fastfail to abort, which is documented to use this |
| 18 | // exception code. |
| 19 | #[cfg (windows)] |
| 20 | const STATUS_FAIL_FAST_EXCEPTION: i32 = 0xC0000409u32 as i32; |
| 21 | |
| 22 | // On Zircon (the Fuchsia kernel), an abort from userspace calls the |
| 23 | // LLVM implementation of __builtin_trap(), e.g., ud2 on x86, which |
| 24 | // raises a kernel exception. If a userspace process does not |
| 25 | // otherwise arrange exception handling, the kernel kills the process |
| 26 | // with this return code. |
| 27 | #[cfg (target_os = "fuchsia" )] |
| 28 | const ZX_TASK_RETCODE_EXCEPTION_KILL: i32 = -1028; |
| 29 | |
| 30 | #[derive (Debug, Clone, PartialEq)] |
| 31 | pub enum TestResult { |
| 32 | TrOk, |
| 33 | TrFailed, |
| 34 | TrFailedMsg(String), |
| 35 | TrIgnored, |
| 36 | TrBench(BenchSamples), |
| 37 | TrTimedFail, |
| 38 | } |
| 39 | |
| 40 | /// Creates a `TestResult` depending on the raw result of test execution |
| 41 | /// and associated data. |
| 42 | pub(crate) fn calc_result<'a>( |
| 43 | desc: &TestDesc, |
| 44 | task_result: Result<(), &'a (dyn Any + 'static + Send)>, |
| 45 | time_opts: Option<&time::TestTimeOptions>, |
| 46 | exec_time: Option<&time::TestExecTime>, |
| 47 | ) -> TestResult { |
| 48 | let result = match (&desc.should_panic, task_result) { |
| 49 | (&ShouldPanic::No, Ok(())) | (&ShouldPanic::Yes, Err(_)) => TestResult::TrOk, |
| 50 | (&ShouldPanic::YesWithMessage(msg), Err(err)) => { |
| 51 | let maybe_panic_str = err |
| 52 | .downcast_ref::<String>() |
| 53 | .map(|e| &**e) |
| 54 | .or_else(|| err.downcast_ref::<&'static str>().copied()); |
| 55 | |
| 56 | if maybe_panic_str.map(|e| e.contains(msg)).unwrap_or(false) { |
| 57 | TestResult::TrOk |
| 58 | } else if let Some(panic_str) = maybe_panic_str { |
| 59 | TestResult::TrFailedMsg(format!( |
| 60 | r#"panic did not contain expected string |
| 61 | panic message: ` {panic_str:?}`, |
| 62 | expected substring: ` {msg:?}`"# |
| 63 | )) |
| 64 | } else { |
| 65 | TestResult::TrFailedMsg(format!( |
| 66 | r#"expected panic with string value, |
| 67 | found non-string value: ` {:?}` |
| 68 | expected substring: ` {:?}`"# , |
| 69 | (*err).type_id(), |
| 70 | msg |
| 71 | )) |
| 72 | } |
| 73 | } |
| 74 | (&ShouldPanic::Yes, Ok(())) | (&ShouldPanic::YesWithMessage(_), Ok(())) => { |
| 75 | TestResult::TrFailedMsg("test did not panic as expected" .to_string()) |
| 76 | } |
| 77 | _ => TestResult::TrFailed, |
| 78 | }; |
| 79 | |
| 80 | // If test is already failed (or allowed to fail), do not change the result. |
| 81 | if result != TestResult::TrOk { |
| 82 | return result; |
| 83 | } |
| 84 | |
| 85 | // Check if test is failed due to timeout. |
| 86 | if let (Some(opts), Some(time)) = (time_opts, exec_time) { |
| 87 | if opts.error_on_excess && opts.is_critical(desc, time) { |
| 88 | return TestResult::TrTimedFail; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | result |
| 93 | } |
| 94 | |
| 95 | /// Creates a `TestResult` depending on the exit code of test subprocess. |
| 96 | pub(crate) fn get_result_from_exit_code( |
| 97 | desc: &TestDesc, |
| 98 | status: ExitStatus, |
| 99 | time_opts: Option<&time::TestTimeOptions>, |
| 100 | exec_time: Option<&time::TestExecTime>, |
| 101 | ) -> TestResult { |
| 102 | let result = match status.code() { |
| 103 | Some(TR_OK) => TestResult::TrOk, |
| 104 | #[cfg (windows)] |
| 105 | Some(STATUS_FAIL_FAST_EXCEPTION) => TestResult::TrFailed, |
| 106 | #[cfg (unix)] |
| 107 | None => match status.signal() { |
| 108 | Some(libc::SIGABRT) => TestResult::TrFailed, |
| 109 | Some(signal) => { |
| 110 | TestResult::TrFailedMsg(format!("child process exited with signal {signal}" )) |
| 111 | } |
| 112 | None => unreachable!("status.code() returned None but status.signal() was None" ), |
| 113 | }, |
| 114 | // Upon an abort, Fuchsia returns the status code ZX_TASK_RETCODE_EXCEPTION_KILL. |
| 115 | #[cfg (target_os = "fuchsia" )] |
| 116 | Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => TestResult::TrFailed, |
| 117 | #[cfg (not(unix))] |
| 118 | None => TestResult::TrFailedMsg(format!("unknown return code" )), |
| 119 | #[cfg (any(windows, unix))] |
| 120 | Some(code) => TestResult::TrFailedMsg(format!("got unexpected return code {code}" )), |
| 121 | #[cfg (not(any(windows, unix)))] |
| 122 | Some(_) => TestResult::TrFailed, |
| 123 | }; |
| 124 | |
| 125 | // If test is already failed (or allowed to fail), do not change the result. |
| 126 | if result != TestResult::TrOk { |
| 127 | return result; |
| 128 | } |
| 129 | |
| 130 | // Check if test is failed due to timeout. |
| 131 | if let (Some(opts), Some(time)) = (time_opts, exec_time) { |
| 132 | if opts.error_on_excess && opts.is_critical(desc, time) { |
| 133 | return TestResult::TrTimedFail; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | result |
| 138 | } |
| 139 | |