Skip to main content

unlab_gpu/
tester.rs

1//
2// Copyright (c) 2026 Ɓukasz Szpakowski
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7//
8//! A tester module.
9use std::env::current_dir;
10use std::env::set_current_dir;
11use std::ffi::OsString;
12use std::fs::create_dir_all;
13use std::io;
14use std::io::Cursor;
15use std::io::Write;
16use std::io::stdout;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::Ordering;
21use std::sync::Arc;
22use std::sync::RwLock;
23use crate::env::*;
24use crate::error::*;
25use crate::fs::*;
26use crate::interp::*;
27use crate::mod_node::*;
28use crate::parser::*;
29use crate::utils::*;
30use crate::value::*;
31
32/// A structure of test result.
33///
34/// The test result can be a success or a failure. A test error with a stack trace is in the
35/// result test if the test result is failure. Also, data from the standard output and the
36/// standard error stores in cursors which are in the test result.
37pub struct TestResult
38{
39    error_pair: Option<(Error, Vec<(Option<Value>, Pos)>)>,
40    stdout: Option<Arc<RwLock<Cursor<Vec<u8>>>>>,
41    stderr: Option<Arc<RwLock<Cursor<Vec<u8>>>>>,
42}
43
44impl TestResult
45{
46    /// Creates a test result.
47    pub fn new(error_pair: Option<(Error, Vec<(Option<Value>, Pos)>)>, stdout: Option<Arc<RwLock<Cursor<Vec<u8>>>>>, stderr: Option<Arc<RwLock<Cursor<Vec<u8>>>>>) -> TestResult
48    { TestResult { error_pair, stdout, stderr } }
49    
50    /// Returns `true` if the test result is success, otherwise `false`.
51    pub fn is_success(&self) -> bool
52    { self.error_pair.is_none() }
53    
54    /// Returns `true` if the test result is failure, otherwise `false`.
55    pub fn is_failure(&self) -> bool
56    { self.error_pair.is_some() }
57
58    /// Returns the test error with the stack trace if the test result is failure, otherwise
59    /// `None`.
60    pub fn error_pair(&self) -> Option<&(Error, Vec<(Option<Value>, Pos)>)>
61    {
62        match &self.error_pair {
63            Some(error_pair) => Some(error_pair),
64            None => None,
65        }
66    }
67    
68    /// Returns the cursor of standard output if the test result has the cursor of standard
69    /// output, otherwise `None`.
70    pub fn stdout(&self) -> Option<&Arc<RwLock<Cursor<Vec<u8>>>>>
71    {
72        match &self.stdout {
73            Some(stdout) => Some(stdout),
74            None => None,
75        }
76    }
77
78    /// Returns the cursor of standard error if the test result has the cursor of standard error,
79    /// otherwise `None`.
80    pub fn stderr(&self) -> Option<&Arc<RwLock<Cursor<Vec<u8>>>>>
81    {
82        match &self.stderr {
83            Some(stderr) => Some(stderr),
84            None => None,
85        }
86    }
87
88    /// Returns `true` if the test result has data from the standard output, otherwise `false`.
89    pub fn has_stdout_data(&self) -> Result<bool>
90    {
91        match &self.stdout {
92            Some(stdout) => {
93                let stdout_g = rw_lock_read(stdout)?;
94                Ok(!stdout_g.get_ref().is_empty())
95            },
96            None => Ok(false),
97        }
98    }
99
100    /// Returns `true` if the test result has data from the standard error, otherwise `false`.
101    pub fn has_stderr_data(&self) -> Result<bool>
102    {
103        match &self.stderr {
104            Some(stderr) => {
105                let stderr_g = rw_lock_read(stderr)?;
106                Ok(!stderr_g.get_ref().is_empty())
107            },
108            None => Ok(false),
109        }
110    }
111}
112
113/// A printer trait.
114///
115/// The printer prints messages for a test result.
116pub trait Print
117{
118    /// Prints the "Loading tests ..." message.
119    fn print_loading(&self, is_done: bool);
120
121    /// Prints the test running with the test identifier.
122    ///
123    /// This method prints "ok" for the test success or "FAILED" for the test failure if the test
124    /// is done, otherwise "FAILED".
125    fn print_running_test(&self, idents: &Vec<String>, ident: &String, is_done: bool, is_ok: bool);
126
127    /// Prints an empty line.
128    fn print_empty_line(&self);
129    
130    /// Prints the "Successes:" message.
131    fn print_successes(&self);
132
133    /// Prints the "Failures:" message.
134    fn print_failures(&self);
135    
136    /// Prints the test result with data from the standard output and the standard error.
137    fn print_test_result(&self, idents: &Vec<String>, ident: &String, test_result: &TestResult) -> Result<()>;
138    
139    /// Prints the number of passed tests and the number of failed tests. 
140    fn print_test_counts(&self, passed_test_count: usize, failed_test_count: usize);
141    
142    /// Prints the newline character for an occurred error.
143    fn print_lf_for_error(&self);
144}
145
146/// A structure of empty printer.
147///
148/// The empty printer is dummy that doesn't print any messages.
149#[derive(Copy, Clone, Debug)]
150pub struct EmptyPrinter;
151
152impl EmptyPrinter
153{
154    /// Creates an empty printer.
155    pub fn new() -> Self
156    { EmptyPrinter }
157}
158
159impl Print for EmptyPrinter
160{
161    fn print_loading(&self, _is_done: bool)
162    {}
163
164    fn print_running_test(&self, _idents: &Vec<String>, _ident: &String, _is_done: bool, _is_ok: bool)
165    {}
166
167    fn print_empty_line(&self)
168    {}
169    
170    fn print_successes(&self)
171    {}
172
173    fn print_failures(&self)
174    {}
175    
176    fn print_test_result(&self, _idents: &Vec<String>, _ident: &String, _test_result: &TestResult) -> Result<()>
177    { Ok(()) }
178    
179    fn print_test_counts(&self, _passed_test_count: usize, _failed_test_count: usize)
180    {}
181    
182    fn print_lf_for_error(&self)
183    {}
184}
185
186fn idents_and_ident_to_string(idents: &[String], ident: &String) -> String
187{
188    let mut s = String::new();
189    let mut is_first = true;
190    for ident2 in idents {
191        if !is_first {
192            s.push_str("::");
193        }
194        s.push_str(ident2.as_str());
195        is_first = false;
196    }
197    s.push_str("::");
198    s.push_str(ident.as_str());
199    s
200}
201
202/// A structure of standard printer.
203///
204/// The standard printer prints messages to the standard output.
205#[derive(Debug)]
206pub struct StdPrinter
207{
208    has_lf_for_error: AtomicBool,
209}
210
211impl StdPrinter
212{
213    /// Creates a standard printer.
214    pub fn new() -> Self
215    { StdPrinter { has_lf_for_error: AtomicBool::new(false), } }
216}
217
218impl Print for StdPrinter
219{
220    fn print_loading(&self, is_done: bool)
221    {
222        if is_done {
223            println!(" done");
224            self.has_lf_for_error.store(false, Ordering::SeqCst);
225        } else {
226            print!("Loading tests ...");
227            let _res = stdout().flush();
228            self.has_lf_for_error.store(true, Ordering::SeqCst);
229        }
230    }
231
232    fn print_running_test(&self, idents: &Vec<String>, ident: &String, is_done: bool, is_ok: bool)
233    {
234        if is_done {
235            if is_ok {
236                println!(" ok");
237            } else {
238                println!(" FAILED");
239            }
240            self.has_lf_for_error.store(false, Ordering::SeqCst);
241        } else {
242            print!("Test {} ...", idents_and_ident_to_string(idents, ident));
243            let _res = stdout().flush();
244            self.has_lf_for_error.store(true, Ordering::SeqCst);
245        }
246    }
247
248    fn print_empty_line(&self)
249    { println!(""); }
250    
251    fn print_successes(&self)
252    {
253        println!("Successes:");
254        println!("");
255    }
256
257    fn print_failures(&self)
258    {
259        println!("Failures:");
260        println!("");
261    }
262    
263    fn print_test_result(&self, idents: &Vec<String>, ident: &String, test_result: &TestResult) -> Result<()>
264    {
265        match &test_result.stdout {
266            Some(stdout) => {
267                let stdout_g = rw_lock_read(stdout)?;
268                if !stdout_g.get_ref().is_empty() {
269                    println!("---- {} stdout ----", idents_and_ident_to_string(idents, ident));
270                    let _res = io::stdout().write_all(stdout_g.get_ref().as_slice());
271                }
272            },
273            None => (),
274        }
275        match &test_result.stderr {
276            Some(stderr) => {
277                let stderr_g = rw_lock_read(stderr)?;
278                if !stderr_g.get_ref().is_empty() {
279                    println!("---- {} stderr ----", idents_and_ident_to_string(idents, ident));
280                    let _res = io::stdout().write_all(stderr_g.get_ref().as_slice());
281                }
282            },
283            None => (),
284        }
285        match &test_result.error_pair {
286            Some((err, stack_trace)) => {
287                println!("Test {} failed", idents_and_ident_to_string(idents, ident));
288                println!("{}", err);
289                for (fun_value, pos) in stack_trace {
290                    match fun_value {
291                        Some(fun_value) => println!("    at {} ({}: {}.{})", fun_value, pos.path, pos.line, pos.column),
292                        None => println!("    at {}: {}.{}", pos.path, pos.line, pos.column),
293                    }
294                }
295            },
296            None => (),
297        }
298        println!("");
299        Ok(())
300    }
301    
302    fn print_test_counts(&self, passed_test_count: usize, failed_test_count: usize)
303    {
304        if failed_test_count == 0 {
305            println!("Test result: ok. {} passed; {} failed", passed_test_count, failed_test_count);
306        } else {
307            println!("Test result: FAILED. {} passed; {} failed", passed_test_count, failed_test_count);
308        }
309    }
310    
311    fn print_lf_for_error(&self)
312    {
313        if self.has_lf_for_error.swap(false, Ordering::SeqCst) {
314            println!("");
315        }
316    }
317}
318
319fn create_and_change_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf>
320{
321    let saved_current_dir = current_dir()?;
322    create_dir_all(path.as_ref())?;
323    set_current_dir(path.as_ref())?;
324    Ok(saved_current_dir)
325}
326
327fn change_and_recusively_remove_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, saved_current_dir: Q) -> io::Result<()>
328{
329    set_current_dir(saved_current_dir)?;
330    recursively_remove(path, true)?;
331    Ok(())
332}
333
334/// A tester structure.
335///
336/// The tester tests a library or libraries by running the tests which were written by a
337/// programmer. The test results can be collected by the tester in order to print the test
338/// results for the programmer.
339pub struct Tester
340{
341    root_mod: Arc<RwLock<ModNode<Value, ()>>>,
342    shared_env: Arc<RwLock<SharedEnv>>,
343    stack_trace: Vec<(Option<Value>, Pos)>,
344    test_results: Vec<((Vec<String>, String), TestResult)>,
345    printer: Arc<dyn Print + Send + Sync>,
346    has_stdout_cursors: bool,
347    has_stderr_cursors: bool,
348}
349
350impl Tester
351{
352    /// Creates a tester.
353    ///
354    /// This method the root module, the library paths, the documentation library and the printer
355    /// that prints the messages. The flags of cursors determines whether data from the standard
356    /// output and the standard error are collect by the cursors.
357    pub fn new(root_mod: Arc<RwLock<ModNode<Value, ()>>>, lib_path: OsString, doc_path: OsString, printer: Arc<dyn Print + Send + Sync>, are_stdout_cursors: bool, are_stderr_cursors: bool) -> Self
358    {
359        Tester {
360            root_mod,
361            shared_env: Arc::new(RwLock::new(SharedEnv::new(lib_path, doc_path, Vec::new()))),
362            stack_trace: Vec::new(),
363            test_results: Vec::new(),
364            printer,
365            has_stdout_cursors: are_stdout_cursors,
366            has_stderr_cursors: are_stderr_cursors,
367        }
368    }
369
370    /// Returns the root module.
371    pub fn root_mod(&self) -> &Arc<RwLock<ModNode<Value, ()>>>
372    { &self.root_mod }
373    
374    /// Returns the shared environment.
375    pub fn shared_env(&self) -> &Arc<RwLock<SharedEnv>>
376    { &self.shared_env }
377
378    /// Returns the stack trace.
379    pub fn stack_trace(&self) -> &[(Option<Value>, Pos)]
380    { self.stack_trace.as_slice() }
381
382    /// Returns the test results.
383    pub fn test_results(&self) -> &[((Vec<String>, String), TestResult)]
384    { self.test_results.as_slice() }
385    
386    /// Returns the printer.
387    pub fn printer(&self) -> &Arc<dyn Print + Send + Sync>
388    { &self.printer }
389    
390    /// Returns the flag of cursor of standard output.
391    pub fn has_stdout_cursors(&self) -> bool
392    { self.has_stdout_cursors }
393
394    /// Returns the flag of cursor of standard error.
395    pub fn has_stderr_cursors(&self) -> bool
396    { self.has_stderr_cursors }
397    
398    /// Loads tests.
399    pub fn load(&mut self) -> Result<()>
400    {
401        self.printer.print_loading(false);
402        let test_paths = match paths_in_dir("tests", Some(2)) {
403            Ok(tmp_paths) => tmp_paths,
404            Err(err) => return Err(Error::Io(err)),
405        };
406        for test_path in test_paths {
407            let mut script_dir = PathBuf::from("tests");
408            script_dir.push(test_path.as_path());
409            let mut path = script_dir.clone();
410            path.push("tests.un");
411            let mut domain_path_buf = test_path.clone();
412            let domain = if domain_path_buf.components().count() >= 2 {
413                domain_path_buf.pop();
414                match domain_path_buf.to_str() {
415                    Some(tmp_domain) => Some(String::from(tmp_domain)),
416                    None => return Err(Error::Tester(String::from("test path component contains invalid UTF-8 character"))),
417                }
418            } else {
419                None
420            };
421            let tree = parse(path)?;
422            let mut env = Env::new_with_script_dir_and_domain_and_shared_env(self.root_mod.clone(), script_dir.clone(), domain, self.shared_env.clone());
423            let mut interp = Interp::new();
424            env.set_stdin(Input::Null);
425            env.set_stdout(Output::Null);
426            env.set_stderr(Output::Null);
427            match interp.interpret(&mut env, &tree) {
428                Ok(()) => (),
429                Err(err) => {
430                    self.stack_trace = interp.stack_trace().to_vec();
431                    return Err(err);
432                },
433            }
434        }
435        self.printer.print_loading(true);
436        Ok(())
437    }
438
439    /// Runs the specified test by the idenfiers of modules and the function identifer.
440    pub fn run_test(&mut self, idents: &Vec<String>, ident: &String) -> Result<()>
441    {
442        self.printer.print_running_test(idents, ident, false, false);
443        let is_test_suite = {
444            let shared_env_g = rw_lock_read(&self.shared_env)?;
445            shared_env_g.has_test_suite(idents)
446        };
447        let mut is_ok = false;
448        if is_test_suite {
449            match ModNode::mod_from(&self.root_mod, idents.as_slice(), false)? {
450                Some(mod1) => {
451                    let fun_value = {
452                        let mod_g = rw_lock_read(&mod1)?;
453                        match mod_g.var(ident) {
454                            Some(fun_value) => fun_value.clone(),
455                            None => return Err(Error::Tester(String::from("undefined test function"))),
456                        }
457                    };
458                    let mut work_test_dir = PathBuf::from("work");
459                    work_test_dir.push("test");
460                    let saved_current_dir = match create_and_change_dir(work_test_dir.as_path()) {
461                        Ok(tmp_saved_current_dir) => tmp_saved_current_dir,
462                        Err(err) => return Err(Error::Io(err)),
463                    };
464                    let mut env = Env::new_with_script_dir_and_domain_and_shared_env(self.root_mod.clone(), PathBuf::from("."), None, self.shared_env.clone());
465                    env.set_stdin(Input::Null);
466                    if self.has_stdout_cursors {
467                        env.set_stdout(Output::Cursor(Arc::new(RwLock::new(Cursor::new(Vec::new())))));
468                    }
469                    if self.has_stderr_cursors {
470                        env.set_stderr(Output::Cursor(Arc::new(RwLock::new(Cursor::new(Vec::new())))));
471                    }
472                    let mut interp = Interp::new();
473                    let error_pair = match fun_value.apply(&mut interp, &mut env, &[]) {
474                        Ok(_) => {
475                            is_ok = true;
476                            None
477                        },
478                        Err(err) => Some((err, interp.stack_trace().to_vec())),
479                    };
480                    let stdout = match env.stdout() {
481                        Output::Cursor(cursor) => Some(cursor.clone()),
482                        _ => None,
483                    };
484                    let stderr = match env.stderr() {
485                        Output::Cursor(cursor) => Some(cursor.clone()),
486                        _ => None,
487                    };
488                    self.test_results.push(((idents.clone(), ident.clone()), TestResult::new(error_pair, stdout, stderr)));
489                    match change_and_recusively_remove_dir(work_test_dir, saved_current_dir) {
490                        Ok(tmp_saved_current_dir) => tmp_saved_current_dir,
491                        Err(err) => return Err(Error::Io(err)),
492                    }
493                },
494                None => return Err(Error::Tester(String::from("undefined test module"))),
495            }
496        } else {
497            return Err(Error::Tester(String::from("module isn't test suite")));
498        }
499        self.printer.print_running_test(idents, ident, true, is_ok);
500        Ok(())
501    }
502
503    /// Runs the tests in the specified test suite by the identifiers of modules.
504    pub fn run_tests_in_test_suite(&mut self, idents: &Vec<String>) -> Result<()>
505    {
506        let is_test_suite = {
507            let shared_env_g = rw_lock_read(&self.shared_env)?;
508            shared_env_g.has_test_suite(idents)
509        };
510        if is_test_suite {
511            match ModNode::mod_from(&self.root_mod, idents.as_slice(), false)? {
512                Some(mod1) => {
513                    let mut fun_idents: Vec<String> = {
514                        let mod_g = rw_lock_read(&mod1)?;
515                        mod_g.vars().keys().map(|id| id.clone()).collect()
516                    };
517                    fun_idents.sort();
518                    for fun_ident in &fun_idents {
519                        self.run_test(idents, fun_ident)?;
520                    }
521                },
522                None => return Err(Error::Tester(String::from("undefined test module"))),
523            }
524        } else {
525            return Err(Error::Tester(String::from("module isn't test suite")));
526        }
527        Ok(())
528    }
529
530    /// Runs all tests.
531    pub fn run_all_tests(&mut self) -> Result<()>
532    {
533        let mut test_suites: Vec<Vec<String>> = {
534            let shared_env_g = rw_lock_read(&self.shared_env)?;
535            shared_env_g.test_suites().iter().map(|ids| ids.clone()).collect()
536        };
537        test_suites.sort();
538        for test_suite in &test_suites {
539            self.run_tests_in_test_suite(test_suite)?;
540        }
541        Ok(())
542    }
543    
544    /// Prints an empty line.
545    pub fn print_empty_line(&self)
546    { self.printer.print_empty_line() }
547
548    /// Prints the test successes.
549    pub fn print_successes(&self) -> Result<()>
550    {
551        let mut count = 0usize;
552        for (_, test_result) in &self.test_results {
553            if test_result.is_success() && (test_result.has_stdout_data()? || test_result.has_stderr_data()?) {
554                count += 1;
555            }
556        }
557        if count > 0 {
558            self.printer.print_successes();
559            for ((idents, ident), test_result) in &self.test_results {
560                if test_result.is_success() && (test_result.has_stdout_data()? || test_result.has_stderr_data()?) {
561                    self.printer.print_test_result(idents, ident, test_result)?;
562                }
563            }
564        }
565        Ok(())
566    }
567
568    /// Prints the test failures.
569    pub fn print_failures(&self) -> Result<()>
570    {
571        let mut count = 0usize;
572        for (_, test_result) in &self.test_results {
573            if !test_result.is_success() {
574                count += 1;
575            }
576        }
577        if count > 0 {
578            self.printer.print_failures();
579            for ((idents, ident), test_result) in &self.test_results {
580                if !test_result.is_success() {
581                    self.printer.print_test_result(idents, ident, test_result)?;
582                }
583            }
584        }
585        Ok(())
586    }
587    
588    /// Prints the number of passed tests and the number of failed tests. 
589    pub fn print_test_counts(&self)
590    {
591        let mut passed_test_count = 0usize;
592        let mut failed_test_count = 0usize;
593        for (_, test_result) in &self.test_results {
594            if test_result.is_success() {
595                passed_test_count += 1;
596            } else {
597                failed_test_count += 1;
598            }
599        }
600        self.printer.print_test_counts(passed_test_count, failed_test_count);
601    }
602}
603
604#[cfg(test)]
605mod tests;