use crate::console::{self, ClearType, Console, Key, Position};
use crate::gpio;
use crate::store::{InMemoryStore, Program, Store};
use async_trait::async_trait;
use endbasic_core::ast::{Value, 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;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CapturedOut {
Clear(ClearType),
Color(Option<u8>, Option<u8>),
EnterAlt,
HideCursor,
LeaveAlt,
Locate(Position),
MoveWithinLine(i16),
Print(String),
ShowCursor,
Write(Vec<u8>),
}
pub struct MockConsole {
golden_in: VecDeque<Key>,
captured_out: Vec<CapturedOut>,
size: Position,
}
impl Default for MockConsole {
fn default() -> Self {
Self {
golden_in: VecDeque::new(),
captured_out: vec![],
size: Position { row: std::usize::MAX, column: std::usize::MAX },
}
}
}
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()
}
pub fn set_size(&mut self, size: Position) {
self.size = size;
}
}
impl Drop for MockConsole {
fn drop(&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(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
self.captured_out.push(CapturedOut::Color(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 {
false
}
fn leave_alt(&mut self) -> io::Result<()> {
self.captured_out.push(CapturedOut::LeaveAlt);
Ok(())
}
fn locate(&mut self, pos: Position) -> io::Result<()> {
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<()> {
self.captured_out.push(CapturedOut::Print(text.to_owned()));
Ok(())
}
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(&self) -> io::Result<Position> {
Ok(self.size)
}
fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
self.captured_out.push(CapturedOut::Write(bytes.to_owned()));
Ok(())
}
}
#[derive(Default)]
pub struct RecordedProgram {
content: String,
}
#[async_trait(?Send)]
impl Program for RecordedProgram {
async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
let append = console::read_line(console, "", "").await?;
self.content.push_str(&append);
self.content.push('\n');
Ok(())
}
fn load(&mut self, text: &str) {
self.content = text.to_owned();
}
fn text(&self) -> String {
self.content.clone()
}
}
#[must_use]
pub struct Tester {
console: Rc<RefCell<MockConsole>>,
store: Rc<RefCell<InMemoryStore>>,
program: Rc<RefCell<RecordedProgram>>,
machine: Machine,
}
impl Default for Tester {
fn default() -> Self {
let console = Rc::from(RefCell::from(MockConsole::default()));
let store = Rc::from(RefCell::from(InMemoryStore::default()));
let program = Rc::from(RefCell::from(RecordedProgram::default()));
let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default()));
let machine = crate::MachineBuilder::default()
.with_console(console.clone())
.with_gpio_pins(gpio_pins)
.make_interactive()
.with_store(store.clone())
.with_program(program.clone())
.build()
.unwrap();
Self { console, store, program, machine }
}
}
impl Tester {
pub fn from(machine: Machine) -> Self {
let console = Rc::from(RefCell::from(MockConsole::default()));
let store = Rc::from(RefCell::from(InMemoryStore::default()));
let program = Rc::from(RefCell::from(RecordedProgram::default()));
Self { console, store, 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 get_machine(&mut self) -> &mut Machine {
&mut self.machine
}
pub fn get_console(&self) -> Rc<RefCell<MockConsole>> {
self.console.clone()
}
pub fn get_store(&self) -> Rc<RefCell<InMemoryStore>> {
self.store.clone()
}
pub fn get_program(&self) -> Rc<RefCell<RecordedProgram>> {
self.program.clone()
}
pub fn set_program(self, text: &str) -> Self {
assert!(!text.is_empty());
{
let mut program = self.program.borrow_mut();
assert!(program.text().is_empty());
program.load(text);
}
self
}
pub fn write_file(self, name: &str, content: &str) -> Self {
self.store.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_store: HashMap<String, String>,
exp_program: 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_store: HashMap::default(),
exp_program: 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 {
assert_eq!(Ok(StopReason::Eof), self.exp_result);
self.exp_result = Err(message.into());
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_file<N: Into<String>, C: Into<String>>(mut self, name: N, content: C) -> Self {
let name = name.into();
assert!(!self.exp_store.contains_key(&name));
self.exp_store.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<S: Into<String>>(mut self, text: S) -> Self {
assert!(self.exp_program.is_empty());
let text = text.into();
assert!(!text.is_empty());
self.exp_program = 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
}
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());
}
}
}
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, self.tester.program.borrow().text());
assert_eq!(self.exp_store, *self.tester.store.borrow().as_hashmap());
}
}
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_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();
}