Skip to main content

endbasic_std/
testutils.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Test utilities for consumers of the EndBASIC interpreter.
18
19use crate::console::{
20    self, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, remove_control_chars,
21};
22use crate::program::Program;
23use crate::storage::Storage;
24use crate::{Machine, MachineBuilder, Signal, gpio};
25use async_channel::{Receiver, Sender};
26use async_trait::async_trait;
27use endbasic_core::{
28    Callable, ConstantDatum, ExprType, GetGlobalError, GlobalDef, GlobalDefKind, StopReason,
29    SymbolKey,
30};
31use futures_lite::future::block_on;
32use std::cell::RefCell;
33use std::collections::{HashMap, VecDeque};
34use std::io;
35use std::rc::Rc;
36use std::result::Result as StdResult;
37use std::str;
38
39type CheckerResult = StdResult<Option<i32>, String>;
40
41/// A captured command or messages sent to the mock console.
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub enum CapturedOut {
44    /// Represents a call to `Console::clear`.
45    Clear(ClearType),
46
47    /// Represents a call to `Console::set_color`.
48    SetColor(Option<u8>, Option<u8>),
49
50    /// Represents a call to `Console::enter_alt`.
51    EnterAlt,
52
53    /// Represents a call to `Console::hide_cursor`.
54    HideCursor,
55
56    /// Represents a call to `Console::leave_alt`.
57    LeaveAlt,
58
59    /// Represents a call to `Console::locate`.
60    Locate(CharsXY),
61
62    /// Represents a call to `Console::move_within_line`.
63    MoveWithinLine(i16),
64
65    /// Represents a call to `Console::print`.
66    Print(String),
67
68    /// Represents a call to `Console::show_cursor`.
69    ShowCursor,
70
71    /// Represents a call to `Console::write`.
72    Write(String),
73
74    /// Represents a call to `Console::draw_circle`.
75    DrawCircle(PixelsXY, u16),
76
77    /// Represents a call to `Console::draw_circle_filled`.
78    DrawCircleFilled(PixelsXY, u16),
79
80    /// Represents a call to `Console::draw_line`.
81    DrawLine(PixelsXY, PixelsXY),
82
83    /// Represents a call to `Console::draw_pixel`.
84    DrawPixel(PixelsXY),
85
86    /// Represents a call to `Console::draw_rect`.
87    DrawRect(PixelsXY, PixelsXY),
88
89    /// Represents a call to `Console::draw_rect_filled`.
90    DrawRectFilled(PixelsXY, PixelsXY),
91
92    /// Represents a call to `Console::sync_now`.
93    SyncNow,
94
95    /// Represents a call to `Console::set_sync`.
96    SetSync(bool),
97}
98
99/// A console that supplies golden input and captures all output.
100pub struct MockConsole {
101    /// Sequence of keys to yield on `read_key` calls.
102    golden_in: VecDeque<Key>,
103
104    /// Sequence of all messages printed.
105    captured_out: Vec<CapturedOut>,
106
107    /// The size of the mock text console.
108    size_chars: CharsXY,
109
110    /// The size of the mock graphical console.
111    size_pixels: Option<SizeInPixels>,
112
113    /// Whether the console is interactive or not.
114    interactive: bool,
115
116    /// Channel through which to send signals, if present.
117    signals_tx: Option<Sender<Signal>>,
118}
119
120impl Default for MockConsole {
121    fn default() -> Self {
122        Self::new(None)
123    }
124}
125
126impl MockConsole {
127    /// Constructs a new mock console with a `signals_tx` channel.
128    fn new(signals_tx: Option<Sender<Signal>>) -> Self {
129        Self {
130            golden_in: VecDeque::new(),
131            captured_out: vec![],
132            size_chars: CharsXY::new(u16::MAX, u16::MAX),
133            size_pixels: None,
134            interactive: false,
135            signals_tx,
136        }
137    }
138
139    /// Adds a bunch of characters as golden input keys.
140    ///
141    /// Note that some escape characters within `s` are interpreted and added as their
142    /// corresponding `Key`s for simplicity.
143    pub fn add_input_chars(&mut self, s: &str) {
144        for ch in s.chars() {
145            match ch {
146                '\n' => self.golden_in.push_back(Key::NewLine),
147                '\r' => self.golden_in.push_back(Key::CarriageReturn),
148                ch => self.golden_in.push_back(Key::Char(ch)),
149            }
150        }
151    }
152
153    /// Adds a bunch of keys as golden input.
154    pub fn add_input_keys(&mut self, keys: &[Key]) {
155        self.golden_in.extend(keys.iter().cloned());
156    }
157
158    /// Obtains a reference to the captured output.
159    pub fn captured_out(&self) -> &[CapturedOut] {
160        self.captured_out.as_slice()
161    }
162
163    /// Takes the captured output for separate analysis.
164    #[must_use]
165    pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
166        let mut copy = Vec::with_capacity(self.captured_out.len());
167        copy.append(&mut self.captured_out);
168        copy
169    }
170
171    /// Sets the size of the mock text console.
172    pub fn set_size_chars(&mut self, size: CharsXY) {
173        self.size_chars = size;
174    }
175
176    /// Sets the size of the mock graphical console.
177    pub fn set_size_pixels(&mut self, size: SizeInPixels) {
178        self.size_pixels = Some(size);
179    }
180
181    /// Sets whether the mock console is interactive or not.
182    pub fn set_interactive(&mut self, interactive: bool) {
183        self.interactive = interactive;
184    }
185}
186
187impl Drop for MockConsole {
188    fn drop(&mut self) {
189        assert!(
190            self.golden_in.is_empty(),
191            "Not all golden input chars were consumed; {} left",
192            self.golden_in.len()
193        );
194    }
195}
196
197#[async_trait(?Send)]
198impl Console for MockConsole {
199    fn clear(&mut self, how: ClearType) -> io::Result<()> {
200        self.captured_out.push(CapturedOut::Clear(how));
201        Ok(())
202    }
203
204    fn color(&self) -> (Option<u8>, Option<u8>) {
205        for o in self.captured_out.iter().rev() {
206            if let CapturedOut::SetColor(fg, bg) = o {
207                return (*fg, *bg);
208            }
209        }
210        (None, None)
211    }
212
213    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
214        self.captured_out.push(CapturedOut::SetColor(fg, bg));
215        Ok(())
216    }
217
218    fn enter_alt(&mut self) -> io::Result<()> {
219        self.captured_out.push(CapturedOut::EnterAlt);
220        Ok(())
221    }
222
223    fn hide_cursor(&mut self) -> io::Result<()> {
224        self.captured_out.push(CapturedOut::HideCursor);
225        Ok(())
226    }
227
228    fn is_interactive(&self) -> bool {
229        self.interactive
230    }
231
232    fn leave_alt(&mut self) -> io::Result<()> {
233        self.captured_out.push(CapturedOut::LeaveAlt);
234        Ok(())
235    }
236
237    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
238        assert!(pos.x < self.size_chars.x);
239        assert!(pos.y < self.size_chars.y);
240        self.captured_out.push(CapturedOut::Locate(pos));
241        Ok(())
242    }
243
244    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
245        self.captured_out.push(CapturedOut::MoveWithinLine(off));
246        Ok(())
247    }
248
249    fn print(&mut self, text: &str) -> io::Result<()> {
250        let text = remove_control_chars(text.to_owned());
251
252        self.captured_out.push(CapturedOut::Print(text));
253        Ok(())
254    }
255
256    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
257        match self.golden_in.pop_front() {
258            Some(ch) => {
259                if ch == Key::Interrupt
260                    && let Some(signals_tx) = &self.signals_tx
261                {
262                    let _ = signals_tx.send(Signal::Break).await;
263                }
264                Ok(Some(ch))
265            }
266            None => Ok(None),
267        }
268    }
269
270    async fn read_key(&mut self) -> io::Result<Key> {
271        match self.golden_in.pop_front() {
272            Some(ch) => {
273                if ch == Key::Interrupt
274                    && let Some(signals_tx) = &self.signals_tx
275                {
276                    let _ = signals_tx.send(Signal::Break).await;
277                }
278                Ok(ch)
279            }
280            None => Ok(Key::Eof),
281        }
282    }
283
284    fn show_cursor(&mut self) -> io::Result<()> {
285        self.captured_out.push(CapturedOut::ShowCursor);
286        Ok(())
287    }
288
289    fn size_chars(&self) -> io::Result<CharsXY> {
290        Ok(self.size_chars)
291    }
292
293    fn size_pixels(&self) -> io::Result<SizeInPixels> {
294        match self.size_pixels {
295            Some(size) => Ok(size),
296            None => Err(io::Error::other("Graphical console size not yet set")),
297        }
298    }
299
300    fn write(&mut self, text: &str) -> io::Result<()> {
301        let text = remove_control_chars(text.to_owned());
302
303        self.captured_out.push(CapturedOut::Write(text));
304        Ok(())
305    }
306
307    fn draw_circle(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
308        self.captured_out.push(CapturedOut::DrawCircle(xy, r));
309        Ok(())
310    }
311
312    fn draw_circle_filled(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
313        self.captured_out.push(CapturedOut::DrawCircleFilled(xy, r));
314        Ok(())
315    }
316
317    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
318        self.captured_out.push(CapturedOut::DrawLine(x1y1, x2y2));
319        Ok(())
320    }
321
322    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
323        self.captured_out.push(CapturedOut::DrawPixel(xy));
324        Ok(())
325    }
326
327    fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
328        self.captured_out.push(CapturedOut::DrawRect(x1y1, x2y2));
329        Ok(())
330    }
331
332    fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
333        self.captured_out.push(CapturedOut::DrawRectFilled(x1y1, x2y2));
334        Ok(())
335    }
336
337    fn sync_now(&mut self) -> io::Result<()> {
338        self.captured_out.push(CapturedOut::SyncNow);
339        Ok(())
340    }
341
342    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
343        let mut previous = true;
344        for o in self.captured_out.iter().rev() {
345            if let CapturedOut::SetSync(e) = o {
346                previous = *e;
347                break;
348            }
349        }
350        self.captured_out.push(CapturedOut::SetSync(enabled));
351        Ok(previous)
352    }
353}
354
355/// Flattens the captured output into a single string resembling what would be shown in the
356/// console for ease of testing.
357pub fn flatten_output(captured_out: Vec<CapturedOut>) -> String {
358    let mut flattened = String::new();
359    for out in captured_out {
360        match out {
361            CapturedOut::Write(bs) => flattened.push_str(&bs),
362            CapturedOut::Print(s) => flattened.push_str(&s),
363            _ => (),
364        }
365    }
366    flattened
367}
368
369/// A stored program that exposes golden contents and accepts new content from the console when
370/// edits are requested.
371#[derive(Default)]
372pub struct RecordedProgram {
373    name: Option<String>,
374    content: String,
375    dirty: bool,
376}
377
378#[async_trait(?Send)]
379impl Program for RecordedProgram {
380    fn is_dirty(&self) -> bool {
381        self.dirty
382    }
383
384    async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
385        let append = console::read_line(console, "", "", None).await?;
386        self.content.push_str(&append);
387        self.content.push('\n');
388        self.dirty = true;
389        Ok(())
390    }
391
392    fn load(&mut self, name: Option<&str>, text: &str) {
393        self.name = name.map(str::to_owned);
394        text.clone_into(&mut self.content);
395        self.dirty = false;
396    }
397
398    fn name(&self) -> Option<&str> {
399        self.name.as_deref()
400    }
401
402    fn set_name(&mut self, name: &str) {
403        self.name = Some(name.to_owned());
404        self.dirty = false;
405    }
406
407    fn text(&self) -> String {
408        self.content.clone()
409    }
410}
411
412/// Builder pattern to prepare an EndBASIC machine for testing purposes.
413#[must_use]
414#[derive(Clone)]
415pub struct Tester {
416    console: Rc<RefCell<MockConsole>>,
417    storage: Rc<RefCell<Storage>>,
418    program: Rc<RefCell<RecordedProgram>>,
419    callables: Vec<Rc<dyn Callable>>,
420    global_defs: Vec<GlobalDef>,
421    interactive: bool,
422    signals_tx: Sender<Signal>,
423    signals_rx: Receiver<Signal>,
424}
425
426impl Default for Tester {
427    /// Creates a new tester for a fully-equipped (interactive) machine.
428    fn default() -> Self {
429        let (signals_tx, signals_rx) = async_channel::unbounded();
430        let console = Rc::from(RefCell::from(MockConsole::new(Some(signals_tx.clone()))));
431        let program = Rc::from(RefCell::from(RecordedProgram::default()));
432        let storage = Rc::from(RefCell::from(Storage::default()));
433        let callables = vec![];
434        let global_defs = vec![];
435        let interactive = true;
436
437        Self {
438            console,
439            storage,
440            program,
441            callables,
442            global_defs,
443            interactive,
444            signals_tx,
445            signals_rx,
446        }
447    }
448}
449
450impl Tester {
451    fn build_machine(
452        console: Rc<RefCell<MockConsole>>,
453        storage: Rc<RefCell<Storage>>,
454        program: Rc<RefCell<RecordedProgram>>,
455        callables: Vec<Rc<dyn Callable>>,
456        global_defs: Vec<GlobalDef>,
457        interactive: bool,
458        signals_chan: (Sender<Signal>, Receiver<Signal>),
459    ) -> Machine {
460        // Default to the no-op pins that always return errors.  GPIO unit tests use MockPins
461        // directly via `make_mock_machine` to validate operation; this Tester wiring is only used
462        // for the error-path tests that go through the real (NoopPins) backend.
463        let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default()));
464        let mut builder = MachineBuilder::default()
465            .with_console(console)
466            .with_globals(global_defs)
467            .with_gpio_pins(gpio_pins)
468            .with_signals_chan(signals_chan);
469
470        for callable in callables {
471            builder.add_callable(callable);
472        }
473
474        if interactive {
475            builder.make_interactive().with_program(program).with_storage(storage).build()
476        } else {
477            builder.build()
478        }
479    }
480
481    /// Creates a new tester with an empty `Machine`.
482    pub fn empty() -> Self {
483        Self { interactive: false, ..Self::default() }
484    }
485
486    /// Registers the given builtin command into the machine, which must not yet be registered.
487    pub fn add_callable(mut self, callable: Rc<dyn Callable>) -> Self {
488        self.callables.push(callable);
489        self
490    }
491
492    /// Adds the `golden_in` characters as console input.
493    pub fn add_input_chars(self, golden_in: &str) -> Self {
494        self.console.borrow_mut().add_input_chars(golden_in);
495        self
496    }
497
498    /// Adds a bunch of keys as golden input to the console.
499    pub fn add_input_keys(self, keys: &[Key]) -> Self {
500        self.console.borrow_mut().add_input_keys(keys);
501        self
502    }
503
504    /// Gets the mock console from the tester.
505    ///
506    /// This method should generally not be used.  Its primary utility is to hook
507    /// externally-instantiated commands into the testing features.
508    pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
509        self.console.clone()
510    }
511
512    /// Gets the recorded program from the tester.
513    ///
514    /// This method should generally not be used.  Its primary utility is to hook
515    /// externally-instantiated commands into the testing features.
516    pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
517        self.program.clone()
518    }
519
520    /// Gets the storage subsystem from the tester.
521    ///
522    /// This method should generally not be used.  Its primary utility is to hook
523    /// externally-instantiated commands into the testing features.
524    pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
525        self.storage.clone()
526    }
527
528    /// Sends a break signal to the machine.
529    pub fn send_break(self) -> Self {
530        block_on(self.signals_tx.send(Signal::Break)).unwrap();
531        self
532    }
533
534    /// Sets a global variable to an initial value.
535    pub fn set_var<S: Into<String>, V: Into<ConstantDatum>>(mut self, name: S, value: V) -> Self {
536        let value = value.into();
537        self.global_defs.push(GlobalDef {
538            name: name.into(),
539            kind: GlobalDefKind::Scalar {
540                etype: match &value {
541                    ConstantDatum::Boolean(..) => ExprType::Boolean,
542                    ConstantDatum::Double(..) => ExprType::Double,
543                    ConstantDatum::Integer(..) => ExprType::Integer,
544                    ConstantDatum::Text(..) => ExprType::Text,
545                },
546                initial_value: Some(value),
547            },
548        });
549        self
550    }
551
552    /// Sets the initial name of the recorded program to `name` (if any) and its contents to `text`.
553    /// Can only be called once and `text` must not be empty.
554    pub fn set_program(self, name: Option<&str>, text: &str) -> Self {
555        assert!(!text.is_empty());
556        {
557            let mut program = self.program.borrow_mut();
558            assert!(program.text().is_empty());
559            program.load(name, text);
560        }
561        self
562    }
563
564    /// Creates or overwrites a file in the storage medium.
565    pub fn write_file(self, name: &str, content: &str) -> Self {
566        block_on(self.storage.borrow_mut().put(name, content.as_bytes())).unwrap();
567        self
568    }
569
570    /// Runs `script` in the configured machine and returns a `Checker` object to validate
571    /// expectations about the execution.
572    pub fn run<S: Into<String>>(&mut self, script: S) -> Checker<'_> {
573        let machine = Self::build_machine(
574            self.console.clone(),
575            self.storage.clone(),
576            self.program.clone(),
577            self.callables.clone(),
578            self.global_defs.clone(),
579            self.interactive,
580            (self.signals_tx.clone(), self.signals_rx.clone()),
581        );
582        let tester = TesterContinuation { tester: self, machine };
583        tester.run(script)
584    }
585
586    /// Creates a continuation from the current tester state without running any code.
587    pub fn continue_from_here(&self) -> TesterContinuation<'_> {
588        let machine = Self::build_machine(
589            self.console.clone(),
590            self.storage.clone(),
591            self.program.clone(),
592            self.callables.clone(),
593            self.global_defs.clone(),
594            self.interactive,
595            (self.signals_tx.clone(), self.signals_rx.clone()),
596        );
597        TesterContinuation { tester: self, machine }
598    }
599
600    /// Runs `scripts` in the configured machine and returns a `Checker` object to validate
601    /// expectations about the execution.
602    ///
603    /// The first entry in `scripts` to fail aborts execution and allows checking the result
604    /// of that specific invocation.
605    ///
606    /// This is useful when compared to `run` because `Machine::exec` compiles the script as one
607    /// unit and thus compilation errors may prevent validating other operations later on.
608    pub fn run_n(&mut self, scripts: &[&str]) -> Checker<'_> {
609        let mut machine = Self::build_machine(
610            self.console.clone(),
611            self.storage.clone(),
612            self.program.clone(),
613            self.callables.clone(),
614            self.global_defs.clone(),
615            self.interactive,
616            (self.signals_tx.clone(), self.signals_rx.clone()),
617        );
618        let mut result = Ok(None);
619        for script in scripts {
620            match machine.compile(&mut script.as_bytes()) {
621                Ok(()) => (),
622                Err(e) => {
623                    result = Err(format!("{}", e));
624                    break;
625                }
626            }
627            result = block_on(machine.exec()).map_err(|e| format!("{}", e));
628            if result.is_err() {
629                break;
630            }
631        }
632        Checker::new(self, machine, result)
633    }
634}
635
636/// A tester that allows continuing a previous check.
637///
638/// This differs from `Tester` in that it provides direct access to the machine, because
639/// the machine has already been built at this point.
640pub struct TesterContinuation<'a> {
641    tester: &'a Tester,
642    machine: Machine,
643}
644
645impl<'a> TesterContinuation<'a> {
646    /// Returns a mutable reference to the machine inside this continuation.
647    pub fn get_machine(&mut self) -> &mut Machine {
648        &mut self.machine
649    }
650
651    /// Runs `script` in the configured machine and returns a `Checker` object to validate
652    /// expectations about the execution.
653    pub fn run<S: Into<String>>(mut self, script: S) -> Checker<'a> {
654        let result = match self.machine.compile(&mut script.into().as_bytes()) {
655            Ok(()) => block_on(self.machine.exec()).map_err(|e| format!("{}", e)),
656            Err(e) => Err(format!("{}", e)),
657        };
658        Checker::new(self.tester, self.machine, result)
659    }
660
661    /// Clears the state of the machine.
662    pub fn clear(mut self) -> Self {
663        self.machine.clear();
664        self
665    }
666}
667
668/// Captures the expected post-execution shape and values of an array.
669struct ExpArray {
670    /// Expected type of each array element.
671    subtype: ExprType,
672
673    /// Expected length of every array dimension in declaration order.
674    dimensions: Vec<usize>,
675
676    /// Sparse collection of expected element values indexed by subscripts.
677    ///
678    /// Any in-bounds position not listed here is expected to contain the default value for
679    /// `subtype`.
680    contents: Vec<(Vec<i32>, ConstantDatum)>,
681}
682
683/// Captures expectations about the execution of a command and validates them.
684#[must_use]
685pub struct Checker<'a> {
686    tester: &'a Tester,
687    machine: Machine,
688    result: CheckerResult,
689    exp_result: CheckerResult,
690    exp_output: Vec<CapturedOut>,
691    exp_drives: HashMap<String, String>,
692    exp_program_name: Option<String>,
693    exp_program_text: String,
694    exp_arrays: HashMap<SymbolKey, ExpArray>,
695    exp_vars: HashMap<SymbolKey, ConstantDatum>,
696}
697
698impl<'a> Checker<'a> {
699    /// Creates a new checker with default expectations based on the results of an execution.
700    ///
701    /// The default expectations are that the execution ran through completion and that it did not
702    /// have any side-effects.
703    fn new(tester: &'a Tester, machine: Machine, result: CheckerResult) -> Self {
704        Self {
705            tester,
706            machine,
707            result,
708            exp_result: Ok(None),
709            exp_output: vec![],
710            exp_drives: HashMap::default(),
711            exp_program_name: None,
712            exp_program_text: String::new(),
713            exp_arrays: HashMap::default(),
714            exp_vars: HashMap::default(),
715        }
716    }
717
718    /// Expects the invocation to have successfully terminated with the given `stop_reason`.
719    ///
720    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
721    /// `expect_err` can only be called once.
722    pub fn expect_ok(mut self, stop_reason: StopReason) -> Self {
723        self.exp_result = Ok(match stop_reason {
724            StopReason::End(code) => Some(code.to_i32()),
725            StopReason::Eof => None,
726            StopReason::Exception(_, _) | StopReason::UpcallAsync(_) | StopReason::Yield => {
727                unreachable!()
728            }
729        });
730        self
731    }
732
733    /// Expects the invocation to have erroneously terminated with the exact `message` during
734    /// compilation.
735    ///
736    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
737    /// `expect_err` can only be called once.
738    pub fn expect_compilation_err<S: Into<String>>(mut self, message: S) -> Self {
739        self.exp_result = Err(message.into());
740        self
741    }
742
743    /// Expects the invocation to have erroneously terminated with the exact `message`.
744    ///
745    /// If not called, defaults to expecting that execution terminated due to EOF.  This or
746    /// `expect_err` can only be called once.
747    pub fn expect_err<S: Into<String>>(mut self, message: S) -> Self {
748        self.exp_result = Err(message.into());
749        self
750    }
751
752    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
753    /// will be tested to have the same `subtype` and `dimensions`, as well as specific `contents`.
754    /// The contents are provided as a collection of subscripts/value pairs to assign to the
755    /// expected array.
756    pub fn expect_array<S: AsRef<str>>(
757        mut self,
758        name: S,
759        subtype: ExprType,
760        dimensions: &[usize],
761        contents: Vec<(Vec<i32>, ConstantDatum)>,
762    ) -> Self {
763        let key = SymbolKey::from(name);
764        assert!(!self.exp_arrays.contains_key(&key));
765        self.exp_arrays
766            .insert(key, ExpArray { subtype, dimensions: dimensions.to_vec(), contents });
767        self
768    }
769
770    /// Adds the `name` array as an array to expect in the final state of the machine.  The array
771    /// will be tested to have the same `subtype` and only one dimension with `contents`.
772    pub fn expect_array_simple<S: AsRef<str>>(
773        mut self,
774        name: S,
775        subtype: ExprType,
776        contents: Vec<ConstantDatum>,
777    ) -> Self {
778        let key = SymbolKey::from(name);
779        assert!(!self.exp_arrays.contains_key(&key));
780        let mut exp_array = Vec::with_capacity(contents.len());
781        for (i, value) in contents.into_iter().enumerate() {
782            exp_array.push((vec![i as i32], value));
783        }
784        self.exp_arrays.insert(
785            key,
786            ExpArray { subtype, dimensions: vec![exp_array.len()], contents: exp_array },
787        );
788        self
789    }
790
791    /// Adds tracking for all the side-effects of a clear operation on the machine.
792    pub fn expect_clear(mut self) -> Self {
793        self.exp_output.append(&mut vec![
794            CapturedOut::LeaveAlt,
795            CapturedOut::SetColor(None, None),
796            CapturedOut::ShowCursor,
797            CapturedOut::SetSync(true),
798        ]);
799        self
800    }
801
802    /// Adds a file to expect in the drive with a `name` and specific `content`.
803    ///
804    /// `name` must be the absolute path to the file that is expected, including the drive name.
805    pub fn expect_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
806        let name = name.into();
807        assert!(!self.exp_drives.contains_key(&name));
808        self.exp_drives.insert(name, content.into());
809        self
810    }
811
812    /// Adds the `out` sequence of captured outputs to the expected outputs of the execution.
813    pub fn expect_output<V: Into<Vec<CapturedOut>>>(mut self, out: V) -> Self {
814        self.exp_output.append(&mut out.into());
815        self
816    }
817
818    /// Adds the `out` sequence of strings to the expected outputs of the execution.
819    ///
820    /// This is a convenience function around `expect_output` that wraps all incoming strings in
821    /// `CapturedOut::Print` objects, as these are the most common outputs in tests.
822    pub fn expect_prints<S: Into<String>, V: Into<Vec<S>>>(mut self, out: V) -> Self {
823        let out = out.into();
824        self.exp_output
825            .append(&mut out.into_iter().map(|x| CapturedOut::Print(x.into())).collect());
826        self
827    }
828
829    /// Sets the expected name of the stored program to `name` and its contents to `text`.  Can only
830    /// be called once and `text` must not be empty.
831    pub fn expect_program<S1: Into<String>, S2: Into<String>>(
832        mut self,
833        name: Option<S1>,
834        text: S2,
835    ) -> Self {
836        assert!(self.exp_program_text.is_empty());
837        let text = text.into();
838        assert!(!text.is_empty());
839        self.exp_program_name = name.map(|x| x.into());
840        self.exp_program_text = text;
841        self
842    }
843
844    /// Adds the `name`/`value` pair as a variable to expect in the final state of the machine.
845    pub fn expect_var<S: AsRef<str>, V: Into<ConstantDatum>>(mut self, name: S, value: V) -> Self {
846        let key = SymbolKey::from(name);
847        assert!(!self.exp_vars.contains_key(&key));
848        self.exp_vars.insert(key, value.into());
849        self
850    }
851
852    /// Takes the captured output for separate analysis.
853    #[must_use]
854    pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
855        assert!(
856            self.exp_output.is_empty(),
857            "Cannot take output if we are already expecting prints because the test would fail"
858        );
859        self.tester.console.borrow_mut().take_captured_out()
860    }
861
862    fn query_array_element(&self, name: &SymbolKey, subscripts: &[i32]) -> Option<ConstantDatum> {
863        match self.machine.vm.get_global_array(&self.machine.image, name, subscripts) {
864            Ok(Some(value)) => Some(value),
865            Ok(None) => self
866                .machine
867                .vm
868                .get_program_array(&self.machine.image, name, subscripts)
869                .unwrap_or_else(|e| panic!("Expected array {} has wrong shape: {}", name, e)),
870            Err(e) => panic!("Expected array {} has wrong shape: {}", name, e),
871        }
872    }
873
874    fn check_array_dims(&self, name: &SymbolKey, dimensions: &[usize]) {
875        if dimensions.is_empty() {
876            panic!("Expected array {} must have at least one dimension", name);
877        }
878
879        let mut subscripts = vec![0; dimensions.len()];
880        for i in 0..dimensions.len() {
881            subscripts[i] = dimensions[i] as i32;
882            match self.machine.vm.get_global_array(&self.machine.image, name, &subscripts) {
883                Err(GetGlobalError::SubscriptOutOfBounds(_)) => (),
884                Ok(None) => {
885                    match self.machine.vm.get_program_array(&self.machine.image, name, &subscripts)
886                    {
887                        Err(GetGlobalError::SubscriptOutOfBounds(_)) => (),
888                        Ok(Some(_)) => panic!(
889                            "Expected array {} dimension {} to be {} but found larger",
890                            name, i, dimensions[i]
891                        ),
892                        Ok(None) => panic!("Expected array {} not defined", name),
893                        Err(e) => panic!("Expected array {} has wrong shape: {}", name, e),
894                    }
895                }
896                Ok(Some(_)) => panic!(
897                    "Expected array {} dimension {} to be {} but found larger",
898                    name, i, dimensions[i]
899                ),
900                Err(e) => panic!("Expected array {} has wrong shape: {}", name, e),
901            }
902            subscripts[i] = 0;
903        }
904    }
905
906    fn check_array(&self, name: &SymbolKey, exp_array: &ExpArray) {
907        let mut exp_contents = HashMap::with_capacity(exp_array.contents.len());
908        for (subscripts, value) in exp_array.contents.iter() {
909            assert_eq!(
910                exp_array.dimensions.len(),
911                subscripts.len(),
912                "Expected array {} has wrong number of subscripts",
913                name
914            );
915            for (i, subscript) in subscripts.iter().enumerate() {
916                assert!(
917                    *subscript >= 0 && *subscript < exp_array.dimensions[i] as i32,
918                    "Expected array {} has out-of-bounds subscript {} at dimension {}",
919                    name,
920                    subscript,
921                    i
922                );
923            }
924            let previous = exp_contents.insert(subscripts.clone(), value.clone());
925            assert!(previous.is_none(), "Expected array {} has duplicate subscripts", name);
926        }
927
928        let default_value = match exp_array.subtype {
929            ExprType::Boolean => ConstantDatum::Boolean(false),
930            ExprType::Double => ConstantDatum::Double(0.0),
931            ExprType::Integer => ConstantDatum::Integer(0),
932            ExprType::Text => ConstantDatum::Text(String::new()),
933        };
934
935        let mut subscripts = vec![0; exp_array.dimensions.len()];
936        loop {
937            let value = self
938                .query_array_element(name, &subscripts)
939                .unwrap_or_else(|| panic!("Expected array {} not defined", name));
940            assert_eq!(
941                exp_contents.get(&subscripts).unwrap_or(&default_value),
942                &value,
943                "Expected array {} at {:?} has wrong value",
944                name,
945                subscripts
946            );
947
948            let mut i = 0;
949            while i < subscripts.len() {
950                subscripts[i] += 1;
951                if subscripts[i] < exp_array.dimensions[i] as i32 {
952                    break;
953                }
954                subscripts[i] = 0;
955                i += 1;
956            }
957            if i == subscripts.len() {
958                break;
959            }
960        }
961
962        self.check_array_dims(name, &exp_array.dimensions);
963    }
964
965    /// Validates all expectations.
966    pub fn check(self) -> TesterContinuation<'a> {
967        assert_eq!(self.exp_result, self.result);
968
969        for (name, exp_value) in self.exp_vars.iter() {
970            let value = match self.machine.vm.get_global(&self.machine.image, name) {
971                Ok(Some(value)) => Some(value),
972                Ok(None) => {
973                    self.machine.vm.get_program(&self.machine.image, name).unwrap_or_else(|e| {
974                        panic!("Expected variable {} has wrong shape: {}", name, e)
975                    })
976                }
977                Err(e) => panic!("Expected variable {} has wrong shape: {}", name, e),
978            };
979            let value = value.unwrap_or_else(|| panic!("Expected variable {} not defined", name));
980            assert_eq!(exp_value, &value, "Expected variable {} has wrong value", name);
981        }
982
983        for (name, exp_array) in self.exp_arrays.iter() {
984            self.check_array(name, exp_array);
985        }
986
987        let drive_contents = {
988            let mut files = HashMap::new();
989            let storage = self.tester.storage.borrow();
990            for (drive_name, target) in storage.mounted().iter() {
991                if target.starts_with("cloud://") {
992                    // TODO(jmmv): Verifying the cloud drives is hard because we would need to mock
993                    // out the requests issued by the checks below.  Ignore them for now.
994                    continue;
995                }
996
997                let root = format!("{}:/", drive_name);
998                for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() {
999                    let path = format!("{}{}", root, name);
1000                    let content = block_on(storage.get(&path)).unwrap();
1001                    let content = String::from_utf8(content).unwrap();
1002                    files.insert(path, content);
1003                }
1004            }
1005            files
1006        };
1007
1008        assert_eq!(self.exp_output, self.tester.console.borrow().captured_out());
1009        assert_eq!(self.exp_program_name.as_deref(), self.tester.program.borrow().name());
1010        assert_eq!(self.exp_program_text, self.tester.program.borrow().text());
1011        assert_eq!(self.exp_drives, drive_contents);
1012
1013        TesterContinuation { tester: self.tester, machine: self.machine }
1014    }
1015}
1016
1017/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`.
1018pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
1019    Tester::default().run(stmt).expect_err(exp_error).check();
1020}
1021
1022/// Executes `stmt` on a default `Tester` instance and checks that it fails with `exp_error`
1023/// during compilation.
1024pub fn check_stmt_compilation_err<S: Into<String>>(exp_error: S, stmt: &str) {
1025    Tester::default().run(stmt).expect_compilation_err(exp_error).check();
1026}
1027
1028/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
1029pub fn check_expr_ok<V: Into<ConstantDatum>>(exp_value: V, expr: &str) {
1030    let exp_value = exp_value.into();
1031    Tester::default().run(format!("result = {}", expr)).expect_var("result", exp_value).check();
1032}
1033
1034/// Executes `expr` on a scripting interpreter and ensures that the result is `exp_value`.
1035///
1036/// Sets all `vars` before evaluating the expression so that the expression can contain variable
1037/// references.
1038pub fn check_expr_ok_with_vars<
1039    V: Into<ConstantDatum>,
1040    VS: Into<Vec<(&'static str, ConstantDatum)>>,
1041>(
1042    exp_value: V,
1043    expr: &str,
1044    vars: VS,
1045) {
1046    let vars = vars.into();
1047
1048    let mut input = String::new();
1049    for (name, value) in vars.as_slice() {
1050        input.push_str(name);
1051        input.push_str(" = ");
1052        input.push_str(&value.as_source());
1053        input.push_str(": ");
1054    }
1055    input.push_str(&format!("result = {}", expr));
1056
1057    let exp_value = exp_value.into();
1058
1059    let mut t = Tester::default();
1060    let mut c = t.run(input);
1061    c = c.expect_var("result", exp_value);
1062    for var in vars.into_iter() {
1063        c = c.expect_var(var.0, var.1.clone());
1064    }
1065    c.check();
1066}
1067
1068/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`.
1069///
1070/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
1071/// machine.
1072pub fn check_expr_error<S: Into<String>>(exp_error: S, expr: &str) {
1073    Tester::default().run(format!("result = {}", expr)).expect_err(exp_error).check();
1074}
1075
1076/// Executes `expr` on a scripting interpreter and ensures that evaluation fails with `exp_error`
1077/// during compilation.
1078///
1079/// Note that `exp_error` is a literal exact match on the formatted error message returned by the
1080/// machine.
1081pub fn check_expr_compilation_error<S: Into<String>>(exp_error: S, expr: &str) {
1082    Tester::default().run(format!("result = {}", expr)).expect_compilation_err(exp_error).check();
1083}