1use 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#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum CapturedOut {
39 Clear(ClearType),
41
42 SetColor(Option<u8>, Option<u8>),
44
45 EnterAlt,
47
48 HideCursor,
50
51 LeaveAlt,
53
54 Locate(CharsXY),
56
57 MoveWithinLine(i16),
59
60 Print(String),
62
63 ShowCursor,
65
66 Write(String),
68
69 DrawCircle(PixelsXY, u16),
71
72 DrawCircleFilled(PixelsXY, u16),
74
75 DrawLine(PixelsXY, PixelsXY),
77
78 DrawPixel(PixelsXY),
80
81 DrawRect(PixelsXY, PixelsXY),
83
84 DrawRectFilled(PixelsXY, PixelsXY),
86
87 SyncNow,
89
90 SetSync(bool),
92}
93
94pub struct MockConsole {
96 golden_in: VecDeque<Key>,
98
99 captured_out: Vec<CapturedOut>,
101
102 size_chars: CharsXY,
104
105 size_pixels: Option<SizeInPixels>,
107
108 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 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 pub fn add_input_keys(&mut self, keys: &[Key]) {
141 self.golden_in.extend(keys.iter().cloned());
142 }
143
144 pub fn captured_out(&self) -> &[CapturedOut] {
146 self.captured_out.as_slice()
147 }
148
149 #[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 pub fn set_size_chars(&mut self, size: CharsXY) {
159 self.size_chars = size;
160 }
161
162 pub fn set_size_pixels(&mut self, size: SizeInPixels) {
164 self.size_pixels = Some(size);
165 }
166
167 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
327pub 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#[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#[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 fn default() -> Self {
396 let console = Rc::from(RefCell::from(MockConsole::default()));
397 let program = Rc::from(RefCell::from(RecordedProgram::default()));
398
399 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 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 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 pub fn add_callable(mut self, callable: Rc<dyn Callable>) -> Self {
436 self.machine.add_callable(callable);
437 self
438 }
439
440 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 pub fn add_input_keys(self, keys: &[Key]) -> Self {
448 self.console.borrow_mut().add_input_keys(keys);
449 self
450 }
451
452 pub fn get_machine(&mut self) -> &mut Machine {
457 &mut self.machine
458 }
459
460 pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
465 self.console.clone()
466 }
467
468 pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
473 self.program.clone()
474 }
475
476 pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
481 self.storage.clone()
482 }
483
484 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 arrays.insert(name.clone(), array.clone());
726 }
727 Symbol::Callable(_) => {
728 }
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 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
766pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
768 Tester::default().run(stmt).expect_err(exp_error).check();
769}
770
771pub 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
777pub 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
785pub 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
809pub 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
817pub 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}