1
use std::fs;
2
use std::path::PathBuf;
3

            
4
use nomiscript::{Annotation, Expr, Program, Reader};
5

            
6
46
fn samples_dir() -> PathBuf {
7
46
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8
46
        .join("tests")
9
46
        .join("samples")
10
46
}
11

            
12
45
fn read_sample(name: &str) -> String {
13
45
    let path = samples_dir().join(name);
14
45
    fs::read_to_string(&path).unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e))
15
45
}
16

            
17
32
fn parse_sample(name: &str) -> Program {
18
32
    let content = read_sample(name);
19
32
    Reader::parse(&content).unwrap_or_else(|e| panic!("Failed to parse {name}: {e}"))
20
32
}
21

            
22
22
fn count_forms(program: &Program, symbol: &str) -> usize {
23
22
    program
24
22
        .exprs
25
22
        .iter()
26
218
        .filter(|expr| {
27
218
            matches!(expr, Expr::List(items) if matches!(items.first(), Some(Expr::Symbol(s)) if s == symbol))
28
218
        })
29
22
        .count()
30
22
}
31

            
32
10
fn count_top_level_exprs(program: &Program) -> usize {
33
10
    program.exprs.len()
34
10
}
35

            
36
32
fn eval_query(query: &Expr, program: &Program) -> Option<i64> {
37
32
    let items = query.as_list()?;
38
32
    let func = items.first()?.as_symbol()?;
39

            
40
32
    match func {
41
32
        "COUNT" => {
42
32
            let arg = items.get(1)?;
43
32
            if let Expr::Quote(inner) = arg {
44
32
                let symbol = inner.as_symbol()?;
45
32
                if symbol == "EXPRS" {
46
10
                    Some(count_top_level_exprs(program) as i64)
47
                } else {
48
22
                    Some(count_forms(program, symbol) as i64)
49
                }
50
            } else {
51
                None
52
            }
53
        }
54
        _ => None,
55
    }
56
32
}
57

            
58
32
fn eval_test_annotation(ann: &Annotation, program: &Program) -> Result<bool, String> {
59
32
    if ann.name != "test" {
60
        return Ok(true);
61
32
    }
62

            
63
32
    let items = ann
64
32
        .value
65
32
        .as_list()
66
32
        .ok_or_else(|| "test annotation must be a list".to_string())?;
67

            
68
32
    let op = items
69
32
        .first()
70
32
        .and_then(|e| e.as_symbol())
71
32
        .ok_or_else(|| "first element must be an operator symbol".to_string())?;
72

            
73
32
    match op {
74
32
        "=" => {
75
32
            let lhs = items
76
32
                .get(1)
77
32
                .ok_or_else(|| "missing left operand".to_string())?;
78
32
            let rhs = items
79
32
                .get(2)
80
32
                .ok_or_else(|| "missing right operand".to_string())?;
81

            
82
32
            let lhs_val = eval_query(lhs, program)
83
32
                .ok_or_else(|| format!("cannot evaluate query: {lhs:?}"))?;
84
32
            let rhs_val = match rhs {
85
32
                Expr::Number(n) => n.to_integer(),
86
                _ => return Err(format!("expected number, got {rhs:?}")),
87
            };
88

            
89
32
            Ok(lhs_val == rhs_val)
90
        }
91
        _ => Err(format!("unknown operator: {op}")),
92
    }
93
32
}
94

            
95
const SAMPLE_FILES: &[&str] = &[
96
    "basics.nms",
97
    "recursion.nms",
98
    "lists.nms",
99
    "higher_order.nms",
100
    "finance.nms",
101
    "strings_and_io.nms",
102
    "let_forms.nms",
103
    "data_structures.nms",
104
    "map_family.nms",
105
];
106

            
107
mod sample_parsing {
108
    use super::*;
109

            
110
    #[test]
111
1
    fn test_all_samples_match_annotations() {
112
9
        for &sample in SAMPLE_FILES {
113
9
            let program = parse_sample(sample);
114

            
115
26
            for ann in &program.annotations {
116
26
                if ann.name == "test" {
117
26
                    let result = eval_test_annotation(ann, &program);
118
26
                    match result {
119
26
                        Ok(true) => {}
120
                        Ok(false) => {
121
                            panic!("{}: test annotation failed: {:?}", sample, ann.value);
122
                        }
123
                        Err(e) => {
124
                            panic!("{sample}: error evaluating annotation: {e}");
125
                        }
126
                    }
127
                }
128
            }
129
        }
130
1
    }
131

            
132
    #[test]
133
1
    fn test_annotation_parsing() {
134
1
        let content = "; @test (= (count 'defun) 1)\n(defun foo () nil)";
135
1
        let program = Reader::parse(content).unwrap();
136
1
        assert_eq!(program.annotations.len(), 1);
137
1
        assert_eq!(program.annotations[0].name, "test");
138

            
139
1
        let result = eval_test_annotation(&program.annotations[0], &program);
140
1
        assert!(result.unwrap());
141
1
    }
142
}
143

            
144
58
fn extract_defun_name(expr: &Expr) -> Option<&str> {
145
58
    if let Expr::List(items) = expr
146
58
        && let (Some(Expr::Symbol(sym)), Some(Expr::Symbol(name))) = (items.first(), items.get(1))
147
58
        && sym == "DEFUN"
148
    {
149
49
        return Some(name.as_str());
150
9
    }
151
9
    None
152
58
}
153

            
154
7
fn extract_defvar_name(expr: &Expr) -> Option<&str> {
155
7
    if let Expr::List(items) = expr
156
7
        && let (Some(Expr::Symbol(sym)), Some(Expr::Symbol(name))) = (items.first(), items.get(1))
157
7
        && sym == "DEFVAR"
158
    {
159
2
        return Some(name.as_str());
160
5
    }
161
5
    None
162
7
}
163

            
164
mod sample_structure {
165
    use super::*;
166

            
167
    #[test]
168
1
    fn test_basics_has_functions() {
169
1
        let program = parse_sample("basics.nms");
170

            
171
1
        let defun_names: Vec<&str> = program
172
1
            .exprs
173
1
            .iter()
174
1
            .filter_map(extract_defun_name)
175
1
            .collect();
176
1
        let defvar_names: Vec<&str> = program
177
1
            .exprs
178
1
            .iter()
179
1
            .filter_map(extract_defvar_name)
180
1
            .collect();
181

            
182
1
        assert!(defvar_names.contains(&"PI"));
183
1
        assert!(defvar_names.contains(&"E"));
184
1
        assert!(defun_names.contains(&"SQUARE"));
185
1
        assert!(defun_names.contains(&"CUBE"));
186
1
        assert!(defun_names.contains(&"ABS"));
187
1
        assert!(defun_names.contains(&"MAX"));
188
1
        assert!(defun_names.contains(&"MIN"));
189
1
    }
190

            
191
    #[test]
192
1
    fn test_recursion_has_factorial() {
193
1
        let program = parse_sample("recursion.nms");
194
1
        let defun_names: Vec<&str> = program
195
1
            .exprs
196
1
            .iter()
197
1
            .filter_map(extract_defun_name)
198
1
            .collect();
199
1
        assert!(defun_names.contains(&"FACTORIAL"));
200
1
    }
201

            
202
    #[test]
203
1
    fn test_lists_has_map_filter_fold() {
204
1
        let program = parse_sample("lists.nms");
205
1
        let defun_names: Vec<&str> = program
206
1
            .exprs
207
1
            .iter()
208
1
            .filter_map(extract_defun_name)
209
1
            .collect();
210

            
211
1
        assert!(defun_names.contains(&"MAP"));
212
1
        assert!(defun_names.contains(&"FILTER"));
213
1
        assert!(defun_names.contains(&"FOLD-LEFT"));
214
1
        assert!(defun_names.contains(&"FOLD-RIGHT"));
215
1
    }
216

            
217
    #[test]
218
1
    fn test_higher_order_has_compose() {
219
1
        let program = parse_sample("higher_order.nms");
220
1
        let defun_names: Vec<&str> = program
221
1
            .exprs
222
1
            .iter()
223
1
            .filter_map(extract_defun_name)
224
1
            .collect();
225
1
        assert!(defun_names.contains(&"COMPOSE"));
226
1
    }
227

            
228
    #[test]
229
1
    fn test_finance_has_money_functions() {
230
1
        let program = parse_sample("finance.nms");
231
1
        let defun_names: Vec<&str> = program
232
1
            .exprs
233
1
            .iter()
234
1
            .filter_map(extract_defun_name)
235
1
            .collect();
236

            
237
1
        assert!(defun_names.contains(&"MAKE-MONEY"));
238
1
        assert!(defun_names.contains(&"MONEY-AMOUNT"));
239
1
        assert!(defun_names.contains(&"MAKE-SPLIT"));
240
1
        assert!(defun_names.contains(&"MAKE-TRANSACTION"));
241
1
    }
242

            
243
    #[test]
244
1
    fn test_strings_has_multiline() {
245
1
        let content = read_sample("strings_and_io.nms");
246
1
        assert!(content.contains("\"\"\""));
247
1
    }
248

            
249
    #[test]
250
1
    fn test_let_forms_has_various_lets() {
251
1
        let content = read_sample("let_forms.nms");
252
1
        assert!(content.contains("(let "));
253
1
        assert!(content.contains("(let* "));
254
1
        assert!(content.contains("(letrec "));
255
1
    }
256
}
257

            
258
mod all_samples {
259
    use super::*;
260

            
261
    #[test]
262
1
    fn test_all_samples_parse_successfully() {
263
9
        for &sample in SAMPLE_FILES {
264
9
            let content = read_sample(sample);
265
9
            let result = Reader::parse(&content);
266
9
            assert!(
267
9
                result.is_ok(),
268
                "Failed to parse {}: {:?}",
269
                sample,
270
                result.err()
271
            );
272

            
273
9
            let program = result.unwrap();
274
9
            assert!(
275
9
                !program.exprs.is_empty(),
276
                "{sample} should have at least one expression"
277
            );
278
        }
279
1
    }
280

            
281
    #[test]
282
1
    fn test_total_expressions() {
283
1
        let total_exprs: usize = SAMPLE_FILES
284
1
            .iter()
285
9
            .map(|s| parse_sample(s).exprs.len())
286
1
            .sum();
287

            
288
1
        assert!(
289
1
            total_exprs > 50,
290
            "Should have substantial test coverage, got {total_exprs}"
291
        );
292
1
    }
293

            
294
    #[test]
295
1
    fn test_no_empty_samples() {
296
1
        let samples_path = samples_dir();
297
1
        let entries =
298
1
            fs::read_dir(&samples_path).unwrap_or_else(|e| panic!("Cannot read samples dir: {e}"));
299

            
300
9
        for entry in entries {
301
9
            let entry = entry.unwrap();
302
9
            let path = entry.path();
303

            
304
9
            if path.extension().is_some_and(|e| e == "nms") {
305
9
                let content = fs::read_to_string(&path).unwrap();
306
9
                assert!(
307
9
                    !content.trim().is_empty(),
308
                    "{} should not be empty",
309
                    path.display()
310
                );
311

            
312
9
                let program = Reader::parse(&content).unwrap();
313
9
                assert!(
314
9
                    !program.exprs.is_empty(),
315
                    "{} should parse to at least one expression",
316
                    path.display()
317
                );
318
            }
319
        }
320
1
    }
321

            
322
    #[test]
323
1
    fn test_all_samples_have_annotations() {
324
9
        for &sample in SAMPLE_FILES {
325
9
            let program = parse_sample(sample);
326
9
            let test_annotations: Vec<_> = program
327
9
                .annotations
328
9
                .iter()
329
26
                .filter(|a| a.name == "test")
330
9
                .collect();
331
9
            assert!(
332
9
                !test_annotations.is_empty(),
333
                "{sample} should have @test annotations"
334
            );
335
        }
336
1
    }
337
}
338

            
339
mod comments {
340
    use super::*;
341

            
342
    #[test]
343
1
    fn test_samples_with_comments_parse() {
344
1
        let content = read_sample("lists.nms");
345
1
        assert!(content.contains("; Uses an accumulator"));
346

            
347
1
        let program = Reader::parse(&content).unwrap();
348
1
        assert!(!program.exprs.is_empty());
349
1
    }
350

            
351
    #[test]
352
1
    fn test_inline_comments_ignored() {
353
1
        let content = read_sample("let_forms.nms");
354
1
        assert!(content.contains("; Solve ax^2"));
355

            
356
1
        let program = Reader::parse(&content).unwrap();
357
5
        for ann in &program.annotations {
358
5
            if ann.name == "test" {
359
5
                assert!(eval_test_annotation(ann, &program).unwrap());
360
            }
361
        }
362
1
    }
363
}
364

            
365
mod error_samples {
366
    use super::*;
367

            
368
    #[test]
369
1
    fn test_unclosed_paren_fails() {
370
1
        let bad_code = "(defun x 10";
371
1
        assert!(Reader::parse(bad_code).is_err());
372
1
    }
373

            
374
    #[test]
375
1
    fn test_unmatched_paren_fails() {
376
1
        let bad_code = "(defvar x 10))";
377
1
        assert!(Reader::parse(bad_code).is_err());
378
1
    }
379

            
380
    #[test]
381
1
    fn test_unclosed_string_fails() {
382
1
        let bad_code = r#"(defvar x "hello)"#;
383
1
        assert!(Reader::parse(bad_code).is_err());
384
1
    }
385

            
386
    #[test]
387
1
    fn test_empty_input_succeeds() {
388
1
        let empty = "";
389
1
        let result = Reader::parse(empty);
390
1
        assert!(result.is_ok());
391
1
        assert!(result.unwrap().exprs.is_empty());
392
1
    }
393

            
394
    #[test]
395
1
    fn test_only_comments_succeeds() {
396
1
        let only_comments = "; just comments\n; more comments";
397
1
        let result = Reader::parse(only_comments);
398
1
        assert!(result.is_ok());
399
1
        assert!(result.unwrap().exprs.is_empty());
400
1
    }
401
}