#![expect(
clippy::redundant_pub_crate,
reason = "explicit pub(crate) documents the crate-wide visibility intent at each item"
)]
use std::path::{Path, PathBuf};
use airsl::{Engine, Policy, Script};
struct FileOutcome {
passed: usize,
failures: Vec<String>,
}
pub(crate) fn run(path: &Path, policy: &Policy) -> i32 {
let files = match discover(path) {
Ok(files) => files,
Err(error) => {
eprintln!("airsl: {error}");
return 1;
}
};
if files.is_empty() {
eprintln!("airsl: no test files found under `{}`", path.display());
eprintln!(" a test file is named `*_test.lua` or `test_*.lua`");
return 1;
}
let mut passed = 0;
let mut failures = Vec::new();
for file in &files {
let outcome = run_file(file, policy);
passed += outcome.passed;
for failure in outcome.failures {
failures.push(failure);
}
}
println!();
if failures.is_empty() {
println!("{passed} passed, 0 failed ({} files)", files.len());
return 0;
}
println!(
"{passed} passed, {} failed ({} files)",
failures.len(),
files.len()
);
println!();
for failure in &failures {
println!("{failure}");
}
1
}
fn discover(path: &Path) -> std::io::Result<Vec<PathBuf>> {
if path.is_file() {
return Ok(vec![path.to_path_buf()]);
}
let mut found = Vec::new();
for entry in walkdir::WalkDir::new(path).sort_by_file_name() {
let entry = entry?;
if entry.file_type().is_file() && is_test_file(entry.path()) {
found.push(entry.path().to_path_buf());
}
}
Ok(found)
}
fn is_test_file(path: &Path) -> bool {
if path.extension().is_none_or(|extension| extension != "lua") {
return false;
}
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
name.ends_with("_test.lua") || name.starts_with("test_")
}
fn run_file(file: &Path, policy: &Policy) -> FileOutcome {
let mut outcome = FileOutcome {
passed: 0,
failures: Vec::new(),
};
println!("{}", file.display());
let engine = match Engine::builder().policy(policy.clone()).build() {
Ok(engine) => engine,
Err(error) => {
outcome
.failures
.push(format!("{}: {error}", file.display()));
return outcome;
}
};
let script = match Script::from_file(file) {
Ok(script) => script,
Err(error) => {
outcome
.failures
.push(format!("{}: {error}", file.display()));
return outcome;
}
};
let table = match engine.eval_to::<mlua::Table>(&script) {
Ok(table) => table,
Err(error) => {
outcome.failures.push(format!(
"{}: {error}\n a test file must return a table of named functions",
file.display()
));
return outcome;
}
};
for (name, case) in cases(&table) {
match case.call::<()>(()) {
Ok(()) => {
outcome.passed += 1;
println!(" ok {name}");
}
Err(error) => {
println!(" FAIL {name}");
outcome
.failures
.push(format!("{}: {name}\n {error}", file.display()));
}
}
}
outcome
}
fn cases(table: &mlua::Table) -> Vec<(String, mlua::Function)> {
let mut found: Vec<(String, mlua::Function)> = table
.pairs::<mlua::Value, mlua::Value>()
.filter_map(std::result::Result::ok)
.filter_map(|(key, value)| match (key, value) {
(mlua::Value::String(name), mlua::Value::Function(case)) => {
name.to_str().ok().map(|name| (name.to_owned(), case))
}
_ => None,
})
.collect();
found.sort_by(|left, right| left.0.cmp(&right.0));
found
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
use super::{discover, is_test_file, run};
use airsl::Policy;
use std::path::Path;
fn write(dir: &Path, name: &str, body: &str) {
std::fs::write(dir.join(name), body).unwrap();
}
#[test]
fn both_naming_conventions_are_recognised() {
assert!(is_test_file(Path::new("a/index_test.lua")));
assert!(is_test_file(Path::new("a/test_index.lua")));
}
#[test]
fn an_ordinary_lua_file_is_not_a_test_file() {
assert!(!is_test_file(Path::new("a/index.lua")));
assert!(!is_test_file(Path::new("a/testing.lua")));
assert!(!is_test_file(Path::new("a/test_index.txt")));
}
#[test]
fn discovery_is_sorted_and_recursive() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("sub")).unwrap();
write(dir.path(), "z_test.lua", "return {}");
write(dir.path(), "a_test.lua", "return {}");
write(&dir.path().join("sub"), "m_test.lua", "return {}");
write(dir.path(), "ignored.lua", "return {}");
let found: Vec<String> = discover(dir.path())
.unwrap()
.iter()
.map(|path| {
path.strip_prefix(dir.path())
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect();
assert_eq!(found, ["a_test.lua", "sub/m_test.lua", "z_test.lua"]);
}
#[test]
fn a_passing_suite_exits_zero() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return { works = function() assert(1 + 1 == 2) end }",
);
assert_eq!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn a_failing_assertion_exits_non_zero() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return { broken = function() assert(false, 'nope') end }",
);
assert_ne!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn one_failure_among_several_still_fails_the_run() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return {
ok_one = function() end,
ok_two = function() end,
bad = function() error('boom') end,
}",
);
assert_ne!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn finding_no_tests_at_all_is_a_failure_not_a_pass() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "not-a-test.lua", "return {}");
assert_ne!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn a_file_that_returns_no_table_is_reported_rather_than_skipped() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "a_test.lua", "return 42");
assert_ne!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn a_file_that_fails_to_compile_is_reported() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "a_test.lua", "this is not lua");
assert_ne!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn a_single_file_can_be_named_directly() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return { works = function() end }",
);
assert_eq!(run(&dir.path().join("a_test.lua"), &Policy::pure()), 0);
}
#[test]
fn non_function_entries_are_ignored_rather_than_run() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return { fixture = 'data', works = function() end }",
);
assert_eq!(run(dir.path(), &Policy::pure()), 0);
}
#[test]
fn tests_run_under_the_policy_they_were_given() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a_test.lua",
"return { denied = function()
local ok = pcall(airsstack.fs.read, '/etc/hostname')
assert(not ok, 'reading should have been refused')
end }",
);
assert_eq!(run(dir.path(), &Policy::confined()), 0);
}
}