use std::path::Path;
use nebu::Goldilocks;
use nox::{NoTrace, NullCalls, Outcome, Reduction};
use crate::ast;
use crate::diagnostic::Diagnostic;
use crate::ir::tree::lower::{nox::NoxCompiler, Noun};
use crate::pipeline::PreparedProject;
use crate::span::Span;
use crate::CompileOptions;
const ARENA: usize = 1 << 18;
const STACK_SIZE: usize = 256 * 1024 * 1024;
const TEST_BUDGET: u64 = 1_000_000;
pub fn discover_tests(file: &ast::File) -> Vec<String> {
file.items
.iter()
.filter_map(|item| match &item.node {
ast::Item::Fn(f) if f.is_test => Some(f.name.node.clone()),
_ => None,
})
.collect()
}
#[derive(Clone, Debug)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub error: Option<String>,
}
pub fn run_tests(entry_path: &Path, options: &CompileOptions) -> Result<String, Vec<Diagnostic>> {
if options.target_config.name != "nox" {
return Err(vec![Diagnostic::error(
format!("test execution for '{}' is unsupported: execution belongs to the target warrior; use prepare_test_programs or trident test", options.target_config.name),
Span::dummy(),
)]);
}
let mut options = options.clone();
options.cfg_flags.insert("test".to_string());
let project = PreparedProject::build_tests(entry_path, &options)?;
let files: Vec<_> = project.modules.iter().map(|m| &m.file).collect();
let mut results = Vec::new();
let mut skipped = 0;
for (module_index, module) in project.modules.iter().enumerate() {
for item in &module.file.items {
let ast::Item::Fn(function) = &item.node else {
continue;
};
if !function.is_test {
continue;
}
if function
.cfg
.as_ref()
.is_some_and(|f| !options.cfg_flags.contains(&f.node))
{
skipped += 1;
continue;
}
let name = format!("{}.{}", module.file.name.node, function.name.node);
let result = if !function.params.is_empty()
|| !function.type_params.is_empty()
|| function.return_ty.is_some()
|| function.body.is_none()
{
Err(
"tests require a body, no parameters, no generics and no return type"
.to_string(),
)
} else {
let mut selected = item.clone();
if let ast::Item::Fn(f) = &mut selected.node {
f.is_pub = true;
}
let mut entry = module.file.clone();
entry.kind = ast::FileKind::Module;
entry.items = vec![selected];
project.exports[module_index].check_entry_requirements(&entry, &options)?;
let mut compiler = NoxCompiler::new();
compiler
.compile_modules(&files, &entry, &options.cfg_flags)
.and_then(execute)
};
results.push(TestResult {
name,
passed: result.is_ok(),
error: result.err(),
});
}
}
if results.is_empty() {
return Ok(format!(
"No active #[test] functions found. {skipped} skipped.\n"
));
}
let passed = results.iter().filter(|r| r.passed).count();
let failed = results.len() - passed;
let mut report = format!(
"running {} test{} on nox\n",
results.len(),
if results.len() == 1 { "" } else { "s" }
);
for result in &results {
report.push_str(&format!(
" test {} ... {}\n",
result.name,
if result.passed { "ok" } else { "FAILED" }
));
if let Some(error) = &result.error {
report.push_str(&format!(" error: {error}\n"));
}
}
report.push_str(&format!(
"\ntest result: {}. {passed} passed; {failed} failed; {skipped} skipped\n",
if failed == 0 { "ok" } else { "FAILED" }
));
if failed == 0 {
Ok(report)
} else {
Err(vec![Diagnostic::error(report, Span::dummy())])
}
}
fn load(arena: &mut Reduction<ARENA>, noun: &Noun) -> Result<nox::Order, String> {
let value = match noun {
Noun::Atom(v) => arena.atom(Goldilocks::new(*v)),
Noun::Cell(head, tail) => {
let head = load(arena, head)?;
let tail = load(arena, tail)?;
arena.pair(head, tail)
}
};
value.ok_or_else(|| "test formula exceeds the nox arena capacity".to_string())
}
fn execute(formula: Noun) -> Result<(), String> {
std::thread::Builder::new()
.name("trident-test".to_string())
.stack_size(STACK_SIZE)
.spawn(move || {
let mut arena = Reduction::<ARENA>::new();
let entry = load(&mut arena, &formula)?;
let subject = arena
.atom(Goldilocks::ZERO)
.ok_or_else(|| "test arena full".to_string())?;
match nox::reduce(
&mut arena,
subject,
entry,
TEST_BUDGET,
&NullCalls,
&mut NoTrace,
) {
Outcome::Ok(_, _) => Ok(()),
Outcome::Halt(_) => Err(format!(
"nox test exhausted its {TEST_BUDGET}-reduction budget"
)),
Outcome::Error(error) => Err(format!("nox execution failed: {error:?}")),
}
})
.map_err(|e| format!("cannot start nox test worker: {e}"))?
.join()
.map_err(|_| "nox test worker panicked".to_string())?
}
#[derive(Clone, Debug)]
pub struct TestProgram {
pub name: String,
pub modules: Vec<crate::ModuleTir>,
}
#[derive(Clone, Debug)]
pub struct TestPrograms {
pub tests: Vec<TestProgram>,
pub skipped: usize,
}
pub fn prepare_test_programs(
entry_path: &Path,
options: &CompileOptions,
) -> Result<TestPrograms, Vec<Diagnostic>> {
use crate::tir::{builder::TIRBuilder, optimize::optimize as optimize_tir, TIROp};
let mut options = options.clone();
options.cfg_flags.insert("test".to_string());
let mut project = PreparedProject::build_tests(entry_path, &options)?;
let mut entries = Vec::new();
let mut skipped = 0;
for (index, module) in project.modules.iter().enumerate() {
for item in &module.file.items {
let ast::Item::Fn(function) = &item.node else {
continue;
};
if !function.is_test {
continue;
}
if function
.cfg
.as_ref()
.is_some_and(|f| !options.cfg_flags.contains(&f.node))
{
skipped += 1;
continue;
}
if !function.params.is_empty()
|| !function.type_params.is_empty()
|| function.return_ty.is_some()
|| function.body.is_none()
{
return Err(vec![Diagnostic::error(
"tests require a body, no parameters, no generics and no return type"
.to_string(),
function.name.span,
)]);
}
let mut selected = item.clone();
if let ast::Item::Fn(f) = &mut selected.node {
f.is_pub = true;
}
let mut view = module.file.clone();
view.kind = ast::FileKind::Module;
view.items = vec![selected];
project.exports[index].check_entry_requirements(&view, &options)?;
entries.push((module.file.name.node.clone(), function.name.node.clone()));
}
}
for module in &mut project.modules {
module.file.kind = ast::FileKind::Module;
for item in &mut module.file.items {
if let ast::Item::Fn(function) = &mut item.node {
function.is_test = false;
}
}
}
let mut harness = "__trident_tests".to_string();
while project
.modules
.iter()
.any(|m| m.file.name.node.replace('.', "_") == harness)
{
harness.push('_');
}
let files: Vec<_> = project.modules.iter().map(|m| &m.file).collect();
let modules: Vec<_> = project
.modules
.iter()
.zip(&project.exports)
.map(|(pm, exports)| {
let ops = TIRBuilder::new(options.target_config.clone())
.with_target_intrinsics(options.target_intrinsic_widths())
.with_cfg_flags(options.cfg_flags.clone())
.with_module_types(&files)
.with_intrinsics(project.intrinsic_map())
.with_module_aliases(project.module_aliases())
.with_constants(project.external_constants())
.with_mono_instances(exports.mono_instances.clone())
.with_call_resolutions(exports.call_resolutions.clone())
.build_file(&pm.file);
crate::ModuleTir {
name: pm.file.name.node.clone(),
is_program: false,
ops: optimize_tir(ops),
}
})
.collect();
let tests = entries
.into_iter()
.map(|(module, function)| {
let mut modules = modules.clone();
modules.push(crate::ModuleTir {
name: harness.clone(),
is_program: true,
ops: vec![
TIROp::Entry("main".into()),
TIROp::FnStart("main".into()),
TIROp::Call(format!("@{}__{}", module.replace('.', "_"), function)),
TIROp::Return,
TIROp::FnEnd,
],
});
TestProgram {
name: format!("{module}.{function}"),
modules,
}
})
.collect();
Ok(TestPrograms { tests, skipped })
}