fixture/
utils.rs

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