Skip to main content

endbasic_std/
testutils.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Test utilities for consumers of the EndBASIC interpreter.
17
18use crate::console::{
19    self, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, remove_control_chars,
20};
21use crate::gpio;
22use crate::program::Program;
23use crate::storage::Storage;
24use async_trait::async_trait;
25use endbasic_core::ast::{ExprType, Value, VarRef};
26use endbasic_core::exec::{self, Machine, StopReason};
27use endbasic_core::syms::{Array, Callable, Symbol, SymbolKey};
28use futures_lite::future::block_on;
29use std::cell::RefCell;
30use std::collections::{HashMap, VecDeque};
31use std::io;
32use std::rc::Rc;
33use std::result::Result;
34use std::str;
35
36/// A captured command or messages sent to the mock console.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum CapturedOut {
39    /// Represents a call to `Console::clear`.
40    Clear(ClearType),
41
42    /// Represents a call to `Console::set_color`.
43    SetColor(Option<u8>, Option<u8>),
44
45    /// Represents a call to `Console::enter_alt`.
46    EnterAlt,
47
48    /// Represents a call to `Console::hide_cursor`.
49    HideCursor,
50
51    /// Represents a call to `Console::leave_alt`.
52    LeaveAlt,
53
54    /// Represents a call to `Console::locate`.
55    Locate(CharsXY),
56
57    /// Represents a call to `Console::move_within_line`.
58    MoveWithinLine(i16),
59
60    /// Represents a call to `Console::print`.
61    Print(String),
62
63    /// Represents a call to `Console::show_cursor`.
64    ShowCursor,
65
66    /// Represents a call to `Console::write`.
67    Write(String),
68
69    /// Represents a call to `Console::draw_circle`.
70    DrawCircle(PixelsXY, u16),
71
72    /// Represents a call to `Console::draw_circle_filled`.
73    DrawCircleFilled(PixelsXY, u16),
74
75    /// Represents a call to `Console::draw_line`.
76    DrawLine(PixelsXY, PixelsXY),
77
78    /// Represents a call to `Console::draw_pixel`.
79    DrawPixel(PixelsXY),
80
81    /// Represents a call to `Console::draw_rect`.
82    DrawRect(PixelsXY, PixelsXY),
83
84    /// Represents a call to `Console::draw_rect_filled`.
85    DrawRectFilled(PixelsXY, PixelsXY),
86
87    /// Represents a call to `Console::sync_now`.
88    SyncNow,
89
90    /// Represents a call to `Console::set_sync`.
91    SetSync(bool),
92}
93
94/// A console that supplies golden input and captures all output.
95pub struct MockConsole {
96    /// Sequence of keys to yield on `read_key` calls.
97    golden_in: VecDeque<Key>,
98
99    /// Sequence of all messages printed.
100    captured_out: Vec<CapturedOut>,
101
102    /// The size of the mock text console.
103    size_chars: CharsXY,
104
105    /// The size of the mock graphical console.
106    size_pixels: Option<SizeInPixels>,
107
108    /// Whether the console is interactive or not.
109    interactive: bool,
110}
111
112impl Default for MockConsole {
113    fn default() -> Self {
114        Self {
115            golden_in: VecDeque::new(),
116            captured_out: vec![],
117            size_chars: CharsXY::new(u16::MAX, u16::MAX),
118            size_pixels: None,
119            interactive: false,
120        }
121    }
122}
123
124impl MockConsole {
125    /// Adds a bunch of characters as golden input keys.
126    ///
127    /// Note that some escape characters within `s` are interpreted and added as their
128    /// corresponding `Key`s for simplicity.
129    pub fn add_input_chars(&mut self, s: &str) {
130        for ch in s.chars() {
131            match ch {
132                '\n' => self.golden_in.push_back(Key::NewLine),
133                '\r' => self.golden_in.push_back(Key::CarriageReturn),
134                ch => self.golden_in.push_back(Key::Char(ch)),
135            }
136        }
137    }
138
139    /// Adds a bunch of keys as golden input.
140    pub fn add_input_keys(&mut self, keys: &[Key]) {
141        self.golden_in.extend(keys.iter().cloned());
142    }
143
144    /// Obtains a reference to the captured output.
145    pub fn captured_out(&self) -> &[CapturedOut] {
146        self.captured_out.as_slice()
147    }
148
149    /// Takes the captured output for separate analysis.
150    #[must_use]
151    pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
152        let mut copy = Vec::with_capacity(self.captured_out.len());
153        copy.append(&mut self.captured_out);
154        copy
155    }
156
157    /// Sets the size of the mock text console.
158    pub fn set_size_chars(&mut self, size: CharsXY) {
159        self.size_chars = size;
160    }
161
162    /// Sets the size of the mock graphical console.
163    pub fn set_size_pixels(&mut self, size: SizeInPixels) {
164        self.size_pixels = Some(size);
165    }
166
167    /// Sets whether the mock console is interactive or not.
168    pub fn set_interactive(&mut self, interactive: bool) {
169        self.interactive = interactive;
170    }
171}
172
173impl Drop for MockConsole {
174    fn drop(&mut self) {
175        assert!(
176            self.golden_in.is_empty(),
177            "Not all golden input chars were consumed; {} left",
178            self.golden_in.len()
179        );
180    }
181}
182
183#[async_trait(?Send)]
184impl Console for MockConsole {
185    fn clear(&mut self, how: ClearType) -> io::Result<()> {
186        self.captured_out.push(CapturedOut::Clear(how));
187        Ok(())
188    }
189
190    fn color(&self) -> (Option<u8>, Option<u8>) {
191        for o in self.captured_out.iter().rev() {
192            if let CapturedOut::SetColor(fg, bg) = o {
193                return (*fg, *bg);
194            }
195        }
196        (None, None)
197    }
198
199    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
200        self.captured_out.push(CapturedOut::SetColor(fg, bg));
201        Ok(())
202    }
203
204    fn enter_alt(&mut self) -> io::Result<()> {
205        self.captured_out.push(CapturedOut::EnterAlt);
206        Ok(())
207    }
208
209    fn hide_cursor(&mut self) -> io::Result<()> {
210        self.captured_out.push(CapturedOut::HideCursor);
211        Ok(())
212    }
213
214    fn is_interactive(&self) -> bool {
215        self.interactive
216    }
217
218    fn leave_alt(&mut self) -> io::Result<()> {
219        self.captured_out.push(CapturedOut::LeaveAlt);
220        Ok(())
221    }
222
223    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
224        assert!(pos.x < self.size_chars.x);
225        assert!(pos.y < self.size_chars.y);
226        self.captured_out.push(CapturedOut::Locate(pos));
227        Ok(())
228    }
229
230    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
231        self.captured_out.push(CapturedOut::MoveWithinLine(off));
232        Ok(())
233    }
234
235    fn print(&mut self, text: &str) -> io::Result<()> {
236        let text = remove_control_chars(text.to_owned());
237
238        self.captured_out.push(CapturedOut::Print(text));
239        Ok(())
240    }
241
242    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
243        match self.golden_in.pop_front() {
244            Some(ch) => Ok(Some(ch)),
245            None => Ok(None),
246        }
247    }
248
249    async fn read_key(&mut self) -> io::Result<Key> {
250        match self.golden_in.pop_front() {
251            Some(ch) => Ok(ch),
252            None => Ok(Key::Eof),
253        }
254    }
255
256    fn show_cursor(&mut self) -> io::Result<()> {
257        self.captured_out.push(CapturedOut::ShowCursor);
258        Ok(())
259    }
260
261    fn size_chars(&self) -> io::Result<CharsXY> {
262        Ok(self.size_chars)
263    }
264
265    fn size_pixels(&self) -> io::Result<SizeInPixels> {
266        match self.size_pixels {
267            Some(size) => Ok(size),
268            None => Err(io::Error::other("Graphical console size not yet set")),
269        }
270    }
271
272    fn write(&mut self, text: &str) -> io::Result<()> {
273        let text = remove_control_chars(text.to_owned());
274
275        self.captured_out.push(CapturedOut::Write(text));
276        Ok(())
277    }
278
279    fn draw_circle(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
280        self.captured_out.push(CapturedOut::DrawCircle(xy, r));
281        Ok(())
282    }
283
284    fn draw_circle_filled(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
285        self.captured_out.push(CapturedOut::DrawCircleFilled(xy, r));
286        Ok(())
287    }
288
289    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
290        self.captured_out.push(CapturedOut::DrawLine(x1y1, x2y2));
291        Ok(())
292    }
293
294    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
295        self.captured_out.push(CapturedOut::DrawPixel(xy));
296        Ok(())
297    }
298
299    fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
300        self.captured_out.push(CapturedOut::DrawRect(x1y1, x2y2));
301        Ok(())
302    }
303
304    fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
305        self.captured_out.push(CapturedOut::DrawRectFilled(x1y1, x2y2));
306        Ok(())
307    }
308
309    fn sync_now(&mut self) -> io::Result<()> {
310        self.captured_out.push(CapturedOut::SyncNow);
311        Ok(())
312    }
313
314    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
315        let mut previous = true;
316        for o in self.captured_out.iter().rev() {
317            if let CapturedOut::SetSync(e) = o {
318                previous = *e;
319                break;
320            }
321        }
322        self.captured_out.push(CapturedOut::SetSync(enabled));
323        Ok(previous)
324    }
325}
326
327/// Flattens the captured output into a single string resembling what would be shown in the
328/// console for ease of testing.
329pub fn flatten_output(captured_out: Vec<CapturedOut>) -> String {
330    let mut flattened = String::new();
331    for out in captured_out {
332        match out {
333            CapturedOut::Write(bs) => flattened.push_str(&bs),
334            CapturedOut::Print(s) => flattened.push_str(&s),
335            _ => (),
336        }
337    }
338    flattened
339}
340
341/// A stored program that exposes golden contents and accepts new content from the console when
342/// edits are requested.
343#[derive(Default)]
344pub struct RecordedProgram {
345    name: Option<String>,
346    content: String,
347    dirty: bool,
348}
349
350#[async_trait(?Send)]
351impl Program for RecordedProgram {
352    fn is_dirty(&self) -> bool {
353        self.dirty
354    }
355
356    async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
357        let append = console::read_line(console, "", "", None).await?;
358        self.content.push_str(&append);
359        self.content.push('\n');
360        self.dirty = true;
361        Ok(())
362    }
363
364    fn load(&mut self, name: Option<&str>, text: &str) {
365        self.name = name.map(str::to_owned);
366        text.clone_into(&mut self.content);
367        self.dirty = false;
368    }
369
370    fn name(&self) -> Option<&str> {
371        self.name.as_deref()
372    }
373
374    fn set_name(&mut self, name: &str) {
375        self.name = Some(name.to_owned());
376        self.dirty = false;
377    }
378
379    fn text(&self) -> String {
380        self.content.clone()
381    }
382}
383
384/// Builder pattern to prepare an EndBASIC machine for testing purposes.
385#[must_use]
386pub struct Tester {
387    console: Rc<RefCell<MockConsole>>,
388    storage: Rc<RefCell<Storage>>,
389    program: Rc<RefCell<RecordedProgram>>,
390    machine: Machine,
391}
392
393impl Default for Tester {
394    /// Creates a new tester for a fully-equipped (interactive) machine.
395    fn default() -> Self {
396        let console = Rc::from(RefCell::from(MockConsole::default()));
397        let program = Rc::from(RefCell::from(RecordedProgram::default()));
398
399        // Default to the no-op pins that always return errors.  GPIO unit tests use MockPins
400        // directly via `make_mock_machine` to validate operation; this Tester wiring is only used
401        // for the error-path tests that go through the real (NoopPins) backend.
402        let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default()));
403
404        let mut builder = crate::MachineBuilder::default()
405            .with_console(console.clone())
406            .with_gpio_pins(gpio_pins)
407            .make_interactive()
408            .with_program(program.clone());
409
410        // Grab access to the machine's storage subsystem before we lose track of it, as we will
411        // need this to check its state.
412        let storage = builder.get_storage();
413
414        let machine = builder.build().unwrap();
415
416        Self { console, storage, program, machine }
417    }
418}
419
420impl Tester {
421    /// Creates a new tester with an empty `Machine`.
422    pub fn empty() -> Self {
423        let console = Rc::from(RefCell::from(MockConsole::default()));
424        let storage = Rc::from(RefCell::from(Storage::default()));
425        let program = Rc::from(RefCell::from(RecordedProgram::default()));
426
427        let machine = Machine::default();
428
429        Self { console, storage, program, machine }
430    }
431
432    /// Registers the given builtin command into the machine, which must not yet be registered.
433    pub fn add_callable(mut self, callable: Rc<dyn Callable>) -> Self {
434        self.machine.add_callable(callable);
435        self
436    }
437
438    /// Adds the `golden_in` characters as console input.
439    pub fn add_input_chars(self, golden_in: &str) -> Self {
440        self.console.borrow_mut().add_input_chars(golden_in);
441        self
442    }
443
444    /// Adds a bunch of keys as golden input to the console.
445    pub fn add_input_keys(self, keys: &[Key]) -> Self {
446        self.console.borrow_mut().add_input_keys(keys);
447        self
448    }
449
450    /// Returns a mutable reference to the machine inside the tester.
451    ///
452    /// This method should generally not be used, except to run native methods that have
453    /// side-effects on the machine that we'd like to validate later.
454    pub fn get_machine(&mut self) -> &mut Machine {
455        &mut self.machine
456    }
457
458    /// Gets the mock console from the tester.
459    ///
460    /// This method should generally not be used.  Its primary utility is to hook
461    /// externally-instantiated commands into the testing features.
462    pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
463        self.console.clone()
464    }
465
466    /// Gets the recorded program from the tester.
467    ///
468    /// This method should generally not be used.  Its primary utility is to hook
469    /// externally-instantiated commands into the testing features.
470    pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
471        self.program.clone()
472    }
473
474    /// Gets the storage subsystem from the tester.
475    ///
476    /// This method should generally not be used.  Its primary utility is to hook
477    /// externally-instantiated commands into the testing features.
478    pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
479        self.storage.clone()
480    }
481
482    /// Sets a variable to an initial value.
483    pub fn set_var(mut self, name: &str, value: Value) -> Self {
484        self.machine.get_mut_symbols().set_var(&VarRef::new(name, None), value).unwrap();
485        self
486    }
487
488    /// Sets the initial name of the recorded program to `name` (if any) and its contents to `text`.
489    /// Can only be called once and `text` must not be empty.
490    pub fn set_program(self, name: Option<&str>, text: &str) -> Self {
491        assert!(!text.is_empty());
492        {
493            let mut program = self.program.borrow_mut();
494            assert!(program.text().is_empty());
495            program.load(name, text);
496        }
497        self
498    }
499
500    /// Creates or overwrites a file in the storage medium.
501    pub fn write_file(self, name: &str, content: &str) -> Self {
502        block_on(self.storage.borrow_mut().put(name, content.as_bytes())).unwrap();
503        self
504    }
505
506    /// Runs `script` in the configured machine and returns a `Checker` object to validate
507    /// expectations about the execution.
508    pub fn run<S: Into<String>>(&mut self, script: S) -> Checker<'_> {
509        let result = block_on(self.machine.exec(&mut script.into().as_bytes()));
510        Checker::new(self, result)
511    }
512
513    /// Runs `scripts` in the configured machine and returns a `Checker` object to validate
514    /// expectations about the execution.
515    ///
516    /// The first entry in `scripts` to fail aborts execution and allows checking the result
517    /// of that specific invocation.
518    ///
519    /// This is useful when compared to `run` because `Machine::exec` compiles the script as one
520    /// unit and thus compilation errors may prevent validating other operations later on.
521    pub fn run_n(&mut self, scripts: &[&str]) -> Checker<'_> {
522        let mut result = Ok(StopReason::Eof);
523        for script in scripts {
524            result = block_on(self.machine.exec(&mut script.as_bytes()));
525            if result.is_err() {
526                break;
527            }
528        }
529        Checker::new(self, result)
530    }
531}
532
533/// Captures expectations about the execution of a command and validates them.
534#[must_use]
535pub struct Checker<'a> {
536    tester: &'a Tester,
537    result: exec::Result<StopReason>,
538    exp_result: Result<StopReason, String>,
539    exp_output: Vec<CapturedOut>,
540    exp_drives: HashMap<String, String>,
541    exp_program_name: Option<String>,
542    exp_program_text: String,
543    exp_arrays: HashMap<SymbolKey, Array>,
544    exp_vars: HashMap<SymbolKey, Value>,
545}
546
547impl<'a> Checker<'a> {
548    /// Creates a new checker with default expectations based on the results of an execution.
549    ///
550    /// The default expectations are that the execution ran through completion and that it did not
551    /// have any side-effects.
552    fn new(tester: &'a Tester, result: exec::Result<StopReason>) -> Self {
553        Self {
554            tester,
555            result,
556            exp_result: Ok(StopReason::Eof),
557            exp_output: vec![],
558            exp_drives: HashMap::default(),
559            exp_program_name: None,
560            exp_program_text: String::new(),
561            exp_arrays: HashMap::default(),
562            exp_vars: HashMap::default(),
563        }
564    }
565
566    /// Expects the invocation to have successfully terminated with the given `stop_reason`.
567    ///
568    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
569    /// `expect_err` can only be called once.
570    pub fn expect_ok(mut self, stop_reason: StopReason) -> Self {
571        assert_eq!(Ok(StopReason::Eof), self.exp_result);
572        self.exp_result = Ok(stop_reason);
573        self
574    }
575
576    /// Expects the invocation to have erroneously terminated with the exact `message` during
577    /// compilation.
578    ///
579    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
580    /// `expect_err` can only be called once.
581    pub fn expect_compilation_err<S: Into<String>>(mut self, message: S) -> Self {
582        let message = message.into();
583        assert_eq!(Ok(StopReason::Eof), self.exp_result);
584        self.exp_result = Err(message.clone());
585        self
586    }
587
588    /// Expects the invocation to have erroneously terminated with the exact `message`.
589    ///
590    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
591    /// `expect_err` can only be called once.
592    pub fn expect_err<S: Into<String>>(mut self, message: S) -> Self {
593        let message = message.into();
594        assert_eq!(Ok(StopReason::Eof), self.exp_result);
595        self.exp_result = Err(message.clone());
596        self
597    }
598
599    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
600    /// will be tested to have the same `subtype` and `dimensions`, as well as specific `contents`.
601    /// The contents are provided as a collection of subscripts/value pairs to assign to the
602    /// expected array.
603    pub fn expect_array<S: AsRef<str>>(
604        mut self,
605        name: S,
606        subtype: ExprType,
607        dimensions: &[usize],
608        contents: Vec<(&[i32], Value)>,
609    ) -> Self {
610        let key = SymbolKey::from(name);
611        assert!(!self.exp_arrays.contains_key(&key));
612        let mut array = Array::new(subtype, dimensions.to_owned());
613        for (subscripts, value) in contents.into_iter() {
614            array.assign(subscripts, value).unwrap();
615        }
616        self.exp_arrays.insert(key, array);
617        self
618    }
619
620    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
621    /// will be tested to have the same `subtype` and only one dimension with `contents`.
622    pub fn expect_array_simple<S: AsRef<str>>(
623        mut self,
624        name: S,
625        subtype: ExprType,
626        contents: Vec<Value>,
627    ) -> Self {
628        let key = SymbolKey::from(name);
629        assert!(!self.exp_arrays.contains_key(&key));
630        let mut array = Array::new(subtype, vec![contents.len()]);
631        for (i, value) in contents.into_iter().enumerate() {
632            array.assign(&[i as i32], value).unwrap();
633        }
634        self.exp_arrays.insert(key, array);
635        self
636    }
637
638    /// Adds tracking for all the side-effects of a clear operation on the machine.
639    pub fn expect_clear(mut self) -> Self {
640        self.exp_output.append(&mut vec![
641            CapturedOut::LeaveAlt,
642            CapturedOut::SetColor(None, None),
643            CapturedOut::ShowCursor,
644            CapturedOut::SetSync(true),
645        ]);
646        self
647    }
648
649    /// Adds a file to expect in the drive with a `name` and specific `content`.
650    ///
651    /// `name` must be the absolute path to the file that is expected, including the drive name.
652    pub fn expect_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
653        let name = name.into();
654        assert!(!self.exp_drives.contains_key(&name));
655        self.exp_drives.insert(name, content.into());
656        self
657    }
658
659    /// Adds the `out` sequence of captured outputs to the expected outputs of the execution.
660    pub fn expect_output<V: Into<Vec<CapturedOut>>>(mut self, out: V) -> Self {
661        self.exp_output.append(&mut out.into());
662        self
663    }
664
665    /// Adds the `out` sequence of strings to the expected outputs of the execution.
666    ///
667    /// This is a convenience function around `expect_output` that wraps all incoming strings in
668    /// `CapturedOut::Print` objects, as these are the most common outputs in tests.
669    pub fn expect_prints<S: Into<String>, V: Into<Vec<S>>>(mut self, out: V) -> Self {
670        let out = out.into();
671        self.exp_output
672            .append(&mut out.into_iter().map(|x| CapturedOut::Print(x.into())).collect());
673        self
674    }
675
676    /// Sets the expected name of the stored program to `name` and its contents to `text`.  Can only
677    /// be called once and `text` must not be empty.
678    pub fn expect_program<S1: Into<String>, S2: Into<String>>(
679        mut self,
680        name: Option<S1>,
681        text: S2,
682    ) -> Self {
683        assert!(self.exp_program_text.is_empty());
684        let text = text.into();
685        assert!(!text.is_empty());
686        self.exp_program_name = name.map(|x| x.into());
687        self.exp_program_text = text;
688        self
689    }
690
691    /// Adds the `name`/`value` pair as a variable to expect in the final state of the machine.
692    pub fn expect_var<S: AsRef<str>, V: Into<Value>>(mut self, name: S, value: V) -> Self {
693        let key = SymbolKey::from(name);
694        assert!(!self.exp_vars.contains_key(&key));
695        self.exp_vars.insert(key, value.into());
696        self
697    }
698
699    /// Takes the captured output for separate analysis.
700    #[must_use]
701    pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
702        assert!(
703            self.exp_output.is_empty(),
704            "Cannot take output if we are already expecting prints because the test would fail"
705        );
706        self.tester.console.borrow_mut().take_captured_out()
707    }
708
709    /// Validates all expectations.
710    pub fn check(self) {
711        match self.result {
712            Ok(stop_reason) => assert_eq!(self.exp_result.unwrap(), stop_reason),
713            Err(e) => assert_eq!(self.exp_result.unwrap_err(), format!("{}", e)),
714        };
715
716        let mut arrays = HashMap::default();
717        let mut vars = HashMap::default();
718        for (name, symbol) in self.tester.machine.get_symbols().locals() {
719            match symbol {
720                Symbol::Array(array) => {
721                    // TODO(jmmv): This array.clone() call is a hack to simplify the equality check
722                    // below.  Should try to avoid it and remove the Clone impl from Array.
723                    arrays.insert(name.clone(), array.clone());
724                }
725                Symbol::Callable(_) => {
726                    // We currently don't support user-defined callables at runtime so there is no
727                    // need to validate anything about them.
728                }
729                Symbol::Variable(value) => {
730                    vars.insert(name.clone(), value.clone());
731                }
732            }
733        }
734
735        let drive_contents = {
736            let mut files = HashMap::new();
737            let storage = self.tester.storage.borrow();
738            for (drive_name, target) in storage.mounted().iter() {
739                if target.starts_with("cloud://") {
740                    // TODO(jmmv): Verifying the cloud drives is hard because we would need to mock
741                    // out the requests issued by the checks below.  Ignore them for now.
742                    continue;
743                }
744
745                let root = format!("{}:/", drive_name);
746                for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() {
747                    let path = format!("{}{}", root, name);
748                    let content = block_on(storage.get(&path)).unwrap();
749                    let content = String::from_utf8(content).unwrap();
750                    files.insert(path, content);
751                }
752            }
753            files
754        };
755
756        assert_eq!(self.exp_vars, vars);
757        assert_eq!(self.exp_arrays, arrays);
758        assert_eq!(self.exp_output, self.tester.console.borrow().captured_out());
759        assert_eq!(self.exp_program_name.as_deref(), self.tester.program.borrow().name());
760        assert_eq!(self.exp_program_text, self.tester.program.borrow().text());
761        assert_eq!(self.exp_drives, drive_contents);
762    }
763}
764
765/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`.
766pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
767    Tester::default().run(stmt).expect_err(exp_error).check();
768}
769
770/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`
771/// during compilation.
772pub fn check_stmt_compilation_err<S: Into<String>>(exp_error: S, stmt: &str) {
773    Tester::default().run(stmt).expect_compilation_err(exp_error).check();
774}
775
776/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
777pub fn check_expr_ok<V: Into<Value>>(exp_value: V, expr: &str) {
778    Tester::default()
779        .run(format!("result = {}", expr))
780        .expect_var("result", exp_value.into())
781        .check();
782}
783
784/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
785///
786/// Sets all `vars` before evaluating the expression so that the expression can contain variable
787/// references.
788pub fn check_expr_ok_with_vars<V: Into<Value>, VS: Into<Vec<(&'static str, Value)>>>(
789    exp_value: V,
790    expr: &str,
791    vars: VS,
792) {
793    let vars = vars.into();
794
795    let mut t = Tester::default();
796    for var in vars.as_slice() {
797        t = t.set_var(var.0, var.1.clone());
798    }
799
800    let mut c = t.run(format!("result = {}", expr));
801    c = c.expect_var("result", exp_value.into());
802    for var in vars.into_iter() {
803        c = c.expect_var(var.0, var.1.clone());
804    }
805    c.check();
806}
807
808/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`.
809///
810/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
811/// machine.
812pub fn check_expr_error<S: Into<String>>(exp_error: S, expr: &str) {
813    Tester::default().run(format!("result = {}", expr)).expect_err(exp_error).check();
814}
815
816/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`
817/// during compilation.
818///
819/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
820/// machine.
821pub fn check_expr_compilation_error<S: Into<String>>(exp_error: S, expr: &str) {
822    Tester::default().run(format!("result = {}", expr)).expect_compilation_err(exp_error).check();
823}