1use std::{
2 fs::{self, DirEntry},
3 io::Write,
4 panic::{self, RefUnwindSafe, UnwindSafe},
5 path::{Path, PathBuf},
6 sync::{Arc, Mutex},
7};
8
9use serde::de::DeserializeOwned;
10use simple_error::SimpleError;
11use tempfile::TempDir;
12
13use crate::{
14 helpers::*,
15 tester::TestResult::{Failure, ParseError, ReadError, Success},
16};
17
18#[derive(Debug, Clone)]
21pub struct TestEnv {
22 current_dir: String,
24}
25
26impl TestEnv {
27 pub fn new(current_dir: &str) -> Option<Self> {
28 fs::create_dir_all(current_dir).ok().map(|_| TestEnv {
29 current_dir: current_dir.to_string(),
30 })
31 }
32
33 pub fn push(&self, child: &str) -> Option<Self> {
34 let mut path = PathBuf::from(&self.current_dir);
35 path.push(child);
36 path.to_str().and_then(TestEnv::new)
37 }
38
39 pub fn current_dir(&self) -> &str {
40 &self.current_dir
41 }
42
43 pub fn clear_log(&self) -> Option<()> {
44 fs::remove_file(self.full_path("log")).ok()
45 }
46
47 pub fn logln(&self, msg: &str) -> Option<()> {
48 println!("{msg}");
49 fs::OpenOptions::new()
50 .create(true)
51 .append(true)
52 .open(self.full_path("log"))
53 .ok()
54 .and_then(|mut file| writeln!(file, "{msg}").ok())
55 }
56
57 pub fn logln_to(&self, msg: &str, rel_path: impl AsRef<Path>) -> Option<()> {
58 println!("{msg}");
59 fs::OpenOptions::new()
60 .create(true)
61 .append(true)
62 .open(self.full_path(rel_path))
63 .ok()
64 .and_then(|mut file| writeln!(file, "{msg}").ok())
65 }
66
67 pub fn read_file(&self, rel_path: impl AsRef<Path>) -> Option<String> {
69 fs::read_to_string(self.full_path(rel_path)).ok()
70 }
71
72 pub fn write_file(&self, rel_path: impl AsRef<Path>, contents: &str) -> Option<()> {
74 fs::write(self.full_path(rel_path), contents).ok()
75 }
76
77 pub fn parse_file<T: DeserializeOwned>(&self, rel_path: impl AsRef<Path>) -> Option<T> {
79 self.read_file(rel_path)
80 .and_then(|input| serde_json::from_str(&input).ok())
81 }
82
83 pub fn copy_file_from(&self, path: impl AsRef<Path>) -> Option<()> {
86 let path = path.as_ref();
87 let new_name = path.file_name()?.to_str()?;
88 self.copy_file_from_as(path, new_name)
89 }
90
91 pub fn copy_file_from_as(&self, path: impl AsRef<Path>, new_name: &str) -> Option<()> {
95 let path = path.as_ref();
96 if !path.is_file() {
97 return None;
98 }
99 fs::copy(path, self.full_path(new_name)).ok().map(|_| ())
100 }
101
102 pub fn copy_file_from_env(&self, other: &TestEnv, path: impl AsRef<Path>) -> Option<()> {
105 self.copy_file_from(other.full_path(path))
106 }
107
108 pub fn copy_file_from_env_as(
112 &self,
113 other: &TestEnv,
114 path: impl AsRef<Path>,
115 new_name: &str,
116 ) -> Option<()> {
117 self.copy_file_from_as(other.full_path(path), new_name)
118 }
119
120 pub fn remove_file(&self, rel_path: impl AsRef<Path>) -> Option<()> {
122 fs::remove_file(self.full_path(rel_path)).ok()
123 }
124
125 pub fn full_path(&self, rel_path: impl AsRef<Path>) -> PathBuf {
128 PathBuf::from(&self.current_dir).join(rel_path)
129 }
130
131 pub fn rel_path(&self, full_path: impl AsRef<Path>) -> Option<String> {
134 match PathBuf::from(full_path.as_ref()).strip_prefix(&self.current_dir) {
135 Err(_) => None,
136 Ok(rel_path) => rel_path.to_str().map(|rp| rp.to_string()),
137 }
138 }
139
140 pub fn full_canonical_path(&self, rel_path: impl AsRef<Path>) -> Option<String> {
143 let full_path = PathBuf::from(&self.current_dir).join(rel_path);
144 full_path
145 .canonicalize()
146 .ok()
147 .and_then(|p| p.to_str().map(|x| x.to_string()))
148 }
149}
150
151#[derive(Debug, Clone)]
152pub enum TestResult {
153 ReadError,
154 ParseError(SimpleError),
155 Success,
156 Failure { message: String, location: String },
157}
158
159type TestFn = Box<dyn Fn(&str, &str) -> TestResult>;
162
163type BatchFn = Box<dyn Fn(&str, &str) -> Option<Vec<(String, String)>>>;
167
168pub struct Test {
169 pub name: String,
171 pub test: TestFn,
173}
174
175pub struct Tester {
194 name: String,
195 root_dir: String,
196 tests: Vec<Test>,
197 batches: Vec<BatchFn>,
198 results: std::collections::BTreeMap<String, Vec<(String, TestResult)>>,
199}
200
201impl TestResult {
202 pub fn is_success(&self) -> bool {
203 matches!(self, TestResult::Success)
204 }
205
206 pub fn is_failure(&self) -> bool {
207 matches!(self, TestResult::Failure { .. })
208 }
209
210 pub fn is_readerror(&self) -> bool {
211 matches!(self, TestResult::ReadError)
212 }
213
214 pub fn is_parseerror(&self) -> bool {
215 matches!(self, TestResult::ParseError(_))
216 }
217}
218
219impl Tester {
220 pub fn new(name: &str, root_dir: &str) -> Tester {
221 Tester {
222 name: name.to_string(),
223 root_dir: root_dir.to_string(),
224 tests: vec![],
225 batches: vec![],
226 results: Default::default(),
227 }
228 }
229
230 pub fn env(&self) -> Option<TestEnv> {
231 TestEnv::new(&self.root_dir)
232 }
233
234 pub fn output_env(&self) -> Option<TestEnv> {
235 let output_dir = self.root_dir.clone() + "/_" + &self.name;
236 TestEnv::new(&output_dir)
237 }
238
239 fn capture_test<F>(test: F) -> TestResult
240 where
241 F: FnOnce() + UnwindSafe,
242 {
243 let test_result = Arc::new(Mutex::new(ParseError(SimpleError::new("no error"))));
244 let old_hook = panic::take_hook();
245 panic::set_hook({
246 let result = test_result.clone();
247 Box::new(move |info| {
248 let mut result = result.lock().unwrap();
249 let message = match info.payload().downcast_ref::<&'static str>() {
250 Some(s) => s.to_string(),
251 None => match info.payload().downcast_ref::<String>() {
252 Some(s) => s.clone(),
253 None => "Unknown error".to_string(),
254 },
255 };
256 let location = match info.location() {
257 Some(l) => l.to_string(),
258 None => "".to_string(),
259 };
260 *result = Failure { message, location };
261 })
262 });
263 let result = panic::catch_unwind(test);
264 panic::set_hook(old_hook);
265 match result {
266 Ok(_) => Success,
267 Err(_) => (*test_result.lock().unwrap()).clone(),
268 }
269 }
270
271 pub fn add_test<T, F>(&mut self, name: &str, test: F)
272 where
273 T: 'static + DeserializeOwned + UnwindSafe,
274 F: Fn(T) + UnwindSafe + RefUnwindSafe + 'static,
275 {
276 let test_fn = move |_path: &str, input: &str| match parse_as::<T>(input) {
277 Ok(test_case) => Tester::capture_test(|| {
278 test(test_case);
279 }),
280 Err(e) => ParseError(e),
281 };
282 self.tests.push(Test {
283 name: name.to_string(),
284 test: Box::new(test_fn),
285 });
286 }
287
288 pub fn add_test_with_env<T, F>(&mut self, name: &str, test: F)
289 where
290 T: 'static + DeserializeOwned + UnwindSafe,
291 F: Fn(T, &TestEnv, &TestEnv, &TestEnv) + UnwindSafe + RefUnwindSafe + 'static,
292 {
293 let test_env = self.env().unwrap();
294 let output_env = self.output_env().unwrap();
295 let test_fn = move |path: &str, input: &str| match parse_as::<T>(input) {
296 Ok(test_case) => Tester::capture_test(|| {
297 let dir = TempDir::new().unwrap();
299 let env = TestEnv::new(dir.path().to_str().unwrap()).unwrap();
300 let output_dir = output_env.full_path(path);
301 let output_env = TestEnv::new(output_dir.to_str().unwrap()).unwrap();
302 test(test_case, &env, &test_env, &output_env);
303 fs::remove_dir_all(env.current_dir()).unwrap();
304 }),
305 Err(e) => ParseError(e),
306 };
307 self.tests.push(Test {
308 name: name.to_string(),
309 test: Box::new(test_fn),
310 });
311 }
312
313 pub fn add_test_batch<T, F>(&mut self, batch: F)
314 where
315 T: 'static + DeserializeOwned,
316 F: Fn(T) -> Vec<(String, String)> + 'static,
317 {
318 let batch_fn = move |_path: &str, input: &str| match parse_as::<T>(input) {
319 Ok(test_batch) => Some(batch(test_batch)),
320 Err(_) => None,
321 };
322 self.batches.push(Box::new(batch_fn));
323 }
324
325 fn results_for(&mut self, name: &str) -> &mut Vec<(String, TestResult)> {
326 self.results.entry(name.to_string()).or_default()
327 }
328
329 fn add_result(&mut self, name: &str, path: &str, result: TestResult) {
330 self.results_for(name).push((path.to_string(), result));
331 }
332
333 fn read_error(&mut self, path: &str) {
334 self.results_for("")
335 .push((path.to_string(), TestResult::ReadError))
336 }
337
338 fn parse_error(&mut self, path: &str) {
339 self.results_for("").push((
340 path.to_string(),
341 TestResult::ParseError(SimpleError::new("no error")),
342 ))
343 }
344
345 pub fn successful_tests(&self, test: &str) -> Vec<String> {
346 let mut tests = Vec::new();
347 if let Some(results) = self.results.get(test) {
348 for (path, res) in results {
349 if let Success = res {
350 tests.push(path.clone())
351 }
352 }
353 }
354 tests
355 }
356
357 pub fn failed_tests(&self, test: &str) -> Vec<(String, String, String)> {
358 let mut tests = Vec::new();
359 if let Some(results) = self.results.get(test) {
360 for (path, res) in results {
361 if let Failure { message, location } = res {
362 tests.push((path.clone(), message.clone(), location.clone()))
363 }
364 }
365 }
366 tests
367 }
368
369 pub fn unreadable_tests(&self) -> Vec<String> {
370 let mut tests = Vec::new();
371 if let Some(results) = self.results.get("") {
372 for (path, res) in results {
373 if let ReadError = res {
374 tests.push(path.clone())
375 }
376 }
377 }
378 tests
379 }
380
381 pub fn unparseable_tests(&self) -> Vec<String> {
382 let mut tests = Vec::new();
383 if let Some(results) = self.results.get("") {
384 for (path, res) in results {
385 if let ParseError(_) = res {
386 tests.push(path.clone())
387 }
388 }
389 }
390 tests
391 }
392
393 fn run_for_input(&mut self, path: &str, input: &str) {
394 let mut results = Vec::new();
395 for Test { name, test } in &self.tests {
396 match test(path, input) {
397 TestResult::ParseError(_) => {
398 continue;
399 },
400 res => results.push((name.to_string(), path, res)),
401 }
402 }
403 if !results.is_empty() {
404 for (name, path, res) in results {
405 self.add_result(&name, path, res)
406 }
407 } else {
408 let mut res_tests = Vec::new();
410 for batch in &self.batches {
411 match batch(path, input) {
412 None => continue,
413 Some(tests) => {
414 for (name, input) in tests {
415 let test_path = path.to_string() + "/" + &name;
416 res_tests.push((test_path, input));
417 }
418 },
419 }
420 }
421 if !res_tests.is_empty() {
422 for (path, input) in res_tests {
423 self.run_for_input(&path, &input);
424 }
425 } else {
426 self.parse_error(path);
428 }
429 }
430 }
431
432 pub fn run_for_file(&mut self, path: &str) {
433 match self.env().unwrap().read_file(path) {
434 None => self.read_error(path),
435 Some(input) => self.run_for_input(path, &input),
436 }
437 }
438
439 pub fn run_foreach_in_dir(&mut self, dir: &str) {
440 let full_dir = PathBuf::from(&self.root_dir).join(dir);
441 let starts_with_underscore = |entry: &DirEntry| {
442 if let Some(last) = entry.path().iter().next_back() {
443 if let Some(last) = last.to_str() {
444 if last.starts_with('_') {
445 return true;
446 }
447 }
448 }
449 false
450 };
451 match full_dir.to_str() {
452 None => self.read_error(dir),
453 Some(full_dir) => match fs::read_dir(full_dir) {
454 Err(_) => self.read_error(full_dir),
455 Ok(paths) => {
456 paths.flatten().for_each(|entry| {
457 if starts_with_underscore(&entry) {
459 return;
460 }
461 if let Ok(kind) = entry.file_type() {
462 let path = format!("{}", entry.path().display());
463 let rel_path = self.env().unwrap().rel_path(path).unwrap();
464 if kind.is_file() || kind.is_symlink() {
465 if rel_path.ends_with(".json") {
466 self.run_for_file(&rel_path);
467 }
468 } else if kind.is_dir() {
469 self.run_foreach_in_dir(&rel_path);
470 }
471 }
472 });
473 },
474 },
475 }
476 }
477
478 pub fn finalize(&mut self) {
479 let env = self.output_env().unwrap();
480 env.write_file("report", "");
481 let print = |msg: &str| {
482 env.logln_to(msg, "report");
483 };
484 let mut do_panic = false;
485
486 print(&format!(
487 "\n====== Report for '{}' tester run ======",
488 &self.name
489 ));
490 for name in self.results.keys() {
491 if name.is_empty() {
492 continue;
493 }
494 print(&format!("\nResults for '{name}'"));
495 let tests = self.successful_tests(name);
496 if !tests.is_empty() {
497 print(" Successful tests: ");
498 for path in tests {
499 print(&format!(" {path}"));
500 if let Some(logs) = env.read_file(path + "/log") {
501 print(&logs)
502 }
503 }
504 }
505 let tests = self.failed_tests(name);
506 if !tests.is_empty() {
507 do_panic = true;
508 print(" Failed tests: ");
509 for (path, message, location) in tests {
510 print(&format!(" {path}, '{message}', {location}"));
511 if let Some(logs) = env.read_file(path + "/log") {
512 print(&logs)
513 }
514 }
515 }
516 }
517 let tests = self.unreadable_tests();
518 if !tests.is_empty() {
519 do_panic = true;
520 print("\nUnreadable tests: ");
521 for path in tests {
522 print(&format!(" {path}"))
523 }
524 }
525 let tests = self.unparseable_tests();
526 if !tests.is_empty() {
527 do_panic = true;
528 print("\nUnparseable tests: ");
529 for path in tests {
530 print(&format!(" {path}"))
531 }
532 }
533 print(&format!(
534 "\n====== End of report for '{}' tester run ======\n",
535 &self.name
536 ));
537 if do_panic {
538 panic!("Some tests failed or could not be read/parsed");
539 }
540 }
541}