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, remove_control_chars, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels,
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::new(io::ErrorKind::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 pins set that always returns errors.  We could have implemented a set of
400        // fake pins here to track GPIO state changes in a nicer way, similar to how we track all
401        // other machine state... but the GPIO module already implements its own mocking feature.
402        // The mocking feature is necessary for integration testing in any case, so we just use that
403        // everywhere instead of having yet another implementation in this module.
404        let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default()));
405
406        let mut builder = crate::MachineBuilder::default()
407            .with_console(console.clone())
408            .with_gpio_pins(gpio_pins)
409            .make_interactive()
410            .with_program(program.clone());
411
412        // Grab access to the machine's storage subsystem before we lose track of it, as we will
413        // need this to check its state.
414        let storage = builder.get_storage();
415
416        let machine = builder.build().unwrap();
417
418        Self { console, storage, program, machine }
419    }
420}
421
422impl Tester {
423    /// Creates a new tester with an empty `Machine`.
424    pub fn empty() -> Self {
425        let console = Rc::from(RefCell::from(MockConsole::default()));
426        let storage = Rc::from(RefCell::from(Storage::default()));
427        let program = Rc::from(RefCell::from(RecordedProgram::default()));
428
429        let machine = Machine::default();
430
431        Self { console, storage, program, machine }
432    }
433
434    /// Registers the given builtin command into the machine, which must not yet be registered.
435    pub fn add_callable(mut self, callable: Rc<dyn Callable>) -> Self {
436        self.machine.add_callable(callable);
437        self
438    }
439
440    /// Adds the `golden_in` characters as console input.
441    pub fn add_input_chars(self, golden_in: &str) -> Self {
442        self.console.borrow_mut().add_input_chars(golden_in);
443        self
444    }
445
446    /// Adds a bunch of keys as golden input to the console.
447    pub fn add_input_keys(self, keys: &[Key]) -> Self {
448        self.console.borrow_mut().add_input_keys(keys);
449        self
450    }
451
452    /// Returns a mutable reference to the machine inside the tester.
453    ///
454    /// This method should generally not be used, except to run native methods that have
455    /// side-effects on the machine that we'd like to validate later.
456    pub fn get_machine(&mut self) -> &mut Machine {
457        &mut self.machine
458    }
459
460    /// Gets the mock console from the tester.
461    ///
462    /// This method should generally not be used.  Its primary utility is to hook
463    /// externally-instantiated commands into the testing features.
464    pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
465        self.console.clone()
466    }
467
468    /// Gets the recorded program from the tester.
469    ///
470    /// This method should generally not be used.  Its primary utility is to hook
471    /// externally-instantiated commands into the testing features.
472    pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
473        self.program.clone()
474    }
475
476    /// Gets the storage subsystem from the tester.
477    ///
478    /// This method should generally not be used.  Its primary utility is to hook
479    /// externally-instantiated commands into the testing features.
480    pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
481        self.storage.clone()
482    }
483
484    /// Sets a variable to an initial value.
485    pub fn set_var(mut self, name: &str, value: Value) -> Self {
486        self.machine.get_mut_symbols().set_var(&VarRef::new(name, None), value).unwrap();
487        self
488    }
489
490    /// Sets the initial name of the recorded program to `name` (if any) and its contents to `text`.
491    /// Can only be called once and `text` must not be empty.
492    pub fn set_program(self, name: Option<&str>, text: &str) -> Self {
493        assert!(!text.is_empty());
494        {
495            let mut program = self.program.borrow_mut();
496            assert!(program.text().is_empty());
497            program.load(name, text);
498        }
499        self
500    }
501
502    /// Creates or overwrites a file in the storage medium.
503    pub fn write_file(self, name: &str, content: &str) -> Self {
504        block_on(self.storage.borrow_mut().put(name, content)).unwrap();
505        self
506    }
507
508    /// Runs `script` in the configured machine and returns a `Checker` object to validate
509    /// expectations about the execution.
510    pub fn run<S: Into<String>>(&mut self, script: S) -> Checker {
511        let result = block_on(self.machine.exec(&mut script.into().as_bytes()));
512        Checker::new(self, result)
513    }
514
515    /// Runs `scripts` in the configured machine and returns a `Checker` object to validate
516    /// expectations about the execution.
517    ///
518    /// The first entry in `scripts` to fail aborts execution and allows checking the result
519    /// of that specific invocation.
520    ///
521    /// This is useful when compared to `run` because `Machine::exec` compiles the script as one
522    /// unit and thus compilation errors may prevent validating other operations later on.
523    pub fn run_n(&mut self, scripts: &[&str]) -> Checker {
524        let mut result = Ok(StopReason::Eof);
525        for script in scripts {
526            result = block_on(self.machine.exec(&mut script.as_bytes()));
527            if result.is_err() {
528                break;
529            }
530        }
531        Checker::new(self, result)
532    }
533}
534
535/// Captures expectations about the execution of a command and validates them.
536#[must_use]
537pub struct Checker<'a> {
538    tester: &'a Tester,
539    result: exec::Result<StopReason>,
540    exp_result: Result<StopReason, String>,
541    exp_output: Vec<CapturedOut>,
542    exp_drives: HashMap<String, String>,
543    exp_program_name: Option<String>,
544    exp_program_text: String,
545    exp_arrays: HashMap<SymbolKey, Array>,
546    exp_vars: HashMap<SymbolKey, Value>,
547}
548
549impl<'a> Checker<'a> {
550    /// Creates a new checker with default expectations based on the results of an execution.
551    ///
552    /// The default expectations are that the execution ran through completion and that it did not
553    /// have any side-effects.
554    fn new(tester: &'a Tester, result: exec::Result<StopReason>) -> Self {
555        Self {
556            tester,
557            result,
558            exp_result: Ok(StopReason::Eof),
559            exp_output: vec![],
560            exp_drives: HashMap::default(),
561            exp_program_name: None,
562            exp_program_text: String::new(),
563            exp_arrays: HashMap::default(),
564            exp_vars: HashMap::default(),
565        }
566    }
567
568    /// Expects the invocation to have successfully terminated with the given `stop_reason`.
569    ///
570    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
571    /// `expect_err` can only be called once.
572    pub fn expect_ok(mut self, stop_reason: StopReason) -> Self {
573        assert_eq!(Ok(StopReason::Eof), self.exp_result);
574        self.exp_result = Ok(stop_reason);
575        self
576    }
577
578    /// Expects the invocation to have erroneously terminated with the exact `message` during
579    /// compilation.
580    ///
581    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
582    /// `expect_err` can only be called once.
583    pub fn expect_compilation_err<S: Into<String>>(mut self, message: S) -> Self {
584        let message = message.into();
585        assert_eq!(Ok(StopReason::Eof), self.exp_result);
586        self.exp_result = Err(message.clone());
587        self
588    }
589
590    /// Expects the invocation to have erroneously terminated with the exact `message`.
591    ///
592    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
593    /// `expect_err` can only be called once.
594    pub fn expect_err<S: Into<String>>(mut self, message: S) -> Self {
595        let message = message.into();
596        assert_eq!(Ok(StopReason::Eof), self.exp_result);
597        self.exp_result = Err(message.clone());
598        self
599    }
600
601    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
602    /// will be tested to have the same `subtype` and `dimensions`, as well as specific `contents`.
603    /// The contents are provided as a collection of subscripts/value pairs to assign to the
604    /// expected array.
605    pub fn expect_array<S: AsRef<str>>(
606        mut self,
607        name: S,
608        subtype: ExprType,
609        dimensions: &[usize],
610        contents: Vec<(&[i32], Value)>,
611    ) -> Self {
612        let key = SymbolKey::from(name);
613        assert!(!self.exp_arrays.contains_key(&key));
614        let mut array = Array::new(subtype, dimensions.to_owned());
615        for (subscripts, value) in contents.into_iter() {
616            array.assign(subscripts, value).unwrap();
617        }
618        self.exp_arrays.insert(key, array);
619        self
620    }
621
622    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
623    /// will be tested to have the same `subtype` and only one dimension with `contents`.
624    pub fn expect_array_simple<S: AsRef<str>>(
625        mut self,
626        name: S,
627        subtype: ExprType,
628        contents: Vec<Value>,
629    ) -> Self {
630        let key = SymbolKey::from(name);
631        assert!(!self.exp_arrays.contains_key(&key));
632        let mut array = Array::new(subtype, vec![contents.len()]);
633        for (i, value) in contents.into_iter().enumerate() {
634            array.assign(&[i as i32], value).unwrap();
635        }
636        self.exp_arrays.insert(key, array);
637        self
638    }
639
640    /// Adds tracking for all the side-effects of a clear operation on the machine.
641    pub fn expect_clear(mut self) -> Self {
642        self.exp_output.append(&mut vec![
643            CapturedOut::LeaveAlt,
644            CapturedOut::SetColor(None, None),
645            CapturedOut::ShowCursor,
646            CapturedOut::SetSync(true),
647        ]);
648        self
649    }
650
651    /// Adds a file to expect in the drive with a `name` and specific `content`.
652    ///
653    /// `name` must be the absolute path to the file that is expected, including the drive name.
654    pub fn expect_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
655        let name = name.into();
656        assert!(!self.exp_drives.contains_key(&name));
657        self.exp_drives.insert(name, content.into());
658        self
659    }
660
661    /// Adds the `out` sequence of captured outputs to the expected outputs of the execution.
662    pub fn expect_output<V: Into<Vec<CapturedOut>>>(mut self, out: V) -> Self {
663        self.exp_output.append(&mut out.into());
664        self
665    }
666
667    /// Adds the `out` sequence of strings to the expected outputs of the execution.
668    ///
669    /// This is a convenience function around `expect_output` that wraps all incoming strings in
670    /// `CapturedOut::Print` objects, as these are the most common outputs in tests.
671    pub fn expect_prints<S: Into<String>, V: Into<Vec<S>>>(mut self, out: V) -> Self {
672        let out = out.into();
673        self.exp_output
674            .append(&mut out.into_iter().map(|x| CapturedOut::Print(x.into())).collect());
675        self
676    }
677
678    /// Sets the expected name of the stored program to `name` and its contents to `text`.  Can only
679    /// be called once and `text` must not be empty.
680    pub fn expect_program<S1: Into<String>, S2: Into<String>>(
681        mut self,
682        name: Option<S1>,
683        text: S2,
684    ) -> Self {
685        assert!(self.exp_program_text.is_empty());
686        let text = text.into();
687        assert!(!text.is_empty());
688        self.exp_program_name = name.map(|x| x.into());
689        self.exp_program_text = text;
690        self
691    }
692
693    /// Adds the `name`/`value` pair as a variable to expect in the final state of the machine.
694    pub fn expect_var<S: AsRef<str>, V: Into<Value>>(mut self, name: S, value: V) -> Self {
695        let key = SymbolKey::from(name);
696        assert!(!self.exp_vars.contains_key(&key));
697        self.exp_vars.insert(key, value.into());
698        self
699    }
700
701    /// Takes the captured output for separate analysis.
702    #[must_use]
703    pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
704        assert!(
705            self.exp_output.is_empty(),
706            "Cannot take output if we are already expecting prints because the test would fail"
707        );
708        self.tester.console.borrow_mut().take_captured_out()
709    }
710
711    /// Validates all expectations.
712    pub fn check(self) {
713        match self.result {
714            Ok(stop_reason) => assert_eq!(self.exp_result.unwrap(), stop_reason),
715            Err(e) => assert_eq!(self.exp_result.unwrap_err(), format!("{}", e)),
716        };
717
718        let mut arrays = HashMap::default();
719        let mut vars = HashMap::default();
720        for (name, symbol) in self.tester.machine.get_symbols().locals() {
721            match symbol {
722                Symbol::Array(array) => {
723                    // TODO(jmmv): This array.clone() call is a hack to simplify the equality check
724                    // below.  Should try to avoid it and remove the Clone impl from Array.
725                    arrays.insert(name.clone(), array.clone());
726                }
727                Symbol::Callable(_) => {
728                    // We currently don't support user-defined callables at runtime so there is no
729                    // need to validate anything about them.
730                }
731                Symbol::Variable(value) => {
732                    vars.insert(name.clone(), value.clone());
733                }
734            }
735        }
736
737        let drive_contents = {
738            let mut files = HashMap::new();
739            let storage = self.tester.storage.borrow();
740            for (drive_name, target) in storage.mounted().iter() {
741                if target.starts_with("cloud://") {
742                    // TODO(jmmv): Verifying the cloud drives is hard because we would need to mock
743                    // out the requests issued by the checks below.  Ignore them for now.
744                    continue;
745                }
746
747                let root = format!("{}:/", drive_name);
748                for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() {
749                    let path = format!("{}{}", root, name);
750                    let content = block_on(storage.get(&path)).unwrap();
751                    files.insert(path, content);
752                }
753            }
754            files
755        };
756
757        assert_eq!(self.exp_vars, vars);
758        assert_eq!(self.exp_arrays, arrays);
759        assert_eq!(self.exp_output, self.tester.console.borrow().captured_out());
760        assert_eq!(self.exp_program_name.as_deref(), self.tester.program.borrow().name());
761        assert_eq!(self.exp_program_text, self.tester.program.borrow().text());
762        assert_eq!(self.exp_drives, drive_contents);
763    }
764}
765
766/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`.
767pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
768    Tester::default().run(stmt).expect_err(exp_error).check();
769}
770
771/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`
772/// during compilation.
773pub fn check_stmt_compilation_err<S: Into<String>>(exp_error: S, stmt: &str) {
774    Tester::default().run(stmt).expect_compilation_err(exp_error).check();
775}
776
777/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
778pub fn check_expr_ok<V: Into<Value>>(exp_value: V, expr: &str) {
779    Tester::default()
780        .run(format!("result = {}", expr))
781        .expect_var("result", exp_value.into())
782        .check();
783}
784
785/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
786///
787/// Sets all `vars` before evaluating the expression so that the expression can contain variable
788/// references.
789pub fn check_expr_ok_with_vars<V: Into<Value>, VS: Into<Vec<(&'static str, Value)>>>(
790    exp_value: V,
791    expr: &str,
792    vars: VS,
793) {
794    let vars = vars.into();
795
796    let mut t = Tester::default();
797    for var in vars.as_slice() {
798        t = t.set_var(var.0, var.1.clone());
799    }
800
801    let mut c = t.run(format!("result = {}", expr));
802    c = c.expect_var("result", exp_value.into());
803    for var in vars.into_iter() {
804        c = c.expect_var(var.0, var.1.clone());
805    }
806    c.check();
807}
808
809/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`.
810///
811/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
812/// machine.
813pub fn check_expr_error<S: Into<String>>(exp_error: S, expr: &str) {
814    Tester::default().run(format!("result = {}", expr)).expect_err(exp_error).check();
815}
816
817/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`
818/// during compilation.
819///
820/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
821/// machine.
822pub fn check_expr_compilation_error<S: Into<String>>(exp_error: S, expr: &str) {
823    Tester::default().run(format!("result = {}", expr)).expect_compilation_err(exp_error).check();
824}