use std::{
env,
fs::remove_dir_all,
ops::Range,
process::{Command, ExitCode},
};
use nix::{
errno::Errno,
fcntl::AT_FDCWD,
sys::stat::{fchmodat, umask, FchmodatFlags, Mode},
unistd::{chdir, getcwd, mkdir, mkdtemp},
};
use syd::{
err::SydResult,
path::{XPath, XPathBuf},
syslog::LogLevel,
wildmatch::inamematch,
};
use crate::util::shuffle_vec;
mod test;
mod util;
use test::*;
#[cfg(all(
not(coverage),
not(feature = "prof"),
not(target_os = "android"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
#[derive(Debug)]
struct TempDir {
path: XPathBuf,
keep: bool,
}
impl TempDir {
fn new(path: &XPath) -> Self {
TempDir {
path: path.to_owned(),
keep: std::env::var_os("SYD_TEST_KEEP").is_some(),
}
}
}
impl Drop for TempDir {
fn drop(&mut self) {
if self.keep {
return;
}
match Command::new("rm").arg("-rf").arg(&self.path).status() {
Ok(status) => {
if !status.success() {
let path = self.path.display();
eprintln!(
"Failed to remove temporary directory \"{path}\": rm returned non-zero."
);
}
}
Err(error) => {
let path = self.path.display();
eprintln!("Failed to remove temporary directory \"{path}\": {error}.");
}
};
}
}
#[derive(Debug)]
enum Arguments {
Index(usize),
Range(Range<usize>),
Pattern(String),
}
struct ArgVec(Vec<Arguments>);
impl From<String> for Arguments {
fn from(arg: String) -> Self {
if let Ok(idx) = arg.parse::<usize>() {
Arguments::Index(idx)
} else if let Some(range) = arg.split_once("..") {
if let (Ok(start), Ok(end)) = (range.0.parse::<usize>(), range.1.parse::<usize>()) {
Arguments::Range(start..end)
} else {
Arguments::Pattern(arg)
}
} else {
Arguments::Pattern(arg)
}
}
}
impl From<String> for ArgVec {
fn from(arg: String) -> Self {
if let Ok(idx) = arg.parse::<usize>() {
ArgVec(vec![Arguments::Index(idx)])
} else if let Some(range) = arg.split_once("..") {
if let (Ok(start), Ok(end)) = (range.0.parse::<usize>(), range.1.parse::<usize>()) {
ArgVec(vec![Arguments::Range(start..end)])
} else {
ArgVec(vec![Arguments::Pattern(arg)])
}
} else {
ArgVec(vec![Arguments::Pattern(arg)])
}
}
}
fn main() -> SydResult<ExitCode> {
syd::set_sigpipe_dfl()?;
syd::log::log_init_simple(LogLevel::Warn)?;
umask(Mode::from_bits_truncate(0o077));
println!("# syd-test: Welcome to the Machine!");
println!("# usage: syd-test [-hlq] [<name-glob>|<number>|<number>..<number>]..");
let args = std::env::args().skip(1).collect::<Vec<_>>();
let mut args_is_empty = args.is_empty();
let mut skip_args = 0;
if !args_is_empty && matches!(args[0].as_str(), "-h" | "--help" | "-l" | "--list") {
for (idx, (name, _)) in TESTS.iter().enumerate() {
#[expect(clippy::disallowed_methods)]
let name = name.strip_prefix("test_syd_").unwrap();
let idx = idx + 1;
println!("{idx:>3}: {name}");
}
return Ok(ExitCode::SUCCESS);
}
let fail_quick = !args_is_empty && matches!(args[0].as_str(), "-q" | "--quick");
if fail_quick {
args_is_empty = args.len() == 1;
skip_args = 1;
}
let fail_quick = fail_quick || std::env::var_os("SYD_TEST_QUICK").is_some();
let tsl = Command::new("tput")
.arg("tsl")
.status()
.map(|s| s.success())
.unwrap_or(false);
let tmpdir = {
#[expect(clippy::disallowed_methods)]
let tmp = format!(
"{}/syd_test_XXXXXX",
env::var("SYD_TEST_TMPDIR").unwrap_or(".".to_string())
);
match mkdtemp(XPath::new(&tmp)) {
Ok(path) => {
let path = path.canonicalize().map(XPathBuf::from)?;
match chdir(&path) {
Ok(_) => {
println!("# Running tests under '{}'.", path.display());
println!("# Use SYD_TEST_TMPDIR to override.");
Some(TempDir::new(&path))
}
Err(error) => {
println!("# chdir failed: {error}.");
None
}
}
}
Err(errno) => {
println!("# mkdtemp failed: {errno}.");
None
}
}
};
let mut test_indices = Vec::new();
let mut test_env_arg = false;
#[expect(clippy::disallowed_methods)]
if let Ok(env) = std::env::var("SYD_TEST") {
if !env.is_empty() {
test_env_arg = true;
let arg: Arguments = env.into();
match arg {
Arguments::Index(i) => test_indices.push(i),
Arguments::Range(r) => test_indices.extend(r),
Arguments::Pattern(p) => {
for (idx, (name, _)) in TESTS.iter().enumerate() {
#[expect(clippy::disallowed_methods)]
let name = name.strip_prefix("test_syd_").unwrap();
if inamematch(&p, name) {
test_indices.push(idx + 1);
if p.to_ascii_lowercase().contains("exp") && name.starts_with("exp_") {
env::set_var("SYD_TEST_EXPENSIVE", "1");
}
}
}
}
}
}
}
let args: Vec<Arguments> = args
.into_iter()
.skip(skip_args)
.map(ArgVec::from)
.flat_map(|arg_vec| arg_vec.0)
.collect();
for arg in args {
match arg {
Arguments::Index(i) => test_indices.push(i),
Arguments::Range(r) => test_indices.extend(r),
Arguments::Pattern(p) => {
for (idx, (name, _)) in TESTS.iter().enumerate() {
#[expect(clippy::disallowed_methods)]
let name = name.strip_prefix("test_syd_").unwrap();
if inamematch(&p, name) {
test_indices.push(idx + 1);
if p.to_ascii_lowercase().contains("exp") && name.starts_with("exp_") {
env::set_var("SYD_TEST_EXPENSIVE", "1");
}
}
}
}
}
}
if !test_env_arg && args_is_empty {
test_indices.extend(1..=TESTS.len());
}
#[expect(clippy::disallowed_methods)]
let (seed, seed_set) = match env::var("SYD_TEST_SEED") {
Err(env::VarError::NotPresent) => {
let mut buf = vec![0u8; size_of::<nix::libc::c_uint>()];
let ret = unsafe {
nix::libc::getrandom(buf.as_mut_ptr().cast(), buf.len(), nix::libc::GRND_RANDOM)
};
if ret == buf.len() as nix::libc::ssize_t {
let ret = nix::libc::c_uint::from_ne_bytes(buf[..4].try_into()?);
eprintln!("# Determined test seed using /dev/random.");
(ret, false)
} else {
eprintln!(
"# getrandom failed ({}), using default seed...",
Errno::last()
);
(31415926, false)
}
}
Ok(val) => match val.parse::<nix::libc::c_uint>() {
Ok(val) => (val, true),
Err(error) => {
eprintln!("# Invalid test seed: {error}!");
return Ok(ExitCode::from(1));
}
},
Err(error) => {
eprintln!("# Invalid test seed: {error}!");
return Ok(ExitCode::from(1));
}
};
if seed_set {
eprintln!("# Test seed: {seed}, manually set with SYD_TEST_SEED.");
} else {
eprintln!("# Test seed: {seed}, set SYD_TEST_SEED to override.");
}
eprintln!("# Shuffling tests using the Fischer-Yates algorithm...");
#[cfg(not(target_os = "android"))]
unsafe {
libc::srand(seed)
};
shuffle_vec(&mut test_indices);
let ntest = test_indices.len();
let ptest = if env::var_os("SYD_TEST_REEXEC").is_none() {
""
} else {
"# "
};
println!("{ptest}1..{ntest}");
let exp_test = env::var_os("SYD_TEST_EXPENSIVE").is_some();
let mut fail_hard = 0;
let mut fail_soft = 0;
let mut skip = 0;
let mut fail_names = Vec::new();
let mut skip_names = Vec::new();
let mut soft_fails = Vec::new();
let mut idx = 0;
let mut rtest = 0;
for &test_idx in test_indices.iter() {
let (name, test) = if let Some((name, test)) = TESTS.get(test_idx - 1) {
(name, test)
} else {
continue;
};
idx += 1;
#[expect(clippy::disallowed_methods)]
let name = name.strip_prefix("test_syd_").unwrap();
env::set_var("SYD_TEST_NAME", name);
if exp_test && !name.starts_with("exp_") {
println!("# ok {idx} - {name} # SKIP not expensive, unset SYD_TEST_EXPENSIVE to run");
skip += 1;
skip_names.push(name.to_string());
continue;
} else if !exp_test && name.starts_with("exp_") {
println!("# ok {idx} - {name} # SKIP expensive test, set SYD_TEST_EXPENSIVE to run");
skip += 1;
skip_names.push(name.to_string());
continue;
}
let status = format!(
"{name} ({idx} of {ntest}: {} ok, {} notok, {} todo, {} left)",
idx - fail_hard - fail_soft - skip - 1,
fail_hard,
fail_soft + skip,
ntest - idx,
);
if tsl {
print!("\x1b]0;syd-test: {status}\x07");
}
println!("\x1b[92m*** {status} ***\x1b[0m");
mkdir(name, Mode::from_bits_truncate(0o700))?;
fchmodat(
AT_FDCWD,
name,
Mode::from_bits_truncate(0o700),
FchmodatFlags::FollowSymlink,
)?;
chdir(name)?;
let cwd = std::env::current_dir()?.canonicalize()?;
std::env::set_var("PWD", &cwd);
std::env::set_var("TMP", &cwd);
std::env::set_var("TMPDIR", &cwd);
let result = test();
chdir("..")?;
rtest += 1;
match result {
Ok(_) => {
if std::env::var_os("SYD_TEST_SOFT_FAIL").is_some() {
fail_soft += 1;
soft_fails.push(name.to_string());
std::env::remove_var("SYD_TEST_SOFT_FAIL");
println!("{ptest}ok {idx} - {name} # TODO");
} else {
println!("{ptest}ok {idx} - {name}");
}
if tmpdir.as_ref().map(|t| t.keep).unwrap_or(false) {
let cwd = getcwd()
.map(XPathBuf::from)
.unwrap_or_else(|_| XPathBuf::from("?"));
eprintln!("# Keeping test directory \"{cwd}\"");
} else {
let _ = remove_dir_all(name);
}
}
Err(error) => {
println!("{ptest}not ok {idx} - {name} - FAIL: {error}");
fail_hard += 1;
fail_names.push(name.to_string());
if fail_quick {
break;
}
}
}
}
let succ = rtest - fail_hard.saturating_sub(fail_soft).saturating_sub(skip);
println!("# {succ} tests passed.");
println!("# {skip} tests skipped.");
if fail_soft > 0 {
soft_fails.sort();
println!("# {fail_soft} tests failed soft, aka known failures:");
for (index, test) in soft_fails.iter().enumerate() {
println!("# {}. {}", index + 1, test);
}
} else {
println!("# {fail_soft} tests failed soft. No known failures! \\o/");
}
if fail_hard > 0 {
fail_names.sort();
println!("# {fail_hard} tests failed hard, aka breaking failures:");
for (index, test) in fail_names.iter().enumerate() {
println!("# {}. {}", index + 1, test);
}
} else {
println!("# {fail_hard} tests failed hard. No breaking failures! \\o/");
}
let code = if cfg!(coverage) {
0
} else {
fail_hard.try_into().unwrap_or(127)
};
if code != 0 {
if let Some(mut tmpdir) = tmpdir {
tmpdir.keep = true;
let tmpdir = tmpdir.path.display();
eprintln!("Keeping temporary test directory \"{tmpdir}\".");
}
}
Ok(ExitCode::from(code))
}