xref: /aosp_15_r20/external/crosvm/e2e_tests/fixture/src/utils.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 //! Provides utility functions used by multiple fixture files.
6 
7 use std::env;
8 use std::io::ErrorKind;
9 #[cfg(any(target_os = "android", target_os = "linux"))]
10 use std::os::unix::process::ExitStatusExt;
11 use std::path::Path;
12 use std::path::PathBuf;
13 use std::process::Command;
14 use std::process::ExitStatus;
15 use std::process::Output;
16 use std::sync::mpsc::sync_channel;
17 use std::sync::mpsc::RecvTimeoutError;
18 use std::thread;
19 use std::time::Duration;
20 use std::time::SystemTime;
21 
22 use anyhow::bail;
23 use anyhow::Result;
24 use tempfile::NamedTempFile;
25 
26 use crate::sys::binary_name;
27 use crate::vhost_user::CmdType;
28 use crate::vhost_user::Config as VuConfig;
29 
30 pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
31 
32 /// Returns the path to the crosvm binary to be tested.
33 ///
34 /// The crosvm binary is expected to be alongside to the integration tests
35 /// binary. Alternatively in the parent directory (cargo will put the
36 /// test binary in target/debug/deps/ but the crosvm binary in target/debug)
find_crosvm_binary() -> PathBuf37 pub fn find_crosvm_binary() -> PathBuf {
38     let binary_name = binary_name();
39     let exe_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
40     let first = exe_dir.join(binary_name);
41     if first.exists() {
42         return first;
43     }
44     let second = exe_dir.parent().unwrap().join(binary_name);
45     if second.exists() {
46         return second;
47     }
48     panic!(
49         "Cannot find {} in ./ or ../ alongside test binary.",
50         binary_name
51     );
52 }
53 
54 /// Run the provided closure in a separate thread and return it's result. If the closure does not
55 /// finish before the timeout is reached, an Error is returned instead.
56 ///
57 /// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
58 /// when an error is returned.
run_with_timeout<F, U>(closure: F, timeout: Duration) -> Result<U> where F: FnOnce() -> U + Send + 'static, U: Send + 'static,59 pub fn run_with_timeout<F, U>(closure: F, timeout: Duration) -> Result<U>
60 where
61     F: FnOnce() -> U + Send + 'static,
62     U: Send + 'static,
63 {
64     run_with_status_check(closure, timeout, || false)
65 }
66 
67 /// Run the provided closure in a separate thread and return it's result. If the closure does not
68 /// finish, continue_fn is called periodically with interval while continue_fn return true. Once
69 /// continue_fn return false, an Error is returned instead.
70 ///
71 /// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
72 /// when an error is returned.
run_with_status_check<F, U, C>( closure: F, interval: Duration, mut continue_fn: C, ) -> Result<U> where F: FnOnce() -> U + Send + 'static, U: Send + 'static, C: FnMut() -> bool,73 pub fn run_with_status_check<F, U, C>(
74     closure: F,
75     interval: Duration,
76     mut continue_fn: C,
77 ) -> Result<U>
78 where
79     F: FnOnce() -> U + Send + 'static,
80     U: Send + 'static,
81     C: FnMut() -> bool,
82 {
83     let (tx, rx) = sync_channel::<()>(1);
84     let handle = thread::spawn(move || {
85         let result = closure();
86         // Notify main thread the closure is done. Fail silently if it's not listening anymore.
87         let _ = tx.send(());
88         result
89     });
90     loop {
91         match rx.recv_timeout(interval) {
92             Ok(_) => {
93                 return Ok(handle.join().unwrap());
94             }
95             Err(RecvTimeoutError::Timeout) => {
96                 if !continue_fn() {
97                     bail!("closure timed out");
98                 }
99             }
100             Err(RecvTimeoutError::Disconnected) => bail!("closure panicked"),
101         }
102     }
103 }
104 
105 #[derive(Debug)]
106 pub enum CommandError {
107     IoError(std::io::Error),
108     ErrorCode(i32),
109     Signal(i32),
110 }
111 
112 /// Extension trait for utilities on std::process::Command
113 pub trait CommandExt {
114     /// Same as Command::output() but will treat non-success status of the Command as an
115     /// error.
output_checked(&mut self) -> std::result::Result<Output, CommandError>116     fn output_checked(&mut self) -> std::result::Result<Output, CommandError>;
117 
118     /// Print the command to be executed
log(&mut self) -> &mut Self119     fn log(&mut self) -> &mut Self;
120 }
121 
122 impl CommandExt for Command {
output_checked(&mut self) -> std::result::Result<Output, CommandError>123     fn output_checked(&mut self) -> std::result::Result<Output, CommandError> {
124         let output = self.output().map_err(CommandError::IoError)?;
125         if !output.status.success() {
126             if let Some(code) = output.status.code() {
127                 return Err(CommandError::ErrorCode(code));
128             } else {
129                 #[cfg(any(target_os = "android", target_os = "linux"))]
130                 if let Some(signal) = output.status.signal() {
131                     return Err(CommandError::Signal(signal));
132                 }
133                 panic!("No error code and no signal should never happen.");
134             }
135         }
136         Ok(output)
137     }
138 
log(&mut self) -> &mut Self139     fn log(&mut self) -> &mut Self {
140         println!("$ {:?}", self);
141         self
142     }
143 }
144 
145 /// Extension trait for utilities on std::process::Child
146 pub trait ChildExt {
147     /// Same as Child.wait(), but will return with an error after the specified timeout.
wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>148     fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>;
149 }
150 
151 impl ChildExt for std::process::Child {
wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>152     fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>> {
153         let start_time = SystemTime::now();
154         while SystemTime::now().duration_since(start_time).unwrap() < timeout {
155             if let Ok(status) = self.try_wait() {
156                 return Ok(status);
157             }
158             thread::sleep(Duration::from_millis(10));
159         }
160         Err(std::io::Error::new(
161             ErrorKind::TimedOut,
162             "Timeout while waiting for child",
163         ))
164     }
165 }
166 
167 /// Calls the `closure` until it returns a non-error Result.
168 /// If it has been re-tried `retries` times, the last result is returned.
retry<F, T, E>(mut closure: F, retries: usize) -> Result<T, E> where F: FnMut() -> Result<T, E>, E: std::fmt::Debug,169 pub fn retry<F, T, E>(mut closure: F, retries: usize) -> Result<T, E>
170 where
171     F: FnMut() -> Result<T, E>,
172     E: std::fmt::Debug,
173 {
174     let mut attempts_left = retries + 1;
175     loop {
176         let result = closure();
177         attempts_left -= 1;
178         if result.is_ok() || attempts_left == 0 {
179             break result;
180         } else {
181             println!("Attempt failed: {:?}", result.err());
182         }
183     }
184 }
185 
186 /// Prepare a temporary ext4 disk file.
prepare_disk_img() -> NamedTempFile187 pub fn prepare_disk_img() -> NamedTempFile {
188     let mut disk = NamedTempFile::new().unwrap();
189     disk.as_file_mut().set_len(DEFAULT_BLOCK_SIZE).unwrap();
190 
191     // Add /sbin and /usr/sbin to PATH since some distributions put mkfs.ext4 in one of those
192     // directories but don't add them to non-root PATH.
193     let path = env::var("PATH").unwrap();
194     let path = [&path, "/sbin", "/usr/sbin"].join(":");
195 
196     // TODO(b/243127910): Use `mkfs.ext4 -d` to include test data.
197     Command::new("mkfs.ext4")
198         .arg(disk.path().to_str().unwrap())
199         .env("PATH", path)
200         .output()
201         .expect("failed to execute process");
202     disk
203 }
204 
create_vu_block_config(cmd_type: CmdType, socket: &Path, disk: &Path) -> VuConfig205 pub fn create_vu_block_config(cmd_type: CmdType, socket: &Path, disk: &Path) -> VuConfig {
206     let socket_path = socket.to_str().unwrap();
207     let disk_path = disk.to_str().unwrap();
208     println!("disk={disk_path}, socket={socket_path}");
209     match cmd_type {
210         CmdType::Device => VuConfig::new(cmd_type, "block").extra_args(vec![
211             "block".to_string(),
212             "--socket-path".to_string(),
213             socket_path.to_string(),
214             "--file".to_string(),
215             disk_path.to_string(),
216         ]),
217         CmdType::Devices => VuConfig::new(cmd_type, "block").extra_args(vec![
218             "--block".to_string(),
219             format!("vhost={},path={}", socket_path, disk_path),
220         ]),
221     }
222 }
223 
create_vu_console_multiport_config( socket: &Path, file_path: Vec<(PathBuf, PathBuf)>, ) -> VuConfig224 pub fn create_vu_console_multiport_config(
225     socket: &Path,
226     file_path: Vec<(PathBuf, PathBuf)>,
227 ) -> VuConfig {
228     let socket_path = socket.to_str().unwrap();
229 
230     let mut args = vec![
231         "console".to_string(),
232         "--socket-path".to_string(),
233         socket_path.to_string(),
234     ];
235 
236     for (i, (output_file, input_file)) in file_path.iter().enumerate() {
237         args.push("--port".to_string());
238         match input_file.file_name().is_some() {
239             true => {
240                 args.push(format!(
241                     "type=file,hardware=virtio-console,name=port{},path={},input={}",
242                     i,
243                     output_file.to_str().unwrap(),
244                     input_file.to_str().unwrap(),
245                 ));
246             }
247             false => {
248                 args.push(format!(
249                     "type=file,hardware=virtio-console,name=port{},path={}",
250                     i,
251                     output_file.to_str().unwrap(),
252                 ));
253             }
254         };
255     }
256     VuConfig::new(CmdType::Device, "console").extra_args(args)
257 }
258