use std::ffi::OsStr;
use std::io::{Read, Write};
use std::process::{Child, Stdio};
#[doc(hidden)]
pub use ctor;
pub const RUN_MAIN_ENV_VAR: &str = "RUNNING_AS_EXAMPLE_TEST";
#[macro_export]
#[expect(
clippy::crate_in_macro_def,
reason = "intentional: `crate::main` must refer to the *calling* example's `main`, not `example_test`"
)]
macro_rules! run_current_example {
() => {
$crate::run_current_example!(::std::iter::empty::<&str>())
};
($args:literal) => {
$crate::run_current_example!(str::split_whitespace($args))
};
($args:expr $(,)?) => {{
$crate::ctor::declarative::ctor! {
#[ctor(unsafe, priority = late)]
fn __example_test_run_main() {
$crate::run_example_main_if_child(|| crate::main());
}
}
$crate::ExampleChild::run_new($args)
}};
}
pub fn run_example_main_if_child<T: ExampleMainReturn>(main_fn: impl FnOnce() -> T) {
if std::env::var_os(RUN_MAIN_ENV_VAR).is_some_and(|value| value == "1") {
let exit_code = main_fn().into_exit_code();
std::process::exit(exit_code);
}
}
pub trait ExampleMainReturn {
fn into_exit_code(self) -> i32;
}
impl ExampleMainReturn for () {
fn into_exit_code(self) -> i32 {
0
}
}
impl<T: ExampleMainReturn, E: std::fmt::Debug> ExampleMainReturn for Result<T, E> {
fn into_exit_code(self) -> i32 {
match self {
Ok(inner) => inner.into_exit_code(),
Err(err) => {
eprintln!("Error: {:?}", err);
1
}
}
}
}
pub struct ExampleChild {
child: Child,
output_buffer: Vec<u8>,
output_len: usize,
}
impl ExampleChild {
pub fn run_new(args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Self {
let current_exe = std::env::current_exe().expect("Failed to get current executable path.");
let mut cmd = std::process::Command::new(current_exe);
cmd.args(args).env(RUN_MAIN_ENV_VAR, "1");
log::info!("Re-executing test binary as example: {:?}", cmd);
let child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
Self {
child,
output_buffer: vec![0; 1024],
output_len: 0,
}
}
pub fn read_string(&mut self, wait_for_string: &str) {
self.read_regex(®ex::escape(wait_for_string));
}
pub fn read_regex(&mut self, wait_for_regex: &str) {
let stdout = self.child.stdout.as_mut().unwrap();
let re = regex::Regex::new(wait_for_regex).unwrap();
while !re.is_match(&String::from_utf8_lossy(
&self.output_buffer[0..self.output_len],
)) {
eprintln!(
"waiting ({}):\n{}",
wait_for_regex,
String::from_utf8_lossy(&self.output_buffer[0..self.output_len])
);
while self.output_buffer.len() - self.output_len < 1024 {
self.output_buffer
.resize(self.output_buffer.len() + 1024, 0);
}
let bytes_read = stdout
.read(&mut self.output_buffer[self.output_len..])
.unwrap();
self.output_len += bytes_read;
if 0 == bytes_read {
panic!("Child process exited before a match was found.");
}
}
}
pub fn read_to_end(&mut self) -> String {
let stdout = self.child.stdout.as_mut().unwrap();
self.output_buffer.truncate(self.output_len);
stdout.read_to_end(&mut self.output_buffer).unwrap();
self.output_len = self.output_buffer.len();
let status = self.child.wait().unwrap();
assert!(
status.success(),
"Child process exited unsuccessfully: {}",
status
);
String::from_utf8_lossy(&self.output_buffer[..self.output_len]).into_owned()
}
pub fn write_line(&mut self, line: &str) {
let stdin = self.child.stdin.as_mut().unwrap();
stdin.write_all(line.as_bytes()).unwrap();
stdin.write_all(b"\n").unwrap();
stdin.flush().unwrap();
}
}
impl Drop for ExampleChild {
fn drop(&mut self) {
let _ = self.child.kill();
self.child.wait().unwrap();
}
}