Lines
92.59 %
Functions
100 %
Branches
//! Unit tests for the introspection / test-framework special forms.
use crate::ast::{Expr, Fraction};
use crate::runtime::SymbolTable;
use super::apropos::apropos;
use super::describe_pp::pp;
use super::test_framework::{assert_equal, deftest, run_tests};
fn syms() -> SymbolTable {
SymbolTable::with_builtins()
}
#[test]
fn pp_constant_number_returns_string() {
let result = pp(&mut syms(), &[Expr::Number(Fraction::from_integer(42))]).unwrap();
assert_eq!(result, Expr::String("42".to_string()));
fn pp_string_literal_round_trips() {
let result = pp(&mut syms(), &[Expr::String("hi".into())]).unwrap();
// format_expr prints strings with their content (no quoting)
// — matches DESCRIBE's value-line convention.
assert_eq!(result, Expr::String("hi".to_string()));
fn pp_nil_returns_nil_text() {
let result = pp(&mut syms(), &[Expr::Nil]).unwrap();
assert_eq!(result, Expr::String("NIL".to_string()));
fn pp_arity_zero_errors() {
assert!(pp(&mut syms(), &[]).is_err());
fn pp_arity_two_errors() {
assert!(pp(&mut syms(), &[Expr::Nil, Expr::Nil]).is_err());
fn apropos_finds_entity_count() {
let result = apropos(&mut syms(), &[Expr::String("entity-count".into())]).unwrap();
let Expr::Quote(inner) = result else {
panic!("apropos must return a quoted list, got {result:?}");
};
let Expr::List(elems) = *inner else {
panic!("apropos must return a quoted list");
assert!(
elems
.iter()
.any(|e| matches!(e, Expr::Symbol(s) if s == "ENTITY-COUNT")),
"missing ENTITY-COUNT in {elems:?}",
);
fn apropos_is_case_insensitive() {
let result = apropos(&mut syms(), &[Expr::String("entity-COUNT".into())]).unwrap();
panic!("expected quoted list");
panic!("expected list");
.any(|e| matches!(e, Expr::Symbol(s) if s == "ENTITY-COUNT"))
fn apropos_results_are_sorted() {
let result = apropos(&mut syms(), &[Expr::String("entity".into())]).unwrap();
let names: Vec<&str> = elems
.filter_map(|e| match e {
Expr::Symbol(s) => Some(s.as_str()),
_ => None,
})
.collect();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted, "apropos must return sorted names");
fn apropos_no_match_returns_empty_quoted_list() {
let result = apropos(&mut syms(), &[Expr::String("nonexistent-xyz-12345".into())]).unwrap();
assert!(elems.is_empty(), "got {elems:?}");
fn apropos_rejects_number_arg() {
let err = apropos(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
.expect_err("number should error");
assert!(err.to_string().contains("APROPOS"));
fn deftest_registers_named_test() {
let mut s = syms();
let name = Expr::Symbol("smoke".to_string());
let body = Expr::Number(Fraction::from_integer(1));
let result = deftest(&mut s, &[name, body.clone()]).unwrap();
assert_eq!(result, Expr::Quote(Box::new(Expr::Symbol("smoke".into()))));
assert_eq!(s.tests(), vec![("smoke".to_string(), body)]);
fn deftest_wraps_multi_form_body_in_begin() {
let _ = deftest(
&mut s,
&[
Expr::Symbol("multi".into()),
Expr::Number(Fraction::from_integer(1)),
Expr::Number(Fraction::from_integer(2)),
],
)
.unwrap();
let (_, body) = s.tests().into_iter().next().unwrap();
let Expr::List(elems) = body else {
panic!("multi-form body must be wrapped in a BEGIN list");
assert_eq!(elems[0], Expr::Symbol("BEGIN".to_string()));
assert_eq!(elems.len(), 3);
fn deftest_rejects_non_symbol_name() {
let err = deftest(&mut syms(), &[Expr::String("not-a-sym".into()), Expr::Nil])
.expect_err("string name must error");
assert!(err.to_string().contains("DEFTEST"));
/// Re-registering a test name overwrites in place instead of appending, so the
/// two-surface compile (a DEFTEST evaluated on both the eval and codegen pass)
/// can't double-register and double-run a test. The latest body wins.
fn deftest_reregistration_is_idempotent() {
let name = Expr::Symbol("dup".to_string());
let first = Expr::Number(Fraction::from_integer(1));
let second = Expr::Number(Fraction::from_integer(2));
deftest(&mut s, &[name.clone(), first]).unwrap();
deftest(&mut s, &[name, second.clone()]).unwrap();
assert_eq!(s.tests(), vec![("dup".to_string(), second)]);
fn assert_equal_passes_for_equal_numbers() {
let one = Expr::Number(Fraction::from_integer(1));
let result = assert_equal(&mut syms(), &[one.clone(), one]).unwrap();
assert_eq!(result, Expr::Nil);
fn assert_equal_fails_for_unequal() {
let err = assert_equal(
&mut syms(),
.expect_err("1 != 2");
assert!(err.to_string().contains("assertion failed"));
fn run_tests_empty_registry_reports_zero() {
let result = run_tests(&mut syms(), &[]).unwrap();
let Expr::String(s) = result else {
panic!("expected String summary");
assert!(s.contains("0 tests"));
assert!(s.contains("0 passed"));
assert!(s.contains("0 failed"));
fn run_tests_counts_one_pass_one_fail() {
s.register_test(
"ok",
Expr::List(vec![
Expr::Symbol("ASSERT-EQUAL".into()),
]),
"bad",
let result = run_tests(&mut s, &[]).unwrap();
let Expr::String(summary) = result else {
panic!("expected summary string");
assert!(summary.contains("2 tests"));
assert!(summary.contains("1 passed"));
assert!(summary.contains("1 failed"));
assert!(summary.contains("bad"));
fn run_tests_rejects_args() {
let err = run_tests(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
.expect_err("args must error");
assert!(err.to_string().contains("RUN-TESTS"));