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 let mut cmd = tokio::process::Command::new(&self.suite.program);
77 cmd.arg("test")
78 .args(&self.suite.cargo_args)
79 .args(["--test", &self.suite.test_target, "--", "--exact", &self.test_path])
80 .current_dir(&self.suite.crate_dir)
81 .stdout(Stdio::inherit())
82 .stderr(Stdio::inherit())
83 .kill_on_drop(true);
84 self.state =
85 State::Running(tokio::spawn(async move { cmd.status().await.map_err(|e| e.to_string()) }));
86 }
87
88 pub async fn verdict(&mut self) -> Result<(), Failed> {
93 let result = match std::mem::replace(&mut self.state, State::Idle) {
94 State::Idle => {
95 return Err(Failed::from(format!(
96 "device test '{}' was never started",
97 self.test_path
98 )))
99 }
100 State::Running(handle) => match handle.await {
101 Err(join) => Err(format!("device test task failed: {join}")),
102 Ok(Err(spawn)) => Err(format!(
103 "spawning {} in {}: {spawn}",
104 self.suite.program,
105 self.suite.crate_dir.display()
106 )),
107 Ok(Ok(status)) if status.success() => Ok(()),
108 Ok(Ok(status)) => {
109 Err(format!("device-side test '{}' reported failure ({status})", self.test_path))
110 }
111 },
112 State::Done(result) => result,
113 };
114 self.state = State::Done(result.clone());
115 result.map_err(Failed::from)
116 }
117}
118
119#[macro_export]
140macro_rules! paired_suite {
141 (@device_test $name:ident) => { concat!("tests::", stringify!($name)) };
142 (@device_test $name:ident $dt:expr) => { $dt };
143 (
144 device_suite: $suite:expr,
145 $(
146 scenario $name:ident $(, device_test: $dt:expr)? , |$cx:ident, $dev:ident| $body:block
147 )*
148 ) => {
149 fn main() -> ::std::process::ExitCode {
150 let __suite = $suite;
151 $crate::run(::std::vec![
152 $(
153 {
154 let __suite = __suite.clone();
155 $crate::BancTest::new(
156 stringify!($name),
157 move |$cx: $crate::TestCx| {
158 ::std::boxed::Box::pin(async move {
159 let mut $dev =
160 __suite.test($crate::paired_suite!(@device_test $name $($dt)?));
161 let __body: ::std::result::Result<(), $crate::Failed> =
162 async { $body Ok(()) }.await;
163 __body?;
164 $dev.verdict().await
165 })
166 },
167 )
168 }
169 ),*
170 ])
171 }
172 };
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn suite(program: &str) -> DeviceSuite {
180 let mut s = DeviceSuite::new(std::env::temp_dir(), "join");
181 s.program = program.into();
182 s
183 }
184
185 #[tokio::test]
186 async fn verdict_tracks_child_exit() {
187 let mut ok = suite("true").test("tests::x");
188 ok.start();
189 assert!(ok.verdict().await.is_ok());
190 assert!(ok.verdict().await.is_ok());
192
193 let mut bad = suite("false").test("tests::x");
194 bad.start();
195 assert!(bad.verdict().await.is_err());
196 assert!(bad.verdict().await.is_err());
197 }
198
199 #[tokio::test]
200 async fn never_started_is_a_failure() {
201 let mut dt = suite("true").test("tests::x");
202 let err = dt.verdict().await.unwrap_err();
203 assert!(err.message().unwrap().contains("never started"));
204 }
205
206 #[tokio::test]
207 async fn spawn_error_is_reported_not_panicked() {
208 let mut dt = suite("/nonexistent/no-such-program").test("tests::x");
209 dt.start();
210 assert!(dt.verdict().await.is_err());
211 }
212}