oak_core/helpers/
building.rs1use crate::{
8 Builder, Language,
9 errors::OakError,
10 helpers::{create_file, json_from_path, source_from_path},
11};
12use serde::{Deserialize, Serialize};
13use serde_json::{Serializer, ser::PrettyFormatter};
14use std::{
15 fmt::Debug,
16 path::{Path, PathBuf},
17 time::Duration,
18};
19use walkdir::WalkDir;
20
21pub struct BuilderTester {
27 root: PathBuf,
28 extensions: Vec<String>,
29 timeout: Duration,
30}
31
32#[derive(Debug, Serialize, Deserialize, PartialEq)]
37pub struct BuilderTestExpected {
38 success: bool,
39 typed_root: Option<TypedRootData>,
40 errors: Vec<String>,
41}
42
43#[derive(Debug, Serialize, Deserialize, PartialEq)]
49pub struct TypedRootData {
50 type_name: String,
51 content: serde_json::Value,
52}
53
54impl BuilderTester {
55 pub fn new<P: AsRef<Path>>(root: P) -> Self {
57 Self { root: root.as_ref().to_path_buf(), extensions: vec![], timeout: Duration::from_secs(10) }
58 }
59
60 pub fn with_extension(mut self, extension: impl ToString) -> Self {
62 self.extensions.push(extension.to_string());
63 self
64 }
65
66 pub fn with_timeout(mut self, timeout: Duration) -> Self {
76 self.timeout = timeout;
77 self
78 }
79
80 pub fn run_tests<L, B>(self, builder: &B) -> Result<(), OakError>
95 where
96 B: Builder<L> + Send + Sync,
97 L: Language + Send + Sync + 'static,
98 L::TypedRoot: Serialize + Debug + Sync + Send,
99 {
100 let test_files = self.find_test_files()?;
101 let force_regenerated = std::env::var("REGENERATE_TESTS").unwrap_or("0".to_string()) == "1";
102 let mut regenerated_any = false;
103
104 for file_path in test_files {
105 println!("Testing file: {}", file_path.display());
106 regenerated_any |= self.test_single_file::<L, B>(&file_path, builder, force_regenerated)?;
107 }
108
109 if regenerated_any && force_regenerated { Err(OakError::test_regenerated(self.root)) } else { Ok(()) }
110 }
111
112 fn find_test_files(&self) -> Result<Vec<PathBuf>, OakError> {
113 let mut files = Vec::new();
114
115 for entry in WalkDir::new(&self.root) {
116 let entry = entry.unwrap();
117 let path = entry.path();
118
119 if path.is_file() {
120 if let Some(ext) = path.extension() {
121 let ext_str = ext.to_str().unwrap_or("");
122 if self.extensions.iter().any(|e| e == ext_str) {
123 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
125 let is_output_file = file_name.ends_with(".parsed.json") || file_name.ends_with(".lexed.json") || file_name.ends_with(".built.json");
126
127 if !is_output_file {
128 files.push(path.to_path_buf());
129 }
130 }
131 }
132 }
133 }
134
135 Ok(files)
136 }
137
138 fn test_single_file<L, B>(&self, file_path: &Path, builder: &B, force_regenerated: bool) -> Result<bool, OakError>
139 where
140 B: Builder<L> + Send + Sync,
141 L: Language + Send + Sync + 'static,
142 L::TypedRoot: Serialize + Debug + Sync + Send,
143 {
144 let source = source_from_path(file_path)?;
145
146 use std::sync::mpsc;
148 let (tx, rx) = mpsc::channel();
149 let timeout = self.timeout;
150 let file_path_string = file_path.display().to_string();
151
152 std::thread::scope(|s| {
153 s.spawn(move || {
154 let mut cache = crate::parser::session::ParseSession::<L>::new(1024);
155 let build_out = builder.build(&source, &[], &mut cache);
156
157 let (success, typed_root) = match &build_out.result {
159 Ok(root) => {
160 match serde_json::to_value(root) {
162 Ok(content) => {
163 let typed_root_data = TypedRootData { type_name: std::any::type_name::<L::TypedRoot>().to_string(), content };
164 (true, Some(typed_root_data))
165 }
166 Err(_) => {
167 (true, None)
169 }
170 }
171 }
172 Err(_) => (false, None),
173 };
174
175 let mut error_messages: Vec<String> = build_out.diagnostics.iter().map(|e| e.to_string()).collect();
177 if let Err(e) = &build_out.result {
178 error_messages.push(e.to_string());
179 }
180
181 let test_result = BuilderTestExpected { success, typed_root, errors: error_messages };
182
183 let _ = tx.send(Ok::<BuilderTestExpected, OakError>(test_result));
184 });
185
186 let mut regenerated = false;
187 match rx.recv_timeout(timeout) {
188 Ok(Ok(test_result)) => {
189 let expected_file = file_path.with_extension(format!("{}.built.json", file_path.extension().unwrap_or_default().to_str().unwrap_or("")));
190
191 if expected_file.exists() && !force_regenerated {
192 let expected: BuilderTestExpected = json_from_path(&expected_file)?;
193
194 if test_result != expected {
195 println!("Test failed for file: {}", file_path.display());
196 println!("Expected: {:#?}", expected);
197 println!("Actual: {:#?}", test_result);
198 return Err(OakError::custom_error("Test results do not match expected results"));
199 }
200 }
201 else {
202 let file = create_file(&expected_file)?;
203 let mut writer = Serializer::with_formatter(file, PrettyFormatter::with_indent(b" "));
204 test_result.serialize(&mut writer)?;
205
206 println!("Created expected result file: {}\nNeed rerun", expected_file.display());
207 if force_regenerated {
208 regenerated = true;
209 }
210 else {
211 return Err(OakError::custom_error("Test expected result file was missing or regenerated"));
212 }
213 }
214 }
215 Ok(Err(e)) => return Err(e),
216 Err(mpsc::RecvTimeoutError::Timeout) => {
217 return Err(OakError::custom_error(format!("Builder test timed out after {:?} for file: {}", timeout, file_path_string)));
218 }
219 Err(mpsc::RecvTimeoutError::Disconnected) => {
220 return Err(OakError::custom_error("Builder thread disconnected unexpectedly"));
221 }
222 }
223 Ok(regenerated)
224 })
225 }
226}