1use libtest_mimic::Failed;
15use std::path::PathBuf;
16use std::process::Stdio;
17
18#[derive(Clone)]
21pub struct DeviceSuite {
22 pub crate_dir: PathBuf,
24 pub test_target: String,
26 pub cargo_args: Vec<String>,
28 pub program: String,
30}
31
32impl DeviceSuite {
33 pub fn new(crate_dir: impl Into<PathBuf>, test_target: impl Into<String>) -> Self {
34 DeviceSuite {
35 crate_dir: crate_dir.into(),
36 test_target: test_target.into(),
37 cargo_args: Vec::new(),
38 program: "cargo".into(),
39 }
40 }
41
42 pub fn test(&self, test_path: impl Into<String>) -> DeviceTest {
45 DeviceTest { suite: self.clone(), test_path: test_path.into(), state: State::Idle }
46 }
47}
48
49enum State {
50 Idle,
51 Running(tokio::task::JoinHandle<Result<std::process::ExitStatus, String>>),
52 Done(Result<(), String>),
53}
54
55pub struct DeviceTest {
56 suite: DeviceSuite,
57 test_path: String,
58 state: State,
59}
60
61impl DeviceTest {
62 pub fn test_path(&self) -> &str {
64 &self.test_path
65 }
66
67 pub fn start(&mut self) {
71 assert!(
72 matches!(self.state, State::Idle),
73 "device test '{}' started twice",
74 self.test_path
75 );
76 if !self.suite.crate_dir.is_dir() {
81 self.state = State::Done(Err(format!(
82 "device crate dir {} does not exist",
83 self.suite.crate_dir.display()
84 )));
85 return;
86 }
87 let mut cmd = tokio::process::Command::new(&self.suite.program);
88 cmd.arg("test")
89 .args(&self.suite.cargo_args)
90 .args(["--test", &self.suite.test_target, "--", "--exact", &self.test_path])
91 .current_dir(&self.suite.crate_dir)
92 .stdout(Stdio::inherit())
93 .stderr(Stdio::inherit())
94 .kill_on_drop(true);
95 self.state =
96 State::Running(tokio::spawn(async move { cmd.status().await.map_err(|e| e.to_string()) }));
97 }
98
99 pub async fn failure_context(&mut self) -> Option<String> {
104 let finished = match &self.state {
105 State::Done(r) => r.is_err(),
106 State::Running(handle) => handle.is_finished(),
107 State::Idle => false,
108 };
109 if !finished {
110 return None;
111 }
112 self.verdict()
113 .await
114 .err()
115 .map(|f| f.message().unwrap_or("device test failed").to_string())
116 }
117
118 pub async fn verdict(&mut self) -> Result<(), Failed> {
123 let result = match std::mem::replace(&mut self.state, State::Idle) {
124 State::Idle => {
125 return Err(Failed::from(format!(
126 "device test '{}' was never started",
127 self.test_path
128 )))
129 }
130 State::Running(handle) => match handle.await {
131 Err(join) => Err(format!("device test task failed: {join}")),
132 Ok(Err(spawn)) => Err(format!(
133 "spawning {} in {}: {spawn}",
134 self.suite.program,
135 self.suite.crate_dir.display()
136 )),
137 Ok(Ok(status)) if status.success() => Ok(()),
138 Ok(Ok(status)) => {
139 Err(format!("device-side test '{}' reported failure ({status})", self.test_path))
140 }
141 },
142 State::Done(result) => result,
143 };
144 self.state = State::Done(result.clone());
145 result.map_err(Failed::from)
146 }
147}
148
149#[macro_export]
170macro_rules! paired_suite {
171 (@device_test $name:ident) => { concat!("tests::", stringify!($name)) };
172 (@device_test $name:ident $dt:expr) => { $dt };
173 (
174 device_suite: $suite:expr,
175 $(
176 scenario $name:ident $(, device_test: $dt:expr)? , |$cx:ident, $dev:ident| $body:block
177 )*
178 ) => {
179 fn main() -> ::std::process::ExitCode {
180 let __suite = $suite;
181 $crate::run(::std::vec![
182 $(
183 {
184 let __suite = __suite.clone();
185 $crate::BancTest::new(
186 stringify!($name),
187 move |$cx: $crate::TestCx| {
188 ::std::boxed::Box::pin(async move {
189 let mut $dev =
190 __suite.test($crate::paired_suite!(@device_test $name $($dt)?));
191 let __body: ::std::result::Result<(), $crate::Failed> =
192 async { $body Ok(()) }.await;
193 if let ::std::result::Result::Err(e) = __body {
194 let msg = e.message().unwrap_or("test failed").to_string();
195 return ::std::result::Result::Err(
196 match $dev.failure_context().await {
197 ::std::option::Option::Some(dev_err) => $crate::Failed::from(
198 ::std::format!("{msg}\ndevice side: {dev_err}"),
199 ),
200 ::std::option::Option::None => $crate::Failed::from(msg),
201 },
202 );
203 }
204 $dev.verdict().await
205 })
206 },
207 )
208 }
209 ),*
210 ])
211 }
212 };
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn suite(program: &str) -> DeviceSuite {
220 let mut s = DeviceSuite::new(std::env::temp_dir(), "join");
221 s.program = program.into();
222 s
223 }
224
225 #[tokio::test]
226 async fn verdict_tracks_child_exit() {
227 let mut ok = suite("true").test("tests::x");
228 ok.start();
229 assert!(ok.verdict().await.is_ok());
230 assert!(ok.verdict().await.is_ok());
232
233 let mut bad = suite("false").test("tests::x");
234 bad.start();
235 assert!(bad.verdict().await.is_err());
236 assert!(bad.verdict().await.is_err());
237 }
238
239 #[tokio::test]
240 async fn missing_crate_dir_fails_at_start_with_context() {
241 let mut suite = DeviceSuite::new("/nonexistent/fw-crate", "join");
242 suite.program = "true".into();
243 let mut dt = suite.test("tests::x");
244 dt.start();
245 let ctx = dt.failure_context().await.unwrap();
247 assert!(ctx.contains("does not exist"), "unexpected context: {ctx}");
248 assert!(dt.verdict().await.is_err());
249 }
250
251 #[cfg(unix)]
252 #[tokio::test]
253 async fn no_failure_context_while_device_still_running() {
254 use std::os::unix::fs::PermissionsExt;
255 let script = std::env::temp_dir().join(format!("banc-slow-{}", std::process::id()));
256 std::fs::write(&script, "#!/bin/sh\nsleep 5\n").unwrap();
257 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
258
259 let mut suite = DeviceSuite::new(std::env::temp_dir(), "join");
260 suite.program = script.to_str().unwrap().into();
261 let mut dt = suite.test("tests::x");
262 dt.start();
263 assert!(dt.failure_context().await.is_none());
264 assert!(matches!(dt.state, State::Running(_)));
266 std::fs::remove_file(script).ok();
267 }
268
269 #[tokio::test]
270 async fn never_started_is_a_failure() {
271 let mut dt = suite("true").test("tests::x");
272 let err = dt.verdict().await.unwrap_err();
273 assert!(err.message().unwrap().contains("never started"));
274 }
275
276 #[tokio::test]
277 async fn spawn_error_is_reported_not_panicked() {
278 let mut dt = suite("/nonexistent/no-such-program").test("tests::x");
279 dt.start();
280 assert!(dt.verdict().await.is_err());
281 }
282}