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