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