diff options
Diffstat (limited to 'xtask/src/codegen/gen_parser_tests.rs')
-rw-r--r-- | xtask/src/codegen/gen_parser_tests.rs | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/xtask/src/codegen/gen_parser_tests.rs b/xtask/src/codegen/gen_parser_tests.rs new file mode 100644 index 000000000..0f550d948 --- /dev/null +++ b/xtask/src/codegen/gen_parser_tests.rs | |||
@@ -0,0 +1,155 @@ | |||
1 | //! This module greps parser's code for specially formatted comments and turnes | ||
2 | //! them into tests. | ||
3 | |||
4 | use std::{ | ||
5 | collections::HashMap, | ||
6 | fs, | ||
7 | path::{Path, PathBuf}, | ||
8 | }; | ||
9 | |||
10 | use crate::{ | ||
11 | codegen::{self, update, Mode}, | ||
12 | project_root, Result, | ||
13 | }; | ||
14 | |||
15 | pub fn generate_parser_tests(mode: Mode) -> Result<()> { | ||
16 | let tests = tests_from_dir(&project_root().join(Path::new(codegen::GRAMMAR_DIR)))?; | ||
17 | fn install_tests(tests: &HashMap<String, Test>, into: &str, mode: Mode) -> Result<()> { | ||
18 | let tests_dir = project_root().join(into); | ||
19 | if !tests_dir.is_dir() { | ||
20 | fs::create_dir_all(&tests_dir)?; | ||
21 | } | ||
22 | // ok is never actually read, but it needs to be specified to create a Test in existing_tests | ||
23 | let existing = existing_tests(&tests_dir, true)?; | ||
24 | for t in existing.keys().filter(|&t| !tests.contains_key(t)) { | ||
25 | panic!("Test is deleted: {}", t); | ||
26 | } | ||
27 | |||
28 | let mut new_idx = existing.len() + 1; | ||
29 | for (name, test) in tests { | ||
30 | let path = match existing.get(name) { | ||
31 | Some((path, _test)) => path.clone(), | ||
32 | None => { | ||
33 | let file_name = format!("{:04}_{}.rs", new_idx, name); | ||
34 | new_idx += 1; | ||
35 | tests_dir.join(file_name) | ||
36 | } | ||
37 | }; | ||
38 | update(&path, &test.text, mode)?; | ||
39 | } | ||
40 | Ok(()) | ||
41 | } | ||
42 | install_tests(&tests.ok, codegen::OK_INLINE_TESTS_DIR, mode)?; | ||
43 | install_tests(&tests.err, codegen::ERR_INLINE_TESTS_DIR, mode) | ||
44 | } | ||
45 | |||
46 | #[derive(Debug)] | ||
47 | struct Test { | ||
48 | pub name: String, | ||
49 | pub text: String, | ||
50 | pub ok: bool, | ||
51 | } | ||
52 | |||
53 | #[derive(Default, Debug)] | ||
54 | struct Tests { | ||
55 | pub ok: HashMap<String, Test>, | ||
56 | pub err: HashMap<String, Test>, | ||
57 | } | ||
58 | |||
59 | fn collect_tests(s: &str) -> Vec<(usize, Test)> { | ||
60 | let mut res = vec![]; | ||
61 | let prefix = "// "; | ||
62 | let lines = s.lines().map(str::trim_start).enumerate(); | ||
63 | |||
64 | let mut block = vec![]; | ||
65 | for (line_idx, line) in lines { | ||
66 | let is_comment = line.starts_with(prefix); | ||
67 | if is_comment { | ||
68 | block.push((line_idx, &line[prefix.len()..])); | ||
69 | } else { | ||
70 | process_block(&mut res, &block); | ||
71 | block.clear(); | ||
72 | } | ||
73 | } | ||
74 | process_block(&mut res, &block); | ||
75 | return res; | ||
76 | |||
77 | fn process_block(acc: &mut Vec<(usize, Test)>, block: &[(usize, &str)]) { | ||
78 | if block.is_empty() { | ||
79 | return; | ||
80 | } | ||
81 | let mut ok = true; | ||
82 | let mut block = block.iter(); | ||
83 | let (start_line, name) = loop { | ||
84 | match block.next() { | ||
85 | Some(&(idx, line)) if line.starts_with("test ") => { | ||
86 | break (idx, line["test ".len()..].to_string()); | ||
87 | } | ||
88 | Some(&(idx, line)) if line.starts_with("test_err ") => { | ||
89 | ok = false; | ||
90 | break (idx, line["test_err ".len()..].to_string()); | ||
91 | } | ||
92 | Some(_) => (), | ||
93 | None => return, | ||
94 | } | ||
95 | }; | ||
96 | let text: String = | ||
97 | block.map(|(_, line)| *line).chain(std::iter::once("")).collect::<Vec<_>>().join("\n"); | ||
98 | assert!(!text.trim().is_empty() && text.ends_with('\n')); | ||
99 | acc.push((start_line, Test { name, text, ok })) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | fn tests_from_dir(dir: &Path) -> Result<Tests> { | ||
104 | let mut res = Tests::default(); | ||
105 | for entry in ::walkdir::WalkDir::new(dir) { | ||
106 | let entry = entry.unwrap(); | ||
107 | if !entry.file_type().is_file() { | ||
108 | continue; | ||
109 | } | ||
110 | if entry.path().extension().unwrap_or_default() != "rs" { | ||
111 | continue; | ||
112 | } | ||
113 | process_file(&mut res, entry.path())?; | ||
114 | } | ||
115 | let grammar_rs = dir.parent().unwrap().join("grammar.rs"); | ||
116 | process_file(&mut res, &grammar_rs)?; | ||
117 | return Ok(res); | ||
118 | fn process_file(res: &mut Tests, path: &Path) -> Result<()> { | ||
119 | let text = fs::read_to_string(path)?; | ||
120 | |||
121 | for (_, test) in collect_tests(&text) { | ||
122 | if test.ok { | ||
123 | if let Some(old_test) = res.ok.insert(test.name.clone(), test) { | ||
124 | Err(format!("Duplicate test: {}", old_test.name))? | ||
125 | } | ||
126 | } else { | ||
127 | if let Some(old_test) = res.err.insert(test.name.clone(), test) { | ||
128 | Err(format!("Duplicate test: {}", old_test.name))? | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | Ok(()) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | fn existing_tests(dir: &Path, ok: bool) -> Result<HashMap<String, (PathBuf, Test)>> { | ||
137 | let mut res = HashMap::new(); | ||
138 | for file in fs::read_dir(dir)? { | ||
139 | let file = file?; | ||
140 | let path = file.path(); | ||
141 | if path.extension().unwrap_or_default() != "rs" { | ||
142 | continue; | ||
143 | } | ||
144 | let name = { | ||
145 | let file_name = path.file_name().unwrap().to_str().unwrap(); | ||
146 | file_name[5..file_name.len() - 3].to_string() | ||
147 | }; | ||
148 | let text = fs::read_to_string(&path)?; | ||
149 | let test = Test { name: name.clone(), text, ok }; | ||
150 | if let Some(old) = res.insert(name, (path, test)) { | ||
151 | println!("Duplicate test: {:?}", old); | ||
152 | } | ||
153 | } | ||
154 | Ok(res) | ||
155 | } | ||