1use crate::{create_file, json_from_path, source_from_path};
8use oak_core::{Builder, Language, errors::OakError};
9use serde::{Deserialize, Serialize};
10use serde_json::Value as JsonValue;
11
12use std::{
13 fmt::Debug,
14 path::{Path, PathBuf},
15 time::Duration,
16};
17use walkdir::WalkDir;
18
19pub struct BuilderTester {
25 root: PathBuf,
26 extensions: Vec<String>,
27 timeout: Duration,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub struct BuilderTestExpected {
36 pub success: bool,
38 pub typed_root: Option<TypedRootData>,
40 pub errors: Vec<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct TypedRootData {
51 pub type_name: String,
53 pub content: JsonValue,
55}
56
57impl BuilderTester {
58 pub fn new<P: AsRef<Path>>(root: P) -> Self {
60 Self { root: root.as_ref().to_path_buf(), extensions: vec![], timeout: Duration::from_secs(10) }
61 }
62
63 pub fn with_extension(mut self, extension: impl ToString) -> Self {
65 self.extensions.push(extension.to_string());
66 self
67 }
68
69 pub fn with_timeout(mut self, timeout: Duration) -> Self {
71 self.timeout = timeout;
72 self
73 }
74
75 pub fn run_tests<L, B>(self, builder: &B) -> Result<(), OakError>
77 where
78 B: Builder<L> + Send + Sync,
79 L: Language + Send + Sync,
80 L::TypedRoot: Serialize + Debug + Sync + Send,
81 {
82 let test_files = self.find_test_files()?;
83 let force_regenerated = std::env::var("REGENERATE_TESTS").unwrap_or("0".to_string()) == "1";
84 let mut regenerated_any = false;
85
86 for file_path in test_files {
87 println!("Testing file: {}", file_path.display());
88 regenerated_any |= self.test_single_file::<L, B>(&file_path, builder, force_regenerated)?
89 }
90
91 if regenerated_any && force_regenerated {
92 println!("Tests regenerated for: {}", self.root.display());
93 Ok(())
94 }
95 else {
96 Ok(())
97 }
98 }
99
100 fn find_test_files(&self) -> Result<Vec<PathBuf>, OakError> {
101 let mut files = Vec::new();
102
103 for entry in WalkDir::new(&self.root) {
104 let entry = entry.unwrap();
105 let path = entry.path();
106
107 if path.is_file() {
108 if let Some(ext) = path.extension() {
109 let ext_str = ext.to_str().unwrap_or("");
110 if self.extensions.iter().any(|e| e == ext_str) {
111 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
113 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");
114
115 if !is_output_file {
116 files.push(path.to_path_buf());
117 }
118 }
119 }
120 }
121 }
122
123 Ok(files)
124 }
125
126 fn test_single_file<L, B>(&self, file_path: &Path, builder: &B, force_regenerated: bool) -> Result<bool, OakError>
127 where
128 B: Builder<L> + Send + Sync,
129 L: Language + Send + Sync,
130 L::TypedRoot: Serialize + Debug + Sync + Send,
131 {
132 let source = source_from_path(file_path)?;
133
134 use std::sync::mpsc;
136 let (tx, rx) = mpsc::channel();
137 let timeout = self.timeout;
138 let file_path_string = file_path.display().to_string();
139
140 std::thread::scope(|s| {
141 s.spawn(move || {
142 let mut cache = oak_core::parser::ParseSession::<L>::new(1024);
143 let build_out = builder.build(&source, &[], &mut cache);
144
145 let (success, typed_root) = match &build_out.result {
147 Ok(root) => {
148 match serde_json::to_value(root) {
150 Ok(content) => {
151 let typed_root_data = TypedRootData { type_name: std::any::type_name::<L::TypedRoot>().to_string(), content };
152 (true, Some(typed_root_data))
153 }
154 Err(_) => {
155 (true, None)
157 }
158 }
159 }
160 Err(_) => (false, None),
161 };
162
163 let mut error_messages: Vec<String> = build_out.diagnostics.iter().map(|e| e.to_string()).collect();
165 if let Err(e) = &build_out.result {
166 error_messages.push(e.to_string());
167 }
168
169 let test_result = BuilderTestExpected { success, typed_root, errors: error_messages };
170
171 let _ = tx.send(Ok::<BuilderTestExpected, OakError>(test_result));
172 });
173
174 let mut regenerated = false;
175 match rx.recv_timeout(timeout) {
176 Ok(Ok(test_result)) => {
177 let expected_file = file_path.with_extension(format!("{}.built.json", file_path.extension().unwrap_or_default().to_str().unwrap_or("")));
178
179 if !expected_file.exists() {
181 let legacy_file = file_path.with_extension("expected.json");
182 if legacy_file.exists() {
183 let _ = std::fs::rename(&legacy_file, &expected_file);
184 }
185 }
186
187 if expected_file.exists() && !force_regenerated {
188 let expected_json = json_from_path(&expected_file)?;
189 let expected: BuilderTestExpected = serde_json::from_value(expected_json).map_err(|e| OakError::custom_error(e.to_string()))?;
190 if test_result != expected {
191 return Err(OakError::test_failure(file_path.to_path_buf(), format!("{:#?}", expected), format!("{:#?}", test_result)));
192 }
193 }
194 else {
195 use std::io::Write;
196 let mut file = create_file(&expected_file)?;
197 let json_val = serde_json::to_string_pretty(&test_result).map_err(|e| OakError::custom_error(e.to_string()))?;
198 file.write_all(json_val.as_bytes()).map_err(|e| OakError::custom_error(e.to_string()))?;
199 regenerated = true;
200 }
201 }
202 Ok(Err(e)) => return Err(e),
203 Err(mpsc::RecvTimeoutError::Timeout) => return Err(OakError::custom_error(format!("Builder test timed out after {:?} for file: {}", timeout, file_path_string))),
204 Err(mpsc::RecvTimeoutError::Disconnected) => return Err(OakError::custom_error("Builder thread disconnected unexpectedly")),
205 }
206 Ok(regenerated)
207 })
208 }
209}