1 //! This module greps parser's code for specially formatted comments and turns
2 //! them into tests.
3
4 use std::{
5 collections::HashMap,
6 fs, iter,
7 path::{Path, PathBuf},
8 };
9
10 #[test]
sourcegen_parser_tests()11 fn sourcegen_parser_tests() {
12 let grammar_dir = sourcegen::project_root().join(Path::new("crates/parser/src/grammar"));
13 let tests = tests_from_dir(&grammar_dir);
14
15 install_tests(&tests.ok, "crates/parser/test_data/parser/inline/ok");
16 install_tests(&tests.err, "crates/parser/test_data/parser/inline/err");
17
18 fn install_tests(tests: &HashMap<String, Test>, into: &str) {
19 let tests_dir = sourcegen::project_root().join(into);
20 if !tests_dir.is_dir() {
21 fs::create_dir_all(&tests_dir).unwrap();
22 }
23 // ok is never actually read, but it needs to be specified to create a Test in existing_tests
24 let existing = existing_tests(&tests_dir, true);
25 for t in existing.keys().filter(|&t| !tests.contains_key(t)) {
26 panic!("Test is deleted: {t}");
27 }
28
29 let mut new_idx = existing.len() + 1;
30 for (name, test) in tests {
31 let path = match existing.get(name) {
32 Some((path, _test)) => path.clone(),
33 None => {
34 let file_name = format!("{new_idx:04}_{name}.rs");
35 new_idx += 1;
36 tests_dir.join(file_name)
37 }
38 };
39 sourcegen::ensure_file_contents(&path, &test.text);
40 }
41 }
42 }
43
44 #[derive(Debug)]
45 struct Test {
46 name: String,
47 text: String,
48 ok: bool,
49 }
50
51 #[derive(Default, Debug)]
52 struct Tests {
53 ok: HashMap<String, Test>,
54 err: HashMap<String, Test>,
55 }
56
collect_tests(s: &str) -> Vec<Test>57 fn collect_tests(s: &str) -> Vec<Test> {
58 let mut res = Vec::new();
59 for comment_block in sourcegen::CommentBlock::extract_untagged(s) {
60 let first_line = &comment_block.contents[0];
61 let (name, ok) = if let Some(name) = first_line.strip_prefix("test ") {
62 (name.to_string(), true)
63 } else if let Some(name) = first_line.strip_prefix("test_err ") {
64 (name.to_string(), false)
65 } else {
66 continue;
67 };
68 let text: String = comment_block.contents[1..]
69 .iter()
70 .cloned()
71 .chain(iter::once(String::new()))
72 .collect::<Vec<_>>()
73 .join("\n");
74 assert!(!text.trim().is_empty() && text.ends_with('\n'));
75 res.push(Test { name, text, ok })
76 }
77 res
78 }
79
tests_from_dir(dir: &Path) -> Tests80 fn tests_from_dir(dir: &Path) -> Tests {
81 let mut res = Tests::default();
82 for entry in sourcegen::list_rust_files(dir) {
83 process_file(&mut res, entry.as_path());
84 }
85 let grammar_rs = dir.parent().unwrap().join("grammar.rs");
86 process_file(&mut res, &grammar_rs);
87 return res;
88
89 fn process_file(res: &mut Tests, path: &Path) {
90 let text = fs::read_to_string(path).unwrap();
91
92 for test in collect_tests(&text) {
93 if test.ok {
94 if let Some(old_test) = res.ok.insert(test.name.clone(), test) {
95 panic!("Duplicate test: {}", old_test.name);
96 }
97 } else if let Some(old_test) = res.err.insert(test.name.clone(), test) {
98 panic!("Duplicate test: {}", old_test.name);
99 }
100 }
101 }
102 }
103
existing_tests(dir: &Path, ok: bool) -> HashMap<String, (PathBuf, Test)>104 fn existing_tests(dir: &Path, ok: bool) -> HashMap<String, (PathBuf, Test)> {
105 let mut res = HashMap::default();
106 for file in fs::read_dir(dir).unwrap() {
107 let file = file.unwrap();
108 let path = file.path();
109 if path.extension().unwrap_or_default() != "rs" {
110 continue;
111 }
112 let name = {
113 let file_name = path.file_name().unwrap().to_str().unwrap();
114 file_name[5..file_name.len() - 3].to_string()
115 };
116 let text = fs::read_to_string(&path).unwrap();
117 let test = Test { name: name.clone(), text, ok };
118 if let Some(old) = res.insert(name, (path, test)) {
119 println!("Duplicate test: {old:?}");
120 }
121 }
122 res
123 }
124