molt_ng/
test_harness.rs

1//! Molt Test Harness
2//!
3//! A Molt test script is a Molt script containing tests of Molt code.  Each
4//! test is a call of the Molt `test` command provided by the
5//! `molt::test_harness` module.  The tests are executed in the context of the
6//! the application's `molt::Interp` (and so can test application-specific commands).
7//!
8//! The test harness keeps track of the number of tests executed, and whether they
9//! passed, failed, or returned an unexpected error.
10//!
11//! The `molt-app` tool provides access to the test harness for a standard Molt
12//! interpreter:
13//!
14//! ```bash
15//! $ molt test test/all.tcl
16//! Molt 0.1.0 -- Test Harness
17//!
18//! 171 tests, 171 passed, 0 failed, 0 errors
19//! ```
20//!
21//! If a test fails or returns an error, the test harness outputs the details.
22//!
23//! See the Molt Book (or the Molt test suite) for examples of test scripts.
24
25use crate::check_args;
26use crate::molt_ok;
27use crate::types::ContextID;
28use crate::Interp;
29use crate::MoltResult;
30use crate::ResultCode;
31use crate::Value;
32use std::env;
33use std::fs;
34use std::path::PathBuf;
35
36/// Executes the Molt test harness, given the command-line arguments,
37/// in the context of the given interpreter.
38///
39///
40/// The first element of the `args` array must be the name of the test script
41/// to execute.  The remaining elements are meant to be test harness options,
42/// but are currently ignored.
43///
44/// See [`molt::interp`](../molt/interp/index.html) for details on how to configure and
45/// add commands to a Molt interpreter.
46///
47/// # Example
48///
49/// ```
50/// use molt::Interp;
51/// use std::env;
52///
53/// // FIRST, get the command line arguments.
54/// let args: Vec<String> = env::args().collect();
55///
56/// // NEXT, create and initialize the interpreter.
57/// let mut interp = Interp::new();
58///
59/// // NOTE: commands can be added to the interpreter here.
60///
61/// // NEXT, evaluate the file, if any.
62/// if args.len() > 1 {
63///     molt::test_harness(&mut interp, &args[1..]);
64/// } else {
65///     eprintln!("Usage: mytest *filename.tcl");
66/// }
67/// ```
68
69pub fn test_harness(interp: &mut Interp, args: &[String]) -> Result<(), ()> {
70    // FIRST, announce who we are.
71    println!("Molt {} -- Test Harness", env!("CARGO_PKG_VERSION"));
72
73    // NEXT, get the script file name
74    if args.is_empty() {
75        eprintln!("missing test script");
76        return Err(());
77    }
78
79    let path = PathBuf::from(&args[0]);
80
81    // NEXT, initialize the test result.
82    let context_id = interp.save_context(TestContext::new());
83
84    // NEXT, install the test commands into the interpreter.
85    interp.add_context_command("test", test_cmd, context_id);
86
87    // NEXT, execute the script.
88    match fs::read_to_string(&args[0]) {
89        Ok(script) => {
90            if let Some(parent) = path.parent() {
91                let _ = env::set_current_dir(parent);
92            }
93
94            if let Err(exception) = interp.eval(&script) {
95                if exception.code() == ResultCode::Error {
96                    eprintln!("{}", exception.value());
97                    return Err(());
98                } else {
99                    eprintln!("Unexpected eval return: {:?}", exception);
100                    return Err(());
101                }
102            }
103        }
104        Err(e) => {
105            println!("{}", e);
106            return Err(());
107        }
108    }
109
110    // NEXT, output the test results:
111    let ctx = interp.context::<TestContext>(context_id);
112    println!(
113        "\n{} tests, {} passed, {} failed, {} errors",
114        ctx.num_tests, ctx.num_passed, ctx.num_failed, ctx.num_errors
115    );
116
117    if ctx.num_failed + ctx.num_errors == 0 {
118        Ok(())
119    } else {
120        Err(())
121    }
122}
123
124struct TestContext {
125    num_tests: usize,
126    num_passed: usize,
127    num_failed: usize,
128    num_errors: usize,
129}
130
131impl TestContext {
132    fn new() -> Self {
133        Self {
134            num_tests: 0,
135            num_passed: 0,
136            num_failed: 0,
137            num_errors: 0,
138        }
139    }
140}
141
142#[derive(Eq, PartialEq, Debug)]
143enum Code {
144    Ok,
145    Error,
146}
147
148impl std::fmt::Display for Code {
149    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150        match self {
151            Code::Ok => write!(f, "-ok"),
152            Code::Error => write!(f, "-error"),
153        }
154    }
155}
156
157#[derive(Debug)]
158struct TestInfo {
159    name: String,
160    description: String,
161    setup: String,
162    body: String,
163    cleanup: String,
164    code: Code,
165    expect: String,
166}
167
168impl TestInfo {
169    fn new(name: &str, description: &str) -> Self {
170        Self {
171            name: name.into(),
172            description: description.into(),
173            setup: String::new(),
174            body: String::new(),
175            cleanup: String::new(),
176            code: Code::Ok,
177            expect: String::new(),
178        }
179    }
180
181    fn print_failure(&self, got_code: &str, received: &str) {
182        println!("\n*** FAILED {} {}", self.name, self.description);
183        println!("Expected {} <{}>", self.code.to_string(), self.expect);
184        println!("Received {} <{}>", got_code, received);
185    }
186
187    fn print_error(&self, result: &MoltResult) {
188        println!("\n*** ERROR {} {}", self.name, self.description);
189        println!("Expected {} <{}>", self.code.to_string(), self.expect);
190
191        match result {
192            Ok(val) => println!("Received -ok <{}>", val),
193            Err(exception) => match exception.code() {
194                ResultCode::Error => println!("Received -error <{}>", exception.value()),
195                ResultCode::Return => println!("Received -return <{}>", exception.value()),
196                ResultCode::Break => println!("Received -break <>"),
197                ResultCode::Continue => println!("Received -continue <>"),
198                _ => unimplemented!(),
199            },
200        }
201    }
202
203    fn print_helper_error(&self, part: &str, msg: &str) {
204        println!(
205            "\n*** ERROR (in {}) {} {}",
206            part, self.name, self.description
207        );
208        println!("    {}", msg);
209    }
210}
211
212/// # test *name* *script* -ok|-error *result*
213///
214/// Executes the script expecting either a successful response or an error.
215///
216/// Note: This is an extremely minimal replacement for tcltest; at some
217/// point I'll need something much more robust.
218///
219/// Note: See the Molt Book for the full syntax.
220fn test_cmd(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
221    // FIRST, check the minimum command line.
222    check_args(1, argv, 4, 0, "name description args...")?;
223
224    // NEXT, see which kind of command it is.
225    let arg = argv[3].as_str();
226    if arg.starts_with('-') {
227        fancy_test(interp, context_id, argv)
228    } else {
229        simple_test(interp, context_id, argv)
230    }
231}
232
233// The simple version of the test command.
234fn simple_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
235    check_args(1, argv, 6, 6, "name description script -ok|-error result")?;
236
237    // FIRST, get the test info
238    let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
239    info.body = argv[3].to_string();
240    info.expect = argv[5].to_string();
241
242    let code = argv[4].as_str();
243
244    info.code = if code == "-ok" {
245        Code::Ok
246    } else if code == "-error" {
247        Code::Error
248    } else {
249        incr_errors(interp, context_id);
250        info.print_helper_error("test command", &format!("invalid option: \"{}\"", code));
251
252        return molt_ok!();
253    };
254
255    // NEXT, run the test.
256    run_test(interp, context_id, &info);
257    molt_ok!()
258}
259
260// The fancier, more flexible version of the test.
261fn fancy_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
262    check_args(
263        1,
264        argv,
265        4,
266        0,
267        "name description option value ?option value...?",
268    )?;
269
270    // FIRST, get the test tinfo
271    let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
272    let mut iter = argv[3..].iter();
273    loop {
274        let opt = iter.next();
275        if opt.is_none() {
276            break;
277        }
278        let opt = opt.unwrap().as_str();
279
280        let val = iter.next();
281        if val.is_none() {
282            incr_errors(interp, context_id);
283            info.print_helper_error("test command", &format!("missing value for {}", opt));
284            return molt_ok!();
285        }
286        let val = val.unwrap().as_str();
287
288        match opt {
289            "-setup" => info.setup = val.to_string(),
290            "-body" => info.body = val.to_string(),
291            "-cleanup" => info.cleanup = val.to_string(),
292            "-ok" => {
293                info.code = Code::Ok;
294                info.expect = val.to_string();
295            }
296            "-error" => {
297                info.code = Code::Error;
298                info.expect = val.to_string();
299            }
300            _ => {
301                incr_errors(interp, context_id);
302                info.print_helper_error("test command", &format!("invalid option: \"{}\"", val));
303                return molt_ok!();
304            }
305        }
306    }
307
308    // NEXT, run the test.
309    run_test(interp, context_id, &info);
310    molt_ok!()
311}
312
313// Run the actual test and save the result.
314fn run_test(interp: &mut Interp, context_id: ContextID, info: &TestInfo) {
315    // FIRST, push a variable scope; -setup, -body, and -cleanup will share it.
316    interp.push_scope();
317
318    // NEXT, execute the parts of the test.
319
320    // Setup
321    if let Err(exception) = interp.eval(&info.setup) {
322        if exception.code() == ResultCode::Error {
323            info.print_helper_error("-setup", exception.value().as_str());
324        }
325    }
326    // if let Err(ResultCode::Error(msg)) = interp.eval(&info.setup) {
327    //     info.print_helper_error("-setup", &msg.to_string());
328    // }
329
330    // Body
331    let body = Value::from(&info.body);
332    let result = interp.eval_value(&body);
333
334    // Cleanup
335    if let Err(exception) = interp.eval(&info.cleanup) {
336        if exception.code() == ResultCode::Error {
337            info.print_helper_error("-cleanup", exception.value().as_str());
338        }
339    }
340    // if let Err(ResultCode::Error(msg)) = interp.eval(&info.cleanup) {
341    //     info.print_helper_error("-cleanup", &msg.to_string());
342    // }
343
344    // NEXT, pop the scope.
345    interp.pop_scope();
346
347    // NEXT, get the context and save the results.
348    let ctx = interp.context::<TestContext>(context_id);
349    ctx.num_tests += 1;
350
351    match &result {
352        Ok(out) => {
353            if info.code == Code::Ok {
354                if *out == Value::from(&info.expect) {
355                    ctx.num_passed += 1;
356                } else {
357                    ctx.num_failed += 1;
358                    info.print_failure("-ok", &out.to_string());
359                }
360                return;
361            }
362        }
363        Err(exception) => {
364            if info.code == Code::Error {
365                if exception.value() == Value::from(&info.expect) {
366                    ctx.num_passed += 1;
367                } else {
368                    ctx.num_failed += 1;
369                    info.print_failure("-error", exception.value().as_str());
370                }
371                return;
372            }
373        }
374    }
375    ctx.num_errors += 1;
376    info.print_error(&result);
377}
378
379// Increment the failure counter.
380fn incr_errors(interp: &mut Interp, context_id: ContextID) {
381    interp.context::<TestContext>(context_id).num_errors += 1;
382}