Lines
100 %
Functions
26.67 %
Branches
//! Self-hosted test framework: `DEFTEST`, `ASSERT-EQUAL`, `RUN-TESTS`.
//!
//! `DEFTEST` registers a named body in the symbol table; `ASSERT-EQUAL`
//! signals a Compile error on mismatch (caught by `RUN-TESTS`); the
//! runner iterates the test registry and produces a multi-line String
//! summary with pass/fail counts and per-failure detail.
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{compile_expr, eval_value, format_expr};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::compile_static_result_for_stack;
/// `(deftest NAME body...)` registers a test in the symbol table.
/// `RUN-TESTS` later iterates the registry and evaluates each body
/// once, counting passes / failures. The test body is wrapped in
/// `BEGIN` so multi-form bodies behave correctly.
pub(super) fn deftest(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() < 2 {
return Err(Error::Compile(
"DEFTEST requires a name and at least one body form".to_string(),
));
}
let name = match &args[0] {
Expr::Symbol(s) => s.clone(),
other => {
return Err(Error::Compile(format!(
"DEFTEST: expected symbol for test name, got {}",
format_expr(other)
)));
};
let body = if args.len() == 2 {
args[1].clone()
} else {
let mut forms = Vec::with_capacity(args.len());
forms.push(Expr::Symbol("BEGIN".to_string()));
forms.extend_from_slice(&args[1..]);
Expr::List(forms)
symbols.register_test(&name, body);
Ok(Expr::Quote(Box::new(Expr::Symbol(name))))
pub(super) fn compile_deftest(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let result = deftest(symbols, args)?;
compile_expr(ctx, emit, symbols, &result)
/// `(assert-equal A B)` evaluates both sides; if the resulting
/// `Expr` values aren't `==` it signals a Compile error. Inside a
/// test, that error is caught by `RUN-TESTS` and counted as a
/// failure; outside a test it propagates as any other compile
/// error.
pub(super) fn assert_equal(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() != 2 {
return Err(Error::Arity {
name: "ASSERT-EQUAL".to_string(),
expected: 2,
actual: args.len(),
});
let a = eval_value(symbols, &args[0])?;
let b = eval_value(symbols, &args[1])?;
if a == b {
Ok(Expr::Nil)
Err(Error::Compile(format!(
"assertion failed: {} != {}",
format_expr(&a),
format_expr(&b)
)))
pub(super) fn compile_assert_equal(
let result = assert_equal(symbols, args)?;
/// `(run-tests)` evaluates every test registered via `DEFTEST` and
/// returns a multi-line String summary: `ran N tests: P passed, F
/// failed' followed by per-failure detail lines.
pub(super) fn run_tests(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if !args.is_empty() {
name: "RUN-TESTS".to_string(),
expected: 0,
let tests = symbols.tests();
let mut pass = 0usize;
let mut failures: Vec<String> = Vec::new();
for (name, body) in &tests {
match eval_value(symbols, body) {
Ok(_) => pass += 1,
Err(err) => failures.push(format!(" {name}: {err}")),
let total = tests.len();
let fail = failures.len();
let mut lines = vec![format!("ran {total} tests: {pass} passed, {fail} failed")];
if !failures.is_empty() {
lines.push("failures:".to_string());
lines.extend(failures);
Ok(Expr::String(lines.join("\n")))
pub(super) fn compile_run_tests(
let result = run_tests(symbols, args)?;
pub(super) fn compile_deftest_for_stack(
) -> Result<WasmType> {
compile_static_result_for_stack(ctx, emit, symbols, &result)
pub(super) fn compile_assert_equal_for_stack(
pub(super) fn compile_run_tests_for_stack(