use crate::console::{
self, remove_control_chars, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels,
};
use crate::gpio;
use crate::program::Program;
use crate::storage::Storage;
use async_trait::async_trait;
use endbasic_core::ast::{Value, VarRef, VarType};
use endbasic_core::exec::{self, Machine, StopReason};
use endbasic_core::syms::{Array, Command, Function, Symbol};
use futures_lite::future::block_on;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::io;
use std::rc::Rc;
use std::result::Result;
use std::str;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CapturedOut {
Clear(ClearType),
SetColor(Option<u8>, Option<u8>),
EnterAlt,
HideCursor,
LeaveAlt,
Locate(CharsXY),
MoveWithinLine(i16),
Print(String),
ShowCursor,
Write(String),
DrawCircle(PixelsXY, u16),
DrawCircleFilled(PixelsXY, u16),
DrawLine(PixelsXY, PixelsXY),
DrawPixel(PixelsXY),
DrawRect(PixelsXY, PixelsXY),
DrawRectFilled(PixelsXY, PixelsXY),
SyncNow,
SetSync(bool),
}
pub struct MockConsole {
golden_in: VecDeque<Key>,
captured_out: Vec<CapturedOut>,
size_chars: CharsXY,
size_pixels: Option<SizeInPixels>,
interactive: bool,
}
impl Default for MockConsole {
fn default() -> Self {
Self {
golden_in: VecDeque::new(),
captured_out: vec![],
size_chars: CharsXY::new(u16::MAX, u16::MAX),
size_pixels: None,
interactive: false,
}
}
}
impl MockConsole {
pub fn add_input_chars(&mut self, s: &str) {
for ch in s.chars() {
match ch {
'\n' => self.golden_in.push_back(Key::NewLine),
'\r' => self.golden_in.push_back(Key::CarriageReturn),
ch => self.golden_in.push_back(Key::Char(ch)),
}
}
}
pub fn add_input_keys(&mut self, keys: &[Key]) {
self.golden_in.extend(keys.iter().cloned());
}
pub fn captured_out(&self) -> &[CapturedOut] {
self.captured_out.as_slice()
}
#[must_use]
pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
let mut copy = Vec::with_capacity(self.captured_out.len());
copy.append(&mut self.captured_out);
copy
}
pub fn set_size_chars(&mut self, size: CharsXY) {
self.size_chars = size;
}
pub fn set_size_pixels(&mut self, size: SizeInPixels) {
self.size_pixels = Some(size);
}
pub fn set_interactive(&mut self, interactive: bool) {
self.interactive = interactive;
}
fn verify_all_used(&mut self) {
assert!(
self.golden_in.is_empty(),
"Not all golden input chars were consumed; {} left",
self.golden_in.len()
);
}
}
#[async_trait(?Send)]
impl Console for MockConsole {
fn clear(&mut self, how: ClearType) -> io::Result<()> {
self.captured_out.push(CapturedOut::Clear(how));
Ok(())
}
fn color(&self) -> (Option<u8>, Option<u8>) {
for o in self.captured_out.iter().rev() {
if let CapturedOut::SetColor(fg, bg) = o {
return (*fg, *bg);
}
}
(None, None)
}
fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
self.captured_out.push(CapturedOut::SetColor(fg, bg));
Ok(())
}
fn enter_alt(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::EnterAlt);
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::HideCursor);
Ok(())
}
fn is_interactive(&self) -> bool {
self.interactive
}
fn leave_alt(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::LeaveAlt);
Ok(())
}
fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
assert!(pos.x < self.size_chars.x);
assert!(pos.y < self.size_chars.y);
self.captured_out.push(CapturedOut::Locate(pos));
Ok(())
}
fn move_within_line(&mut self, off: i16) -> io::Result<()> {
self.captured_out.push(CapturedOut::MoveWithinLine(off));
Ok(())
}
fn print(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text.to_owned());
self.captured_out.push(CapturedOut::Print(text));
Ok(())
}
async fn poll_key(&mut self) -> io::Result<Option<Key>> {
match self.golden_in.pop_front() {
Some(ch) => Ok(Some(ch)),
None => Ok(None),
}
}
async fn read_key(&mut self) -> io::Result<Key> {
match self.golden_in.pop_front() {
Some(ch) => Ok(ch),
None => Ok(Key::Eof),
}
}
fn show_cursor(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::ShowCursor);
Ok(())
}
fn size_chars(&self) -> io::Result<CharsXY> {
Ok(self.size_chars)
}
fn size_pixels(&self) -> io::Result<SizeInPixels> {
match self.size_pixels {
Some(size) => Ok(size),
None => Err(io::Error::new(io::ErrorKind::Other, "Graphical console size not yet set")),
}
}
fn write(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text.to_owned());
self.captured_out.push(CapturedOut::Write(text));
Ok(())
}
fn draw_circle(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawCircle(xy, r));
Ok(())
}
fn draw_circle_filled(&mut self, xy: PixelsXY, r: u16) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawCircleFilled(xy, r));
Ok(())
}
fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawLine(x1y1, x2y2));
Ok(())
}
fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawPixel(xy));
Ok(())
}
fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawRect(x1y1, x2y2));
Ok(())
}
fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.captured_out.push(CapturedOut::DrawRectFilled(x1y1, x2y2));
Ok(())
}
fn sync_now(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::SyncNow);
Ok(())
}
fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
let mut previous = true;
for o in self.captured_out.iter().rev() {
if let CapturedOut::SetSync(e) = o {
previous = *e;
break;
}
}
self.captured_out.push(CapturedOut::SetSync(enabled));
Ok(previous)
}
}
pub fn flatten_output(captured_out: Vec<CapturedOut>) -> String {
let mut flattened = String::new();
for out in captured_out {
match out {
CapturedOut::Write(bs) => flattened.push_str(&bs),
CapturedOut::Print(s) => flattened.push_str(&s),
_ => (),
}
}
flattened
}
#[derive(Default)]
pub struct RecordedProgram {
name: Option<String>,
content: String,
dirty: bool,
}
#[async_trait(?Send)]
impl Program for RecordedProgram {
fn is_dirty(&self) -> bool {
self.dirty
}
async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
let append = console::read_line(console, "", "", None).await?;
self.content.push_str(&append);
self.content.push('\n');
self.dirty = true;
Ok(())
}
fn load(&mut self, name: Option<&str>, text: &str) {
self.name = name.map(str::to_owned);
self.content = text.to_owned();
self.dirty = false;
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn set_name(&mut self, name: &str) {
self.name = Some(name.to_owned());
self.dirty = false;
}
fn text(&self) -> String {
self.content.clone()
}
}
#[must_use]
pub struct Tester {
console: Rc<RefCell<MockConsole>>,
storage: Rc<RefCell<Storage>>,
program: Rc<RefCell<RecordedProgram>>,
machine: Machine,
}
impl Default for Tester {
fn default() -> Self {
let console = Rc::from(RefCell::from(MockConsole::default()));
let program = Rc::from(RefCell::from(RecordedProgram::default()));
let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default()));
let mut builder = crate::MachineBuilder::default()
.with_console(console.clone())
.with_gpio_pins(gpio_pins)
.make_interactive()
.with_program(program.clone());
let storage = builder.get_storage();
let machine = builder.build().unwrap();
Self { console, storage, program, machine }
}
}
impl Tester {
pub fn empty() -> Self {
let console = Rc::from(RefCell::from(MockConsole::default()));
let storage = Rc::from(RefCell::from(Storage::default()));
let program = Rc::from(RefCell::from(RecordedProgram::default()));
let machine = Machine::default();
Self { console, storage, program, machine }
}
pub fn add_command(mut self, command: Rc<dyn Command>) -> Self {
self.machine.add_command(command);
self
}
pub fn add_function(mut self, function: Rc<dyn Function>) -> Self {
self.machine.add_function(function);
self
}
pub fn add_input_chars(self, golden_in: &str) -> Self {
self.console.borrow_mut().add_input_chars(golden_in);
self
}
pub fn add_input_keys(self, keys: &[Key]) -> Self {
self.console.borrow_mut().add_input_keys(keys);
self
}
pub fn get_machine(&mut self) -> &mut Machine {
&mut self.machine
}
pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
self.console.clone()
}
pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
self.program.clone()
}
pub fn get_storage(&self) -> Rc<RefCell<Storage>> {
self.storage.clone()
}
pub fn set_var(mut self, name: &str, value: Value) -> Self {
self.machine.get_mut_symbols().set_var(&VarRef::new(name, VarType::Auto), value).unwrap();
self
}
pub fn set_program(self, name: Option<&str>, text: &str) -> Self {
assert!(!text.is_empty());
{
let mut program = self.program.borrow_mut();
assert!(program.text().is_empty());
program.load(name, text);
}
self
}
pub fn write_file(self, name: &str, content: &str) -> Self {
block_on(self.storage.borrow_mut().put(name, content)).unwrap();
self
}
pub fn run<S: Into<String>>(&mut self, script: S) -> Checker {
let result = block_on(self.machine.exec(&mut script.into().as_bytes()));
Checker::new(self, result)
}
}
#[must_use]
pub struct Checker<'a> {
tester: &'a Tester,
result: exec::Result<StopReason>,
exp_result: Result<StopReason, String>,
exp_output: Vec<CapturedOut>,
exp_drives: HashMap<String, String>,
exp_program_name: Option<String>,
exp_program_text: String,
exp_arrays: HashMap<String, Array>,
exp_vars: HashMap<String, Value>,
}
impl<'a> Checker<'a> {
fn new(tester: &'a Tester, result: exec::Result<StopReason>) -> Self {
Self {
tester,
result,
exp_result: Ok(StopReason::Eof),
exp_output: vec![],
exp_drives: HashMap::default(),
exp_program_name: None,
exp_program_text: String::new(),
exp_arrays: HashMap::default(),
exp_vars: HashMap::default(),
}
}
pub fn expect_ok(mut self, stop_reason: StopReason) -> Self {
assert_eq!(Ok(StopReason::Eof), self.exp_result);
self.exp_result = Ok(stop_reason);
self
}
pub fn expect_err<S: Into<String>>(mut self, message: S) -> Self {
let message = message.into();
assert_eq!(Ok(StopReason::Eof), self.exp_result);
self.exp_result = Err(message.clone());
self.exp_vars.insert("0ERRMSG".to_string(), Value::Text(message));
self
}
pub fn expect_uncatchable_err<S: Into<String>>(mut self, message: S) -> Self {
let message = message.into();
assert_eq!(Ok(StopReason::Eof), self.exp_result);
self.exp_result = Err(message);
self
}
pub fn expect_array<S: Into<String>>(
mut self,
name: S,
subtype: VarType,
dimensions: &[usize],
contents: Vec<(&[i32], Value)>,
) -> Self {
let name = name.into().to_ascii_uppercase();
assert!(!self.exp_arrays.contains_key(&name));
let mut array = Array::new(subtype, dimensions.to_owned());
for (subscripts, value) in contents.into_iter() {
array.assign(subscripts, value).unwrap();
}
self.exp_arrays.insert(name, array);
self
}
pub fn expect_array_simple<S: Into<String>>(
mut self,
name: S,
subtype: VarType,
contents: Vec<Value>,
) -> Self {
let name = name.into().to_ascii_uppercase();
assert!(!self.exp_arrays.contains_key(&name));
let mut array = Array::new(subtype, vec![contents.len()]);
for (i, value) in contents.into_iter().enumerate() {
array.assign(&[i as i32], value).unwrap();
}
self.exp_arrays.insert(name, array);
self
}
pub fn expect_clear(mut self) -> Self {
self.exp_output.append(&mut vec![
CapturedOut::LeaveAlt,
CapturedOut::SetColor(None, None),
CapturedOut::ShowCursor,
CapturedOut::SetSync(true),
]);
self
}
pub fn expect_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
let name = name.into();
assert!(!self.exp_drives.contains_key(&name));
self.exp_drives.insert(name, content.into());
self
}
pub fn expect_output<V: Into<Vec<CapturedOut>>>(mut self, out: V) -> Self {
self.exp_output.append(&mut out.into());
self
}
pub fn expect_prints<S: Into<String>, V: Into<Vec<S>>>(mut self, out: V) -> Self {
let out = out.into();
self.exp_output
.append(&mut out.into_iter().map(|x| CapturedOut::Print(x.into())).collect());
self
}
pub fn expect_program<S1: Into<String>, S2: Into<String>>(
mut self,
name: Option<S1>,
text: S2,
) -> Self {
assert!(self.exp_program_text.is_empty());
let text = text.into();
assert!(!text.is_empty());
self.exp_program_name = name.map(|x| x.into());
self.exp_program_text = text;
self
}
pub fn expect_var<S: Into<String>, V: Into<Value>>(mut self, name: S, value: V) -> Self {
let name = name.into().to_ascii_uppercase();
assert!(!self.exp_vars.contains_key(&name));
self.exp_vars.insert(name, value.into());
self
}
#[must_use]
pub fn take_captured_out(&mut self) -> Vec<CapturedOut> {
assert!(
self.exp_output.is_empty(),
"Cannot take output if we are already expecting prints because the test would fail"
);
self.tester.console.borrow_mut().take_captured_out()
}
pub fn check(self) {
match self.result {
Ok(stop_reason) => assert_eq!(self.exp_result.unwrap(), stop_reason),
Err(e) => assert_eq!(self.exp_result.unwrap_err(), format!("{}", e)),
};
let mut arrays = HashMap::default();
let mut vars = HashMap::default();
for (name, symbol) in self.tester.machine.get_symbols().as_hashmap() {
match symbol {
Symbol::Array(array) => {
arrays.insert(name.to_owned(), array.clone());
}
Symbol::Command(_) | Symbol::Function(_) => {
}
Symbol::Variable(value) => {
vars.insert(name.to_owned(), value.clone());
}
}
}
let drive_contents = {
let mut files = HashMap::new();
let storage = self.tester.storage.borrow();
for (drive_name, target) in storage.mounted().iter() {
if target.starts_with("cloud://") {
continue;
}
let root = format!("{}:/", drive_name);
for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() {
let path = format!("{}{}", root, name);
let content = block_on(storage.get(&path)).unwrap();
files.insert(path, content);
}
}
files
};
assert_eq!(self.exp_vars, vars);
assert_eq!(self.exp_arrays, arrays);
assert_eq!(self.exp_output, self.tester.console.borrow().captured_out());
assert_eq!(self.exp_program_name.as_deref(), self.tester.program.borrow().name());
assert_eq!(self.exp_program_text, self.tester.program.borrow().text());
assert_eq!(self.exp_drives, drive_contents);
self.tester.console.borrow_mut().verify_all_used();
}
}
pub fn check_stmt_err<S: Into<String>>(exp_error: S, stmt: &str) {
Tester::default().run(stmt).expect_err(exp_error).check();
}
pub fn check_stmt_uncatchable_err<S: Into<String>>(exp_error: S, stmt: &str) {
Tester::default().run(stmt).expect_uncatchable_err(exp_error).check();
}
pub fn check_expr_ok<V: Into<Value>>(exp_value: V, expr: &str) {
Tester::default()
.run(format!("result = {}", expr))
.expect_var("result", exp_value.into())
.check();
}
pub fn check_expr_error<S: Into<String>>(exp_error: S, expr: &str) {
Tester::default().run(format!("result = {}", expr)).expect_err(exp_error).check();
}