Skip to main content

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 mut cargo_cmd = std::process::Command::new("cargo");
39        cargo_cmd
40            .args(["run", "--frozen", "--no-default-features"])
41            .args(["-p", pkg_name, "--example", test_name]);
42        if let Some(features) = trybuild_internals_api::features::find()
43            && !features.is_empty()
44        {
45            cargo_cmd.args(["--features", &features.join(",")]);
46        }
47        cargo_cmd
48            .arg("--")
49            .args(args)
50            .env("RUNNING_AS_EXAMPLE_TEST", "1");
51
52        log::info!("Running cargo command: {:?}", cargo_cmd);
53
54        let child = cargo_cmd
55            .stdin(Stdio::piped())
56            .stdout(Stdio::piped())
57            .spawn()
58            .unwrap();
59        Self {
60            child,
61            output_buffer: vec![0; 1024],
62            output_len: 0,
63        }
64    }
65
66    /// Waits for a specific string process output before returning.
67    ///
68    /// When a child process is spawned often you want to wait until the child process is ready before
69    /// moving on. One way to do that synchronization is by waiting for the child process to output
70    /// something and match regex against that output. For example, you could wait until the child
71    /// process outputs "Client live!" which would indicate that it is ready to receive input now on
72    /// stdin.
73    pub fn read_string(&mut self, wait_for_string: &str) {
74        self.read_regex(&regex::escape(wait_for_string));
75    }
76
77    /// Waits for a specific regex process output before returning.
78    pub fn read_regex(&mut self, wait_for_regex: &str) {
79        let stdout = self.child.stdout.as_mut().unwrap();
80        let re = regex::Regex::new(wait_for_regex).unwrap();
81
82        while !re.is_match(&String::from_utf8_lossy(
83            &self.output_buffer[0..self.output_len],
84        )) {
85            eprintln!(
86                "waiting ({}):\n{}",
87                wait_for_regex,
88                String::from_utf8_lossy(&self.output_buffer[0..self.output_len])
89            );
90
91            while self.output_buffer.len() - self.output_len < 1024 {
92                self.output_buffer
93                    .resize(self.output_buffer.len() + 1024, 0);
94            }
95
96            let bytes_read = stdout
97                .read(&mut self.output_buffer[self.output_len..])
98                .unwrap();
99            self.output_len += bytes_read;
100
101            if 0 == bytes_read {
102                panic!("Child process exited before a match was found.");
103            }
104        }
105    }
106
107    /// Writes a line to the child process stdin. A newline is automatically appended and should not be included in `line`.
108    pub fn write_line(&mut self, line: &str) {
109        let stdin = self.child.stdin.as_mut().unwrap();
110        stdin.write_all(line.as_bytes()).unwrap();
111        stdin.write_all(b"\n").unwrap();
112        stdin.flush().unwrap();
113    }
114}
115
116/// Terminates the inner [`Child`] process when dropped.
117///
118/// When a `Child` is dropped normally nothing happens but in unit tests you usually want to
119/// terminate the child and wait for it to terminate. This does that for us.
120impl Drop for ExampleChild {
121    fn drop(&mut self) {
122        #[cfg(target_family = "windows")]
123        let _ = self.child.kill(); // Windows throws `PermissionDenied` if the process has already exited.
124        #[cfg(not(target_family = "windows"))]
125        self.child.kill().unwrap();
126
127        self.child.wait().unwrap();
128    }
129}
130
131/// Extract the example name from the [`std::file!`] path, used by the [`run_current_example!`] macro.
132pub fn extract_example_name(file: &str) -> Option<String> {
133    let pathbuf = std::path::PathBuf::from(file);
134    let mut path = pathbuf.as_path();
135    while path.parent()?.file_name()? != "examples" {
136        path = path.parent().unwrap();
137    }
138    Some(path.file_stem()?.to_string_lossy().into_owned())
139}