1use crate::{create_file, source_from_path};
8use oak_core::{Builder, Language, errors::OakError};
9
10#[cfg(feature = "serde")]
11use crate::json_from_path;
12#[cfg(feature = "serde")]
13use serde::Serialize;
14
15#[cfg(feature = "serde")]
16use serde_json::Value as JsonValue;
17
18use std::{
19 fmt::Debug,
20 path::{Path, PathBuf},
21 time::Duration,
22};
23use walkdir::WalkDir;
24
25pub struct BuilderTester {
31 root: PathBuf,
32 extensions: Vec<String>,
33 timeout: Duration,
34}
35
36#[derive(Debug, Clone, PartialEq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct BuilderTestExpected {
43 pub success: bool,
45 pub typed_root: Option<TypedRootData>,
47 pub errors: Vec<String>,
49}
50
51#[derive(Debug, Clone, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct TypedRootData {
59 pub type_name: String,
61 #[cfg(feature = "serde")]
63 pub content: JsonValue,
64 #[cfg(not(feature = "serde"))]
65 pub content: (),
66}
67
68impl BuilderTester {
69 pub fn new<P: AsRef<Path>>(root: P) -> Self {
71 Self { root: root.as_ref().to_path_buf(), extensions: vec![], timeout: Duration::from_secs(10) }
72 }
73
74 pub fn with_extension(mut self, extension: impl ToString) -> Self {
76 self.extensions.push(extension.to_string());
77 self
78 }
79
80 pub fn with_timeout(mut self, timeout: Duration) -> Self {
82 self.timeout = timeout;
83 self
84 }
85
86 #[cfg(feature = "serde")]
88 pub fn run_tests<L, B>(self, builder: &B) -> Result<(), OakError>
89 where
90 B: Builder<L> + Send + Sync,
91 L: Language + Send + Sync,
92 L::TypedRoot: serde::Serialize + Debug + Sync + Send,
93 {
94 let test_files = self.find_test_files()?;
95 let force_regenerated = std::env::var("REGENERATE_TESTS").unwrap_or("0".to_string()) == "1";
96 let mut regenerated_any = false;
97
98 for file_path in test_files {
99 println!("Testing file: {}", file_path.display());
100 regenerated_any |= self.test_single_file::<L, B>(&file_path, builder, force_regenerated)?
101 }
102
103 if regenerated_any && force_regenerated {
104 println!("Tests regenerated for: {}", self.root.display());
105 Ok(())
106 }
107 else {
108 Ok(())
109 }
110 }
111
112 #[cfg(not(feature = "serde"))]
114 pub fn run_tests<L, B>(self, _builder: &B) -> Result<(), OakError>
115 where
116 B: Builder<L> + Send + Sync,
117 L: Language + Send + Sync,
118 L::TypedRoot: Debug + Sync + Send,
119 {
120 Ok(())
121 }
122
123 fn find_test_files(&self) -> Result<Vec<PathBuf>, OakError> {
124 let mut files = Vec::new();
125
126 for entry in WalkDir::new(&self.root) {
127 let entry = entry.unwrap();
128 let path = entry.path();
129
130 if path.is_file() {
131 if let Some(ext) = path.extension() {
132 let ext_str = ext.to_str().unwrap_or("");
133 if self.extensions.iter().any(|e| e == ext_str) {
134 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
136 let is_output_file = file_name.ends_with(".parsed.json") || file_name.ends_with(".lexed.json") || file_name.ends_with(".built.json") || file_name.ends_with(".expected.json");
137
138 if !is_output_file {
139 files.push(path.to_path_buf());
140 }
141 }
142 }
143 }
144 }
145
146 Ok(files)
147 }
148
149 #[cfg(feature = "serde")]
150 fn test_single_file<L, B>(&self, file_path: &Path, builder: &B, force_regenerated: bool) -> Result<bool, OakError>
151 where
152 B: Builder<L> + Send + Sync,
153 L: Language + Send + Sync,
154 L::TypedRoot: serde::Serialize + Debug + Sync + Send,
155 {
156 let source = source_from_path(file_path)?;
157
158 use std::sync::{Arc, Mutex};
160 let result: Arc<Mutex<Option<Result<BuilderTestExpected, OakError>>>> = Arc::new(Mutex::new(None));
161 let result_clone = Arc::clone(&result);
162 let timeout = self.timeout;
163 let file_path_string = file_path.display().to_string();
164
165 std::thread::scope(|s| {
166 let handle = s.spawn(move || {
167 let mut cache = oak_core::parser::ParseSession::<L>::new(1024);
168 let build_out = builder.build(&source, &[], &mut cache);
169
170 let (success, typed_root) = match &build_out.result {
172 Ok(root) => {
173 match serde_json::to_value(root) {
175 Ok(content) => {
176 let typed_root_data = TypedRootData { type_name: std::any::type_name::<L::TypedRoot>().to_string(), content };
177 (true, Some(typed_root_data))
178 }
179 Err(_) => {
180 (true, None)
182 }
183 }
184 }
185 Err(_) => (false, None),
186 };
187
188 let mut error_messages: Vec<String> = build_out.diagnostics.iter().map(|e| e.to_string()).collect();
190 if let Err(e) = &build_out.result {
191 error_messages.push(e.to_string());
192 }
193
194 let test_result = BuilderTestExpected { success, typed_root, errors: error_messages };
195
196 let mut result = result_clone.lock().unwrap();
197 *result = Some(Ok(test_result));
198 });
199
200 let start_time = std::time::Instant::now();
202 let timeout_occurred = loop {
203 if handle.is_finished() {
205 break false;
206 }
207
208 if start_time.elapsed() > timeout {
210 break true;
211 }
212
213 std::thread::sleep(std::time::Duration::from_millis(10));
215 };
216
217 if timeout_occurred {
219 return Err(OakError::custom_error(format!("Builder test timed out after {:?} for file: {}", timeout, file_path_string)));
220 }
221
222 let test_result = {
224 let result_guard = result.lock().unwrap();
225 match result_guard.as_ref() {
226 Some(Ok(test_result)) => test_result.clone(),
227 Some(Err(e)) => return Err(e.clone()),
228 None => return Err(OakError::custom_error("Builder thread disconnected unexpectedly")),
229 }
230 };
231
232 let mut regenerated = false;
233 let expected_file = file_path.with_extension(format!("{}.built.json", file_path.extension().unwrap_or_default().to_str().unwrap_or("")));
234
235 if !expected_file.exists() {
237 let legacy_file = file_path.with_extension("expected.json");
238 if legacy_file.exists() {
239 let _ = std::fs::rename(&legacy_file, &expected_file);
240 }
241 }
242
243 if expected_file.exists() && !force_regenerated {
244 let expected_json = json_from_path(&expected_file)?;
245 let expected: BuilderTestExpected = serde_json::from_value(expected_json).map_err(|e| OakError::custom_error(e.to_string()))?;
246 if test_result != expected {
247 return Err(OakError::test_failure(file_path.to_path_buf(), format!("{:#?}", expected), format!("{:#?}", test_result)));
248 }
249 }
250 else {
251 use std::io::Write;
252 let mut file = create_file(&expected_file)?;
253 let mut buf = Vec::new();
254 let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
256 test_result.serialize(&mut ser).map_err(|e| OakError::custom_error(e.to_string()))?;
257 file.write_all(&buf).map_err(|e| OakError::custom_error(e.to_string()))?;
258 regenerated = true;
259 }
260
261 Ok(regenerated)
262 })
263 }
264}