use std::path::PathBuf;
use std::process::{self, Command, Stdio};
use birdcage::{Birdcage, Exception, Sandbox};
test_mods! {
mod canonicalize;
#[cfg(target_os = "linux")]
mod consistent_id_mappings;
mod delete_before_lockdown;
mod env;
mod exec;
mod exec_symlinked_dir;
mod exec_symlinked_dirs_exec;
mod exec_symlinked_file;
mod fs;
mod fs_broken_symlink;
mod fs_null;
mod fs_readonly;
mod fs_restrict_child;
mod fs_symlink;
mod fs_symlink_dir;
mod fs_symlink_dir_separate_perms;
mod fs_write_also_read;
mod full_env;
mod full_sandbox;
mod missing_exception;
mod net;
#[cfg(target_os = "linux")]
mod seccomp;
}
const TEST_DIR: &str = "integration";
pub struct TestSetup {
pub sandbox: Birdcage,
pub data: String,
}
fn main() {
let mut args = std::env::args().skip(1);
let test_name = match args.next() {
Some(test_name) => test_name,
None => {
spawn_tests();
return;
},
};
let test = match TESTS.iter().find(|(cmd, ..)| cmd == &test_name) {
Some(test) => test,
None => unreachable!("invalid test module name: {test_name:?}"),
};
let arg = args.next().unwrap();
match arg.as_str() {
"--setup" => {
let tempdir = args.next().unwrap();
run_setup(&test_name, tempdir, &test.1);
},
_ => test.2(arg),
}
}
fn spawn_tests() {
eprintln!("\nrunning {} tests", TESTS.len());
let current_exe = std::env::current_exe().unwrap();
let mut children = Vec::new();
for (cmd, ..) in TESTS {
let tempdir = tempfile::tempdir().unwrap();
let child = Command::new(¤t_exe)
.arg(cmd)
.arg("--setup")
.arg(tempdir.path())
.stderr(Stdio::piped())
.spawn()
.unwrap();
children.push((cmd, child, tempdir));
}
let mut passed = 0;
for (name, child, tempdir) in children {
let output = match child.wait_with_output() {
Ok(output) => output,
Err(err) => {
eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[31mHARNESS FAILURE\x1b[0m: {err}");
continue;
},
};
if !output.status.success() {
eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[31mFAILED\x1b[0m");
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("\n---- {TEST_DIR}/{name}.rs stderr ----\n{}\n", stderr.trim());
}
} else {
eprintln!("test {TEST_DIR}/{name}.rs ... \x1b[32mok\x1b[0m");
passed += 1;
}
tempdir.close().unwrap();
}
let failed = TESTS.len() - passed;
if failed > 0 {
eprintln!("\ntest result: \x1b[31mFAILED\x1b[0m. {} passed; {} failed", passed, failed);
} else {
eprintln!("\ntest result: \x1b[32mok\x1b[0m. {} passed; {} failed", passed, failed);
}
eprintln!();
}
fn run_setup(test_name: &str, tempdir: String, setup: &fn(PathBuf) -> TestSetup) {
let mut test_setup = setup(PathBuf::from(tempdir));
let current_exe = std::env::current_exe().unwrap();
for path in [current_exe.clone(), "/usr/lib".into(), "/lib64".into(), "/lib".into()] {
if path.exists() {
test_setup.sandbox.add_exception(Exception::ExecuteAndRead(path)).unwrap();
}
}
let mut command = birdcage::process::Command::new(current_exe);
command.args([test_name, test_setup.data.as_str()]);
let child = test_setup.sandbox.spawn(command).unwrap();
let output = child.wait_with_output().unwrap();
if !output.status.success() {
process::exit(output.status.code().unwrap_or(1));
}
}
#[macro_export]
macro_rules! test_mods {
($($(#[$cfg:meta])? mod $mod:ident);*;) => {
$(
$( #[$cfg] )?
mod $mod;
)*
const TESTS: &[(&str, fn(std::path::PathBuf) -> $crate::TestSetup, fn(String))] = &[$(
$( #[$cfg] )?
(stringify!($mod), $mod :: setup, $mod :: validate),
)*];
};
}