1use crate::console::{
19 self, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, remove_control_chars,
20};
21use crate::gpio;
22use crate::program::Program;
23use crate::storage::Storage;
24use async_trait::async_trait;
25use endbasic_core::ast::{ExprType, Value, VarRef};
26use endbasic_core::exec::{self, Machine, StopReason};
27use endbasic_core::syms::{Array, Callable, Symbol, SymbolKey};
28use futures_lite::future::block_on;
29use std::cell::RefCell;
30use std::collections::{HashMap, VecDeque};
31use std::io;
32use std::rc::Rc;
33use std::result::Result;
34use std::str;
35
36#[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::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()));
403
404 let mut builder = crate::MachineBuilder::default()
405 .with_console(console.clone())
406 .with_gpio_pins(gpio_pins)
407 .make_interactive()
408 .with_program(program.clone());
409
410 let storage = builder.get_storage();
413
414 let machine = builder.build().unwrap();
415
416 Self { console, storage, program, machine }
417 }
418}
419
420impl Tester {
421 pub fn empty() -> Self {
423 let console = Rc::from(RefCell::from(MockConsole::default()));
424 let storage = Rc::from(RefCell::from(Storage::default()));
425 let program = Rc::from(RefCell::from(RecordedProgram::default()));
426
427 let machine = Machine::default();
428
429 Self { console, storage, program, machine }
430 }
431
432 pub fn add_callable(mut self, callable: Rc<dyn Callable>) -> Self {
434 self.machine.add_callable(callable);
435 self
436 }
437
438 pub fn add_input_chars(self, golden_in: &str) -> Self {
440 self.console.borrow_mut().add_input_chars(golden_in);
441 self
442 }
443
444 pub fn add_input_keys(self, keys: &[Key]) -> Self {
446 self.console.borrow_mut().add_input_keys(keys);
447 self
448 }
449
450 pub fn get_machine(&mut self) -> &mut Machine {
455 &mut self.machine
456 }
457
458 pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
463 self.console.clone()
464 }
465
466 pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
471 self.program.clone()
472 }
473
474 pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
479 self.storage.clone()
480 }
481
482 pub fn set_var(mut self, name: &str, value: Value) -> Self {
484 self.machine.get_mut_symbols().set_var(&VarRef::new(name, None), value).unwrap();
485 self
486 }
487
488 pub fn set_program(self, name: Option<&str>, text: &str) -> Self {
491 assert!(!text.is_empty());
492 {
493 let mut program = self.program.borrow_mut();
494 assert!(program.text().is_empty());
495 program.load(name, text);
496 }
497 self
498 }
499
500 pub fn write_file(self, name: &str, content: &str) -> Self {
502 block_on(self.storage.borrow_mut().put(name, content.as_bytes())).unwrap();
503 self
504 }
505
506 pub fn run<S: Into<String>>(&mut self, script: S) -> Checker<'_> {
509 let result = block_on(self.machine.exec(&mut script.into().as_bytes()));
510 Checker::new(self, result)
511 }
512
513 pub fn run_n(&mut self, scripts: &[&str]) -> Checker<'_> {
522 let mut result = Ok(StopReason::Eof);
523 for script in scripts {
524 result = block_on(self.machine.exec(&mut script.as_bytes()));
525 if result.is_err() {
526 break;
527 }
528 }
529 Checker::new(self, result)
530 }
531}
532
533#[must_use]
535pub struct Checker<'a> {
536 tester: &'a Tester,
537 result: exec::Result<StopReason>,
538 exp_result: Result<StopReason, String>,
539 exp_output: Vec<CapturedOut>,
540 exp_drives: HashMap<String, String>,
541 exp_program_name: Option<String>,
542 exp_program_text: String,
543 exp_arrays: HashMap<SymbolKey, Array>,
544 exp_vars: HashMap<SymbolKey, Value>,
545}
546
547impl<'a> Checker<'a> {
548 fn new(tester: &'a Tester, result: exec::Result<StopReason>) -> Self {
553 Self {
554 tester,
555 result,
556 exp_result: Ok(StopReason::Eof),
557 exp_output: vec![],
558 exp_drives: HashMap::default(),
559 exp_program_name: None,
560 exp_program_text: String::new(),
561 exp_arrays: HashMap::default(),
562 exp_vars: HashMap::default(),
563 }
564 }
565
566 pub fn expect_ok(mut self, stop_reason: StopReason) -> Self {
571 assert_eq!(Ok(StopReason::Eof), self.exp_result);
572 self.exp_result = Ok(stop_reason);
573 self
574 }
575
576 pub fn expect_compilation_err<S: Into<String>>(mut self, message: S) -> Self {
582 let message = message.into();
583 assert_eq!(Ok(StopReason::Eof), self.exp_result);
584 self.exp_result = Err(message.clone());
585 self
586 }
587
588 pub fn expect_err<S: Into<String>>(mut self, message: S) -> Self {
593 let message = message.into();
594 assert_eq!(Ok(StopReason::Eof), self.exp_result);
595 self.exp_result = Err(message.clone());
596 self
597 }
598
599 pub fn expect_array<S: AsRef<str>>(
604 mut self,
605 name: S,
606 subtype: ExprType,
607 dimensions: &[usize],
608 contents: Vec<(&[i32], Value)>,
609 ) -> Self {
610 let key = SymbolKey::from(name);
611 assert!(!self.exp_arrays.contains_key(&key));
612 let mut array = Array::new(subtype, dimensions.to_owned());
613 for (subscripts, value) in contents.into_iter() {
614 array.assign(subscripts, value).unwrap();
615 }
616 self.exp_arrays.insert(key, array);
617 self
618 }
619
620 pub fn expect_array_simple<S: AsRef<str>>(
623 mut self,
624 name: S,
625 subtype: ExprType,
626 contents: Vec<Value>,
627 ) -> Self {
628 let key = SymbolKey::from(name);
629 assert!(!self.exp_arrays.contains_key(&key));
630 let mut array = Array::new(subtype, vec![contents.len()]);
631 for (i, value) in contents.into_iter().enumerate() {
632 array.assign(&[i as i32], value).unwrap();
633 }
634 self.exp_arrays.insert(key, array);
635 self
636 }
637
638 pub fn expect_clear(mut self) -> Self {
640 self.exp_output.append(&mut vec![
641 CapturedOut::LeaveAlt,
642 CapturedOut::SetColor(None, None),
643 CapturedOut::ShowCursor,
644 CapturedOut::SetSync(true),
645 ]);
646 self
647 }
648
649 pub fn expect_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
653 let name = name.into();
654 assert!(!self.exp_drives.contains_key(&name));
655 self.exp_drives.insert(name, content.into());
656 self
657 }
658
659 pub fn expect_output<V: Into<Vec<CapturedOut>>>(mut self, out: V) -> Self {
661 self.exp_output.append(&mut out.into());
662 self
663 }
664
665 pub fn expect_prints<S: Into<String>, V: Into<Vec<S>>>(mut self, out: V) -> Self {
670 let out = out.into();
671 self.exp_output
672 .append(&mut out.into_iter().map(|x| CapturedOut::Print(x.into())).collect());
673 self
674 }
675
676 pub fn expect_program<S1: Into<String>, S2: Into<String>>(
679 mut self,
680 name: Option<S1>,
681 text: S2,
682 ) -> Self {
683 assert!(self.exp_program_text.is_empty());
684 let text = text.into();
685 assert!(!text.is_empty());
686 self.exp_program_name = name.map(|x| x.into());
687 self.exp_program_text = text;
688 self
689 }
690
691 pub fn expect_var<S: AsRef<str>, V: Into<Value>>(mut self, name: S, value: V) -> Self {
693 let key = SymbolKey::from(name);
694 assert!(!self.exp_vars.contains_key(&key));
695 self.exp_vars.insert(key, value.into());
696 self
697 }
698
699 #[must_use]
701 pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
702 assert!(
703 self.exp_output.is_empty(),
704 "Cannot take output if we are already expecting prints because the test would fail"
705 );
706 self.tester.console.borrow_mut().take_captured_out()
707 }
708
709 pub fn check(self) {
711 match self.result {
712 Ok(stop_reason) => assert_eq!(self.exp_result.unwrap(), stop_reason),
713 Err(e) => assert_eq!(self.exp_result.unwrap_err(), format!("{}", e)),
714 };
715
716 let mut arrays = HashMap::default();
717 let mut vars = HashMap::default();
718 for (name, symbol) in self.tester.machine.get_symbols().locals() {
719 match symbol {
720 Symbol::Array(array) => {
721 arrays.insert(name.clone(), array.clone());
724 }
725 Symbol::Callable(_) => {
726 }
729 Symbol::Variable(value) => {
730 vars.insert(name.clone(), value.clone());
731 }
732 }
733 }
734
735 let drive_contents = {
736 let mut files = HashMap::new();
737 let storage = self.tester.storage.borrow();
738 for (drive_name, target) in storage.mounted().iter() {
739 if target.starts_with("cloud://") {
740 continue;
743 }
744
745 let root = format!("{}:/", drive_name);
746 for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() {
747 let path = format!("{}{}", root, name);
748 let content = block_on(storage.get(&path)).unwrap();
749 let content = String::from_utf8(content).unwrap();
750 files.insert(path, content);
751 }
752 }
753 files
754 };
755
756 assert_eq!(self.exp_vars, vars);
757 assert_eq!(self.exp_arrays, arrays);
758 assert_eq!(self.exp_output, self.tester.console.borrow().captured_out());
759 assert_eq!(self.exp_program_name.as_deref(), self.tester.program.borrow().name());
760 assert_eq!(self.exp_program_text, self.tester.program.borrow().text());
761 assert_eq!(self.exp_drives, drive_contents);
762 }
763}
764
765pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
767 Tester::default().run(stmt).expect_err(exp_error).check();
768}
769
770pub fn check_stmt_compilation_err<S: Into<String>>(exp_error: S, stmt: &str) {
773 Tester::default().run(stmt).expect_compilation_err(exp_error).check();
774}
775
776pub fn check_expr_ok<V: Into<Value>>(exp_value: V, expr: &str) {
778 Tester::default()
779 .run(format!("result = {}", expr))
780 .expect_var("result", exp_value.into())
781 .check();
782}
783
784pub fn check_expr_ok_with_vars<V: Into<Value>, VS: Into<Vec<(&'static str, Value)>>>(
789 exp_value: V,
790 expr: &str,
791 vars: VS,
792) {
793 let vars = vars.into();
794
795 let mut t = Tester::default();
796 for var in vars.as_slice() {
797 t = t.set_var(var.0, var.1.clone());
798 }
799
800 let mut c = t.run(format!("result = {}", expr));
801 c = c.expect_var("result", exp_value.into());
802 for var in vars.into_iter() {
803 c = c.expect_var(var.0, var.1.clone());
804 }
805 c.check();
806}
807
808pub fn check_expr_error<S: Into<String>>(exp_error: S, expr: &str) {
813 Tester::default().run(format!("result = {}", expr)).expect_err(exp_error).check();
814}
815
816pub fn check_expr_compilation_error<S: Into<String>>(exp_error: S, expr: &str) {
822 Tester::default().run(format!("result = {}", expr)).expect_compilation_err(exp_error).check();
823}