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