1#![allow(clippy::disallowed_types, clippy::print_stdout)]
4
5use std::{
6 collections::HashMap,
7 fs, iter,
8 path::{Path, PathBuf},
9 time::SystemTime,
10};
11
12use anyhow::Result;
13use itertools::Itertools as _;
14
15use crate::{
16 codegen::{CommentBlock, ensure_file_contents, reformat},
17 project_root,
18 util::list_rust_files,
19};
20
21pub(crate) fn generate(check: bool) {
22 let parser_crate_root = project_root().join("crates/parser");
23 let parser_test_data = parser_crate_root.join("test_data");
24 let parser_test_data_inline = parser_test_data.join("parser/inline");
25
26 let tests = tests_from_dir(&parser_crate_root.join("src/grammar"));
27
28 let mut some_file_was_updated = false;
29 some_file_was_updated |=
30 install_tests(&tests.ok, parser_test_data_inline.join("ok"), check).unwrap();
31 some_file_was_updated |=
32 install_tests(&tests.err, parser_test_data_inline.join("err"), check).unwrap();
33
34 if some_file_was_updated {
35 let _ = fs::File::open(parser_crate_root.join("src/tests.rs"))
36 .unwrap()
37 .set_modified(SystemTime::now());
38 }
39
40 let ok_tests = tests.ok.values().sorted_by(|a, b| a.name.cmp(&b.name)).map(|test| {
41 let test_name = quote::format_ident!("{}", test.name);
42 let test_file = format!("test_data/parser/inline/ok/{test_name}.rs");
43 let (test_func, args) = match &test.edition {
44 Some(edition) => {
45 let edition = quote::format_ident!("Edition{edition}");
46 (
47 quote::format_ident!("run_and_expect_no_errors_with_edition"),
48 quote::quote! {#test_file, crate::Edition::#edition},
49 )
50 }
51 None => (quote::format_ident!("run_and_expect_no_errors"), quote::quote! {#test_file}),
52 };
53 quote::quote! {
54 #[test]
55 fn #test_name() {
56 #test_func(#args);
57 }
58 }
59 });
60 let err_tests = tests.err.values().sorted_by(|a, b| a.name.cmp(&b.name)).map(|test| {
61 let test_name = quote::format_ident!("{}", test.name);
62 let test_file = format!("test_data/parser/inline/err/{test_name}.rs");
63 let (test_func, args) = match &test.edition {
64 Some(edition) => {
65 let edition = quote::format_ident!("Edition{edition}");
66 (
67 quote::format_ident!("run_and_expect_errors_with_edition"),
68 quote::quote! {#test_file, crate::Edition::#edition},
69 )
70 }
71 None => (quote::format_ident!("run_and_expect_errors"), quote::quote! {#test_file}),
72 };
73 quote::quote! {
74 #[test]
75 fn #test_name() {
76 #test_func(#args);
77 }
78 }
79 });
80
81 let output = quote::quote! {
82 mod ok {
83 use crate::tests::*;
84 #(#ok_tests)*
85 }
86 mod err {
87 use crate::tests::*;
88 #(#err_tests)*
89 }
90 };
91
92 let pretty = reformat(output.to_string());
93 ensure_file_contents(
94 crate::flags::CodegenType::ParserTests,
95 parser_test_data.join("generated/runner.rs").as_ref(),
96 &pretty,
97 check,
98 );
99}
100
101fn install_tests(tests: &HashMap<String, Test>, tests_dir: PathBuf, check: bool) -> Result<bool> {
102 if !tests_dir.is_dir() {
103 fs::create_dir_all(&tests_dir)?;
104 }
105 let existing = existing_tests(&tests_dir, TestKind::Ok)?;
106 if let Some((t, (path, _))) = existing.iter().find(|&(t, _)| !tests.contains_key(t)) {
107 panic!("Test `{t}` is deleted: {}", path.display());
108 }
109
110 let mut some_file_was_updated = false;
111
112 for (name, test) in tests {
113 let path = match existing.get(name) {
114 Some((path, _test)) => path.clone(),
115 None => tests_dir.join(name).with_extension("rs"),
116 };
117 if ensure_file_contents(crate::flags::CodegenType::ParserTests, &path, &test.text, check) {
118 some_file_was_updated = true;
119 }
120 }
121
122 Ok(some_file_was_updated)
123}
124
125#[derive(Debug)]
126struct Test {
127 name: String,
128 text: String,
129 kind: TestKind,
130 edition: Option<String>,
131}
132
133#[derive(Copy, Clone, Debug)]
134enum TestKind {
135 Ok,
136 Err,
137}
138
139#[derive(Default, Debug)]
140struct Tests {
141 ok: HashMap<String, Test>,
142 err: HashMap<String, Test>,
143}
144
145fn collect_tests(s: &str) -> Vec<Test> {
146 let mut res = Vec::new();
147 for comment_block in CommentBlock::extract_untagged(s) {
148 let first_line = &comment_block.contents[0];
149 let (name, kind) = if let Some(name) = first_line.strip_prefix("test ") {
150 (name.to_owned(), TestKind::Ok)
151 } else if let Some(name) = first_line.strip_prefix("test_err ") {
152 (name.to_owned(), TestKind::Err)
153 } else {
154 continue;
155 };
156 let (name, edition) = match *name.split(' ').collect_vec().as_slice() {
157 [name, edition] => {
158 assert!(!edition.contains(' '));
159 (name.to_owned(), Some(edition.to_owned()))
160 }
161 [name] => (name.to_owned(), None),
162 _ => panic!("invalid test name: {name:?}"),
163 };
164 let text: String = edition
165 .as_ref()
166 .map(|edition| format!("// {edition}"))
167 .into_iter()
168 .chain(comment_block.contents[1..].iter().cloned())
169 .chain(iter::once(String::new()))
170 .collect::<Vec<_>>()
171 .join("\n");
172 assert!(!text.trim().is_empty() && text.ends_with('\n'));
173 res.push(Test { name, edition, text, kind })
174 }
175 res
176}
177
178fn tests_from_dir(dir: &Path) -> Tests {
179 let mut res = Tests::default();
180 for entry in list_rust_files(dir) {
181 process_file(&mut res, entry.as_path());
182 }
183 let grammar_rs = dir.parent().unwrap().join("grammar.rs");
184 process_file(&mut res, &grammar_rs);
185 return res;
186
187 fn process_file(res: &mut Tests, path: &Path) {
188 let text = fs::read_to_string(path).unwrap();
189
190 for test in collect_tests(&text) {
191 if let TestKind::Ok = test.kind {
192 if let Some(old_test) = res.ok.insert(test.name.clone(), test) {
193 panic!("Duplicate test: {}", old_test.name);
194 }
195 } else if let Some(old_test) = res.err.insert(test.name.clone(), test) {
196 panic!("Duplicate test: {}", old_test.name);
197 }
198 }
199 }
200}
201
202fn existing_tests(dir: &Path, ok: TestKind) -> Result<HashMap<String, (PathBuf, Test)>> {
203 let mut res = HashMap::new();
204 for file in fs::read_dir(dir)? {
205 let path = file?.path();
206 let rust_file = path.extension().and_then(|ext| ext.to_str()) == Some("rs");
207
208 if rust_file {
209 let name = path.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap();
210 let text = fs::read_to_string(&path)?;
211 let edition =
212 text.lines().next().and_then(|it| it.strip_prefix("// ")).map(ToOwned::to_owned);
213 let test = Test { name: name.clone(), text, kind: ok, edition };
214 if let Some(old) = res.insert(name, (path, test)) {
215 println!("Duplicate test: {old:?}");
216 }
217 }
218 }
219 Ok(res)
220}
221
222#[test]
223fn test() {
224 generate(true);
225}