1
//! Unit tests for the introspection / test-framework special forms.
2

            
3
use crate::ast::{Expr, Fraction};
4
use crate::runtime::SymbolTable;
5

            
6
use super::apropos::apropos;
7
use super::describe_pp::pp;
8
use super::test_framework::{assert_equal, deftest, run_tests};
9

            
10
19
fn syms() -> SymbolTable {
11
19
    SymbolTable::with_builtins()
12
19
}
13

            
14
#[test]
15
1
fn pp_constant_number_returns_string() {
16
1
    let result = pp(&mut syms(), &[Expr::Number(Fraction::from_integer(42))]).unwrap();
17
1
    assert_eq!(result, Expr::String("42".to_string()));
18
1
}
19

            
20
#[test]
21
1
fn pp_string_literal_round_trips() {
22
1
    let result = pp(&mut syms(), &[Expr::String("hi".into())]).unwrap();
23
    // format_expr prints strings with their content (no quoting)
24
    // — matches DESCRIBE's value-line convention.
25
1
    assert_eq!(result, Expr::String("hi".to_string()));
26
1
}
27

            
28
#[test]
29
1
fn pp_nil_returns_nil_text() {
30
1
    let result = pp(&mut syms(), &[Expr::Nil]).unwrap();
31
1
    assert_eq!(result, Expr::String("nil".to_string()));
32
1
}
33

            
34
#[test]
35
1
fn pp_arity_zero_errors() {
36
1
    assert!(pp(&mut syms(), &[]).is_err());
37
1
}
38

            
39
#[test]
40
1
fn pp_arity_two_errors() {
41
1
    assert!(pp(&mut syms(), &[Expr::Nil, Expr::Nil]).is_err());
42
1
}
43

            
44
#[test]
45
1
fn apropos_finds_entity_count() {
46
1
    let result = apropos(&mut syms(), &[Expr::String("entity-count".into())]).unwrap();
47
1
    let Expr::Quote(inner) = result else {
48
        panic!("apropos must return a quoted list, got {result:?}");
49
    };
50
1
    let Expr::List(elems) = *inner else {
51
        panic!("apropos must return a quoted list");
52
    };
53
1
    assert!(
54
1
        elems
55
1
            .iter()
56
1
            .any(|e| matches!(e, Expr::Symbol(s) if s == "ENTITY-COUNT")),
57
        "missing ENTITY-COUNT in {elems:?}",
58
    );
59
1
}
60

            
61
#[test]
62
1
fn apropos_is_case_insensitive() {
63
1
    let result = apropos(&mut syms(), &[Expr::String("entity-COUNT".into())]).unwrap();
64
1
    let Expr::Quote(inner) = result else {
65
        panic!("expected quoted list");
66
    };
67
1
    let Expr::List(elems) = *inner else {
68
        panic!("expected list");
69
    };
70
1
    assert!(
71
1
        elems
72
1
            .iter()
73
1
            .any(|e| matches!(e, Expr::Symbol(s) if s == "ENTITY-COUNT"))
74
    );
75
1
}
76

            
77
#[test]
78
1
fn apropos_results_are_sorted() {
79
1
    let result = apropos(&mut syms(), &[Expr::String("entity".into())]).unwrap();
80
1
    let Expr::Quote(inner) = result else {
81
        panic!("expected quoted list");
82
    };
83
1
    let Expr::List(elems) = *inner else {
84
        panic!("expected list");
85
    };
86
1
    let names: Vec<&str> = elems
87
1
        .iter()
88
14
        .filter_map(|e| match e {
89
14
            Expr::Symbol(s) => Some(s.as_str()),
90
            _ => None,
91
14
        })
92
1
        .collect();
93
1
    let mut sorted = names.clone();
94
1
    sorted.sort();
95
1
    assert_eq!(names, sorted, "apropos must return sorted names");
96
1
}
97

            
98
#[test]
99
1
fn apropos_no_match_returns_nil() {
100
1
    let result = apropos(&mut syms(), &[Expr::String("nonexistent-xyz-12345".into())]).unwrap();
101
1
    assert_eq!(result, Expr::Nil);
102
1
}
103

            
104
#[test]
105
1
fn apropos_rejects_number_arg() {
106
1
    let err = apropos(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
107
1
        .expect_err("number should error");
108
1
    assert!(err.to_string().contains("APROPOS"));
109
1
}
110

            
111
#[test]
112
1
fn deftest_registers_named_test() {
113
1
    let mut s = syms();
114
1
    let name = Expr::Symbol("smoke".to_string());
115
1
    let body = Expr::Number(Fraction::from_integer(1));
116
1
    let result = deftest(&mut s, &[name, body.clone()]).unwrap();
117
1
    assert_eq!(result, Expr::Quote(Box::new(Expr::Symbol("smoke".into()))));
118
1
    assert_eq!(s.tests(), vec![("smoke".to_string(), body)]);
119
1
}
120

            
121
#[test]
122
1
fn deftest_wraps_multi_form_body_in_begin() {
123
1
    let mut s = syms();
124
1
    let _ = deftest(
125
1
        &mut s,
126
1
        &[
127
1
            Expr::Symbol("multi".into()),
128
1
            Expr::Number(Fraction::from_integer(1)),
129
1
            Expr::Number(Fraction::from_integer(2)),
130
1
        ],
131
1
    )
132
1
    .unwrap();
133
1
    let (_, body) = s.tests().into_iter().next().unwrap();
134
1
    let Expr::List(elems) = body else {
135
        panic!("multi-form body must be wrapped in a BEGIN list");
136
    };
137
1
    assert_eq!(elems[0], Expr::Symbol("BEGIN".to_string()));
138
1
    assert_eq!(elems.len(), 3);
139
1
}
140

            
141
#[test]
142
1
fn deftest_rejects_non_symbol_name() {
143
1
    let err = deftest(&mut syms(), &[Expr::String("not-a-sym".into()), Expr::Nil])
144
1
        .expect_err("string name must error");
145
1
    assert!(err.to_string().contains("DEFTEST"));
146
1
}
147

            
148
/// Re-registering a test name overwrites in place instead of appending, so the
149
/// two-surface compile (a DEFTEST evaluated on both the eval and codegen pass)
150
/// can't double-register and double-run a test. The latest body wins.
151
#[test]
152
1
fn deftest_reregistration_is_idempotent() {
153
1
    let mut s = syms();
154
1
    let name = Expr::Symbol("dup".to_string());
155
1
    let first = Expr::Number(Fraction::from_integer(1));
156
1
    let second = Expr::Number(Fraction::from_integer(2));
157
1
    deftest(&mut s, &[name.clone(), first]).unwrap();
158
1
    deftest(&mut s, &[name, second.clone()]).unwrap();
159
1
    assert_eq!(s.tests(), vec![("dup".to_string(), second)]);
160
1
}
161

            
162
#[test]
163
1
fn assert_equal_passes_for_equal_numbers() {
164
1
    let one = Expr::Number(Fraction::from_integer(1));
165
1
    let result = assert_equal(&mut syms(), &[one.clone(), one]).unwrap();
166
1
    assert_eq!(result, Expr::Nil);
167
1
}
168

            
169
#[test]
170
1
fn assert_equal_fails_for_unequal() {
171
1
    let err = assert_equal(
172
1
        &mut syms(),
173
1
        &[
174
1
            Expr::Number(Fraction::from_integer(1)),
175
1
            Expr::Number(Fraction::from_integer(2)),
176
1
        ],
177
    )
178
1
    .expect_err("1 != 2");
179
1
    assert!(err.to_string().contains("assertion failed"));
180
1
}
181

            
182
#[test]
183
1
fn run_tests_empty_registry_reports_zero() {
184
1
    let result = run_tests(&mut syms(), &[]).unwrap();
185
1
    let Expr::String(s) = result else {
186
        panic!("expected String summary");
187
    };
188
1
    assert!(s.contains("0 tests"));
189
1
    assert!(s.contains("0 passed"));
190
1
    assert!(s.contains("0 failed"));
191
1
}
192

            
193
#[test]
194
1
fn run_tests_counts_one_pass_one_fail() {
195
1
    let mut s = syms();
196
1
    s.register_test(
197
        "ok",
198
1
        Expr::List(vec![
199
1
            Expr::Symbol("ASSERT-EQUAL".into()),
200
1
            Expr::Number(Fraction::from_integer(1)),
201
1
            Expr::Number(Fraction::from_integer(1)),
202
1
        ]),
203
    );
204
1
    s.register_test(
205
        "bad",
206
1
        Expr::List(vec![
207
1
            Expr::Symbol("ASSERT-EQUAL".into()),
208
1
            Expr::Number(Fraction::from_integer(1)),
209
1
            Expr::Number(Fraction::from_integer(2)),
210
1
        ]),
211
    );
212
1
    let result = run_tests(&mut s, &[]).unwrap();
213
1
    let Expr::String(summary) = result else {
214
        panic!("expected summary string");
215
    };
216
1
    assert!(summary.contains("2 tests"));
217
1
    assert!(summary.contains("1 passed"));
218
1
    assert!(summary.contains("1 failed"));
219
1
    assert!(summary.contains("bad"));
220
1
}
221

            
222
#[test]
223
1
fn run_tests_rejects_args() {
224
1
    let err = run_tests(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
225
1
        .expect_err("args must error");
226
1
    assert!(err.to_string().contains("RUN-TESTS"));
227
1
}