use std::path::{Path, PathBuf};
use std::process::ExitCode;
use doge_compiler::Stmt;
use crate::{EXIT_FAILURE, EXIT_OK, MISSING_FILE_HEADLINE};
const TEST_PREFIX: &str = "test";
const TEST_FILE_PREFIX: &str = "test_";
const DOGE_EXT: &str = ".doge";
const PASS_HEADLINE: &str = "much wow. very pass.";
const FAIL_HEADLINE: &str = "very fail. much sad.";
const EMPTY_HEADLINE: &str = "very empty. much untested.";
#[derive(Default)]
struct Totals {
discovered: usize,
passed: usize,
failed: usize,
}
pub(crate) fn run_test(path: &str) -> ExitCode {
let files = match collect_files(path) {
Ok(files) => files,
Err(code) => return code,
};
let mut totals = Totals::default();
for file in &files {
run_file(file, &mut totals);
}
if totals.discovered == 0 && totals.failed == 0 {
eprintln!(
"{EMPTY_HEADLINE}\n\n no tests found — a test is a top-level function that takes no arguments and whose name starts with `{TEST_PREFIX}`"
);
return ExitCode::from(EXIT_FAILURE);
}
println!();
if totals.failed == 0 {
println!("{PASS_HEADLINE} {} passed.", totals.passed);
ExitCode::from(EXIT_OK)
} else {
println!(
"{FAIL_HEADLINE} {} passed, {} failed.",
totals.passed, totals.failed
);
ExitCode::from(EXIT_FAILURE)
}
}
fn run_file(path: &Path, totals: &mut Totals) {
let display = path.display().to_string();
let source = match std::fs::read_to_string(path) {
Ok(source) => source,
Err(err) => {
eprintln!("{MISSING_FILE_HEADLINE}\n\n doge cannot read {display}: {err}");
totals.failed += 1;
return;
}
};
let deps = match crate::deps::locate(Some(&display)) {
Ok(located) => located.deps,
Err(message) => {
eprintln!("{message}");
totals.failed += 1;
return;
}
};
let program = match doge_compiler::load_program_with_deps(&display, &source, deps) {
Ok(program) => program,
Err(diag) => {
eprint!("{}", diag.render());
totals.failed += 1;
return;
}
};
if let Err(diag) = doge_compiler::check_program(&program) {
eprint!("{}", diag.render());
totals.failed += 1;
return;
}
let tests: Vec<String> = program.files[0]
.script
.stmts
.iter()
.filter_map(|stmt| match stmt {
Stmt::FuncDef { name, params, .. }
if name.starts_with(TEST_PREFIX) && params.is_empty() =>
{
Some(name.clone())
}
_ => None,
})
.collect();
if tests.is_empty() {
return;
}
let program = std::sync::Arc::new(program);
let mut interp = doge_interp::Interp::new();
println!("such test: {display}");
if let Err(err) = interp.prepare(program.clone()) {
let (_, line) = interp.error_site();
println!(
" ✗ (setup) — {}: {} ({display}:{line})",
err.kind.as_str(),
err.message
);
totals.failed += 1;
return;
}
for name in &tests {
totals.discovered += 1;
match interp.call_entry_function(name) {
Ok(_) => {
println!(" ✓ {name}");
totals.passed += 1;
}
Err(err) => {
let (_, line) = interp.error_site();
println!(
" ✗ {name} — {}: {} ({display}:{line})",
err.kind.as_str(),
err.message
);
totals.failed += 1;
}
}
}
}
fn collect_files(path: &str) -> Result<Vec<PathBuf>, ExitCode> {
let root = Path::new(path);
let meta = std::fs::metadata(root).map_err(|err| {
eprintln!("{MISSING_FILE_HEADLINE}\n\n doge cannot read {path}: {err}");
ExitCode::from(EXIT_FAILURE)
})?;
if !meta.is_dir() {
return Ok(vec![root.to_path_buf()]);
}
let mut files = Vec::new();
collect_dir(root, &mut files)?;
files.sort();
Ok(files)
}
fn collect_dir(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), ExitCode> {
let entries = std::fs::read_dir(dir).map_err(|err| {
eprintln!(
"{MISSING_FILE_HEADLINE}\n\n doge cannot read {}: {err}",
dir.display()
);
ExitCode::from(EXIT_FAILURE)
})?;
for entry in entries {
let entry = entry.map_err(|err| {
eprintln!(
"{MISSING_FILE_HEADLINE}\n\n doge cannot read {}: {err}",
dir.display()
);
ExitCode::from(EXIT_FAILURE)
})?;
let path = entry.path();
if path.is_dir() {
collect_dir(&path, out)?;
} else if is_test_file(&path) {
out.push(path);
}
}
Ok(())
}
fn is_test_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(TEST_FILE_PREFIX) && name.ends_with(DOGE_EXT))
}