1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#[cfg(test)]
mod tests {
use crate::analyzer::Analyzer;
use crate::lexer::Lexer;
use crate::parser::Parser;
use std::fs;
use std::path::{Path, PathBuf};
fn compile_to_error(vox_path: &Path) -> Result<(), String> {
let source_name = vox_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown_case");
let source = fs::read_to_string(vox_path)
.unwrap_or_else(|e| panic!("failed to read {}: {}", vox_path.display(), e));
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize();
// The case is displayed under its bare file name - the `.err`
// fixtures pin that - but its `see` paths resolve against the
// directory it actually lives in, the same as any other Vox source.
let mut parser = Parser::new(tokens)
.with_source(source_name, &source)
.with_include_base(vox_path.parent().unwrap_or(Path::new(".")));
let mut program = match parser.parse() {
Ok(p) => p,
Err(err) => return Err(err.to_string()),
};
// A `.shared` sidecar marks a case that must be analyzed as a
// `--shared` compile (top-level executable statements are rejected
// only in that mode). The marker is a zero-byte file named
// `<case>.shared` next to the `.vox`.
let shared = vox_path.with_extension("shared").exists();
let mut analyzer = Analyzer::new()
.with_source(source_name, &source)
.with_shared_mode(shared);
analyzer.analyze(&mut program);
if analyzer.errors.is_empty() {
Ok(())
} else {
let joined = analyzer
.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n");
Err(joined)
}
}
fn collect_cases(root: &Path) -> Vec<PathBuf> {
let mut cases = Vec::new();
let entries = fs::read_dir(root).expect("compile_fail directory should exist");
for entry in entries {
let entry = entry.expect("directory entry should be readable");
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("vox") {
cases.push(path);
}
}
cases.sort();
cases
}
#[test]
fn compile_fail_corpus_reports_errors() {
let root = Path::new("tests/compile_fail");
let cases = collect_cases(root);
assert!(
!cases.is_empty(),
"compile_fail corpus is empty; add at least one failing case"
);
for vox_path in cases {
let case_name = vox_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown_case");
let err_path = vox_path.with_extension("err");
let expected = fs::read_to_string(&err_path)
.unwrap_or_else(|e| panic!("missing .err for {}: {}", case_name, e));
// Each non-blank line is an independently required substring of
// the rendered error, not one substring spanning the whole file.
// A single-line `.err` behaves exactly as before; this only adds
// power for multi-line fixtures, letting a `.err` pin the
// message *and* a `help:` line (which the plan 270 §S1.5
// diagnostic puts on its own line, separated by an ANSI-coded
// gutter that would otherwise break a single-substring match).
let expected_lines: Vec<&str> = expected
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect();
assert!(
!expected_lines.is_empty(),
".err for {} must contain an expected error substring",
case_name
);
match compile_to_error(&vox_path) {
Ok(()) => panic!("{} unexpectedly compiled successfully", case_name),
Err(actual) => {
for line in &expected_lines {
assert!(
actual.contains(line),
"{} failed with unexpected error.\nExpected substring: {:?}\nActual:\n{}",
case_name,
line,
actual
);
}
}
}
}
}
}