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( |
43 | desc: &TestDesc, |
44 | panic_payload: Option<&(dyn Any + Send)>, |
45 | time_opts: Option<&time::TestTimeOptions>, |
46 | exec_time: Option<&time::TestExecTime>, |
47 | ) -> TestResult { |
48 | let result = match (desc.should_panic, panic_payload) { |
49 | // The test did or didn't panic, as expected. |
50 | (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestResult::TrOk, |
51 | |
52 | // Check the actual panic message against the expected message. |
53 | (ShouldPanic::YesWithMessage(msg), Some(err)) => { |
54 | let maybe_panic_str = err |
55 | .downcast_ref::<String>() |
56 | .map(|e| &**e) |
57 | .or_else(|| err.downcast_ref::<&'static str>().copied()); |
58 | |
59 | if maybe_panic_str.map(|e| e.contains(msg)).unwrap_or(false) { |
60 | TestResult::TrOk |
61 | } else if let Some(panic_str) = maybe_panic_str { |
62 | TestResult::TrFailedMsg(format!( |
63 | r#"panic did not contain expected string |
64 | panic message: {panic_str:?} |
65 | expected substring: {msg:?}"# |
66 | )) |
67 | } else { |
68 | TestResult::TrFailedMsg(format!( |
69 | r#"expected panic with string value, |
70 | found non-string value: ` {:?}` |
71 | expected substring: {msg:?}"# , |
72 | (*err).type_id() |
73 | )) |
74 | } |
75 | } |
76 | |
77 | // The test should have panicked, but didn't panic. |
78 | (ShouldPanic::Yes, None) | (ShouldPanic::YesWithMessage(_), None) => { |
79 | let fn_location = if !desc.source_file.is_empty() { |
80 | &format!(" at {}: {}: {}" , desc.source_file, desc.start_line, desc.start_col) |
81 | } else { |
82 | "" |
83 | }; |
84 | TestResult::TrFailedMsg(format!("test did not panic as expected {}" , fn_location)) |
85 | } |
86 | |
87 | // The test should not have panicked, but did panic. |
88 | (ShouldPanic::No, Some(_)) => TestResult::TrFailed, |
89 | }; |
90 | |
91 | // If test is already failed (or allowed to fail), do not change the result. |
92 | if result != TestResult::TrOk { |
93 | return result; |
94 | } |
95 | |
96 | // Check if test is failed due to timeout. |
97 | if let (Some(opts), Some(time)) = (time_opts, exec_time) { |
98 | if opts.error_on_excess && opts.is_critical(desc, time) { |
99 | return TestResult::TrTimedFail; |
100 | } |
101 | } |
102 | |
103 | result |
104 | } |
105 | |
106 | /// Creates a `TestResult` depending on the exit code of test subprocess. |
107 | pub(crate) fn get_result_from_exit_code( |
108 | desc: &TestDesc, |
109 | status: ExitStatus, |
110 | time_opts: Option<&time::TestTimeOptions>, |
111 | exec_time: Option<&time::TestExecTime>, |
112 | ) -> TestResult { |
113 | let result = match status.code() { |
114 | Some(TR_OK) => TestResult::TrOk, |
115 | #[cfg (windows)] |
116 | Some(STATUS_FAIL_FAST_EXCEPTION) => TestResult::TrFailed, |
117 | #[cfg (unix)] |
118 | None => match status.signal() { |
119 | Some(libc::SIGABRT) => TestResult::TrFailed, |
120 | Some(signal) => { |
121 | TestResult::TrFailedMsg(format!("child process exited with signal {signal}" )) |
122 | } |
123 | None => unreachable!("status.code() returned None but status.signal() was None" ), |
124 | }, |
125 | // Upon an abort, Fuchsia returns the status code ZX_TASK_RETCODE_EXCEPTION_KILL. |
126 | #[cfg (target_os = "fuchsia" )] |
127 | Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => TestResult::TrFailed, |
128 | #[cfg (not(unix))] |
129 | None => TestResult::TrFailedMsg(format!("unknown return code" )), |
130 | #[cfg (any(windows, unix))] |
131 | Some(code) => TestResult::TrFailedMsg(format!("got unexpected return code {code}" )), |
132 | #[cfg (not(any(windows, unix)))] |
133 | Some(_) => TestResult::TrFailed, |
134 | }; |
135 | |
136 | // If test is already failed (or allowed to fail), do not change the result. |
137 | if result != TestResult::TrOk { |
138 | return result; |
139 | } |
140 | |
141 | // Check if test is failed due to timeout. |
142 | if let (Some(opts), Some(time)) = (time_opts, exec_time) { |
143 | if opts.error_on_excess && opts.is_critical(desc, time) { |
144 | return TestResult::TrTimedFail; |
145 | } |
146 | } |
147 | |
148 | result |
149 | } |
150 | |