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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
89
34
            Ok(lhs_val == rhs_val)
90
        }
91
        _ => Err(format!("unknown operator: {op}")),
92
    }
93
34
}
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
    "tag-metro-splits.nms",
106
];
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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