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_empty_quoted_list() {
100
1
    let result = apropos(&mut syms(), &[Expr::String("nonexistent-xyz-12345".into())]).unwrap();
101
1
    let Expr::Quote(inner) = result else {
102
        panic!("expected quoted list");
103
    };
104
1
    let Expr::List(elems) = *inner else {
105
        panic!("expected list");
106
    };
107
1
    assert!(elems.is_empty(), "got {elems:?}");
108
1
}
109

            
110
#[test]
111
1
fn apropos_rejects_number_arg() {
112
1
    let err = apropos(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
113
1
        .expect_err("number should error");
114
1
    assert!(err.to_string().contains("APROPOS"));
115
1
}
116

            
117
#[test]
118
1
fn deftest_registers_named_test() {
119
1
    let mut s = syms();
120
1
    let name = Expr::Symbol("smoke".to_string());
121
1
    let body = Expr::Number(Fraction::from_integer(1));
122
1
    let result = deftest(&mut s, &[name, body.clone()]).unwrap();
123
1
    assert_eq!(result, Expr::Quote(Box::new(Expr::Symbol("smoke".into()))));
124
1
    assert_eq!(s.tests(), vec![("smoke".to_string(), body)]);
125
1
}
126

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

            
147
#[test]
148
1
fn deftest_rejects_non_symbol_name() {
149
1
    let err = deftest(&mut syms(), &[Expr::String("not-a-sym".into()), Expr::Nil])
150
1
        .expect_err("string name must error");
151
1
    assert!(err.to_string().contains("DEFTEST"));
152
1
}
153

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

            
168
#[test]
169
1
fn assert_equal_passes_for_equal_numbers() {
170
1
    let one = Expr::Number(Fraction::from_integer(1));
171
1
    let result = assert_equal(&mut syms(), &[one.clone(), one]).unwrap();
172
1
    assert_eq!(result, Expr::Nil);
173
1
}
174

            
175
#[test]
176
1
fn assert_equal_fails_for_unequal() {
177
1
    let err = assert_equal(
178
1
        &mut syms(),
179
1
        &[
180
1
            Expr::Number(Fraction::from_integer(1)),
181
1
            Expr::Number(Fraction::from_integer(2)),
182
1
        ],
183
    )
184
1
    .expect_err("1 != 2");
185
1
    assert!(err.to_string().contains("assertion failed"));
186
1
}
187

            
188
#[test]
189
1
fn run_tests_empty_registry_reports_zero() {
190
1
    let result = run_tests(&mut syms(), &[]).unwrap();
191
1
    let Expr::String(s) = result else {
192
        panic!("expected String summary");
193
    };
194
1
    assert!(s.contains("0 tests"));
195
1
    assert!(s.contains("0 passed"));
196
1
    assert!(s.contains("0 failed"));
197
1
}
198

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

            
228
#[test]
229
1
fn run_tests_rejects_args() {
230
1
    let err = run_tests(&mut syms(), &[Expr::Number(Fraction::from_integer(1))])
231
1
        .expect_err("args must error");
232
1
    assert!(err.to_string().contains("RUN-TESTS"));
233
1
}