example_test/
lib.rs

1use std::ffi::OsStr;
2use std::io::{Read, Write};
3use std::process::{Child, Stdio};
4
5/// Calls [`ExampleChild::run_new`] with the package name and test name infered from the call site.
6/// Only arguments need to be specified.
7#[macro_export]
8macro_rules! run_current_example {
9    () => {
10        $crate::run_current_example!(::std::iter::empty::<&str>())
11    };
12    ($args:literal) => {
13        $crate::run_current_example!(str::split_whitespace($args))
14    };
15    ($args:expr $(,)?) => {
16        $crate::ExampleChild::run_new(
17            &::std::env::var("CARGO_PKG_NAME").unwrap(),
18            &$crate::extract_example_name(file!()).expect("Failed to determine example name."),
19            $args,
20        )
21    };
22}
23
24/// A wrapper around [`std::process::Child`] that allows us to wait for a specific outputs.
25///
26/// Terminates the inner [`Child`] process when dropped.
27pub struct ExampleChild {
28    child: Child,
29    output_buffer: Vec<u8>,
30    output_len: usize,
31}
32impl ExampleChild {
33    pub fn run_new(
34        pkg_name: &str,
35        test_name: &str,
36        args: impl IntoIterator<Item = impl AsRef<OsStr>>,
37    ) -> Self {
38        let child = std::process::Command::new("cargo")
39            .args(["run", "-p", pkg_name, "--example"])
40            .arg(test_name)
41            .arg("--")
42            .args(args)
43            .stdin(Stdio::piped())
44            .stdout(Stdio::piped())
45            .spawn()
46            .unwrap();
47        Self {
48            child,
49            output_buffer: vec![0; 1024],
50            output_len: 0,
51        }
52    }
53
54    /// Waits for a specific string process output before returning.
55    ///
56    /// When a child process is spawned often you want to wait until the child process is ready before
57    /// moving on. One way to do that synchronization is by waiting for the child process to output
58    /// something and match regex against that output. For example, you could wait until the child
59    /// process outputs "Client live!" which would indicate that it is ready to receive input now on
60    /// stdin.
61    pub fn read_string(&mut self, wait_for_string: &str) {
62        self.read_regex(&regex::escape(wait_for_string));
63    }
64
65    /// Waits for a specific regex process output before returning.
66    pub fn read_regex(&mut self, wait_for_regex: &str) {
67        let stdout = self.child.stdout.as_mut().unwrap();
68        let re = regex::Regex::new(wait_for_regex).unwrap();
69
70        while !re.is_match(&String::from_utf8_lossy(
71            &self.output_buffer[0..self.output_len],
72        )) {
73            eprintln!(
74                "waiting ({}):\n{}",
75                wait_for_regex,
76                String::from_utf8_lossy(&self.output_buffer[0..self.output_len])
77            );
78
79            while self.output_buffer.len() - self.output_len < 1024 {
80                self.output_buffer
81                    .resize(self.output_buffer.len() + 1024, 0);
82            }
83
84            let bytes_read = stdout
85                .read(&mut self.output_buffer[self.output_len..])
86                .unwrap();
87            self.output_len += bytes_read;
88
89            if 0 == bytes_read {
90                panic!("Child process exited before a match was found.");
91            }
92        }
93    }
94
95    /// Writes a line to the child process stdin. A newline is automatically appended and should not be included in `line`.
96    pub fn write_line(&mut self, line: &str) {
97        let stdin = self.child.stdin.as_mut().unwrap();
98        stdin.write_all(line.as_bytes()).unwrap();
99        stdin.write_all(b"\n").unwrap();
100        stdin.flush().unwrap();
101    }
102}
103
104/// Terminates the inner [`Child`] process when dropped.
105///
106/// When a `Child` is dropped normally nothing happens but in unit tests you usually want to
107/// terminate the child and wait for it to terminate. This does that for us.
108impl Drop for ExampleChild {
109    fn drop(&mut self) {
110        #[cfg(target_family = "windows")]
111        let _ = self.child.kill(); // Windows throws `PermissionDenied` if the process has already exited.
112        #[cfg(not(target_family = "windows"))]
113        self.child.kill().unwrap();
114
115        self.child.wait().unwrap();
116    }
117}
118
119/// Extract the example name from the [`std::file!`] path, used by the [`run_current_example!`] macro.
120pub fn extract_example_name(file: &str) -> Option<String> {
121    let pathbuf = std::path::PathBuf::from(file);
122    let mut path = pathbuf.as_path();
123    while path.parent()?.file_name()? != "examples" {
124        path = path.parent().unwrap();
125    }
126    Some(path.file_stem()?.to_string_lossy().into_owned())
127}