use crate::ast::{ArgSep, Expr, Value, VarType};
use crate::eval::{CallableMetadata, CallableMetadataBuilder};
use crate::exec::{self, BuiltinCommand, Machine};
use async_trait::async_trait;
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
#[derive(Clone, Debug)]
pub enum Key {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
Backspace,
CarriageReturn,
Char(char),
Eof,
Escape,
Interrupt,
NewLine,
Unknown(String),
}
#[derive(Debug, Eq, PartialEq)]
pub enum ClearType {
All,
CurrentLine,
UntilNewLine,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Position {
pub row: usize,
pub column: usize,
}
impl std::ops::Sub for Position {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
Position { row: self.row - other.row, column: self.column - other.column }
}
}
#[async_trait(?Send)]
pub trait Console {
fn clear(&mut self, how: ClearType) -> io::Result<()>;
fn color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()>;
fn enter_alt(&mut self) -> io::Result<()>;
fn hide_cursor(&mut self) -> io::Result<()>;
fn is_interactive(&self) -> bool;
fn leave_alt(&mut self) -> io::Result<()>;
fn locate(&mut self, pos: Position) -> io::Result<()>;
fn move_within_line(&mut self, off: i16) -> io::Result<()>;
fn print(&mut self, text: &str) -> io::Result<()>;
async fn read_key(&mut self) -> io::Result<Key>;
fn show_cursor(&mut self) -> io::Result<()>;
fn size(&self) -> io::Result<Position>;
fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
}
async fn read_line_interactive(
console: &mut dyn Console,
prompt: &str,
previous: &str,
) -> io::Result<String> {
let mut line = String::from(previous);
console.clear(ClearType::UntilNewLine)?;
if !prompt.is_empty() || !line.is_empty() {
console.write(format!("{}{}", prompt, line).as_bytes())?;
}
let width = {
let console_size = console.size()?;
console_size.column - prompt.len()
};
let mut pos = line.len();
loop {
match console.read_key().await? {
Key::ArrowUp | Key::ArrowDown => {
}
Key::ArrowLeft => {
if pos > 0 {
console.move_within_line(-1)?;
pos -= 1;
}
}
Key::ArrowRight => {
if pos < line.len() {
console.move_within_line(1)?;
pos += 1;
}
}
Key::Backspace => {
if pos > 0 {
console.hide_cursor()?;
console.move_within_line(-1)?;
console.write(line[pos..].as_bytes())?;
console.write(&[b' '])?;
console.move_within_line(-((line.len() - pos) as i16 + 1))?;
console.show_cursor()?;
line.remove(pos - 1);
pos -= 1;
}
}
Key::CarriageReturn => {
if cfg!(not(target_os = "windows")) {
console.write(&[b'\r', b'\n'])?;
break;
}
}
Key::Char(ch) => {
debug_assert!(line.len() < width);
if line.len() == width - 1 {
continue;
}
if pos < line.len() {
console.hide_cursor()?;
console.write(&[ch as u8])?;
console.write(line[pos..].as_bytes())?;
console.move_within_line(-((line.len() - pos) as i16))?;
console.show_cursor()?;
} else {
console.write(&[ch as u8])?;
}
line.insert(pos, ch);
pos += 1;
}
Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
Key::Escape => {
}
Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
Key::NewLine => {
console.write(&[b'\r', b'\n'])?;
break;
}
Key::Unknown(_) => (),
}
}
Ok(line)
}
async fn read_line_raw(console: &mut dyn Console) -> io::Result<String> {
let mut line = String::new();
loop {
match console.read_key().await? {
Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight => (),
Key::Backspace => {
if !line.is_empty() {
line.pop();
}
}
Key::CarriageReturn => {
if cfg!(not(target_os = "windows")) {
break;
}
}
Key::Char(ch) => line.push(ch),
Key::Escape => (),
Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
Key::NewLine => break,
Key::Unknown(bad_input) => line += &bad_input,
}
}
Ok(line)
}
pub async fn read_line(
console: &mut dyn Console,
prompt: &str,
previous: &str,
) -> io::Result<String> {
if console.is_interactive() {
read_line_interactive(console, prompt, previous).await
} else {
read_line_raw(console).await
}
}
pub struct ClsCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl ClsCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("CLS", VarType::Void)
.with_syntax("")
.with_category("Console manipulation")
.with_description("Clears the screen.")
.build(),
console,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for ClsCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
_machine: &mut Machine,
) -> exec::Result<()> {
if !args.is_empty() {
return exec::new_usage_error("CLS takes no arguments");
}
self.console.borrow_mut().clear(ClearType::All)?;
Ok(())
}
}
pub struct ColorCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl ColorCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("COLOR", VarType::Void)
.with_syntax("[fg%][, [bg%]]")
.with_category("Console manipulation")
.with_description(
"Sets the foreground and background colors.
Color numbers are given as ANSI numbers and can be between 0 and 255. If a color number is not \
specified, then the color is reset to the console's default. The console default does not \
necessarily match any other color specifiable in the 0 to 255 range, as it might be transparent.",
)
.build(),
console,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for ColorCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
let (fg_expr, bg_expr): (&Option<Expr>, &Option<Expr>) = match args {
[] => (&None, &None),
[(fg, ArgSep::End)] => (fg, &None),
[(fg, ArgSep::Long), (bg, ArgSep::End)] => (fg, bg),
_ => {
return exec::new_usage_error(
"COLOR takes at most two arguments separated by a comma",
)
}
};
fn get_color(e: &Option<Expr>, machine: &Machine) -> exec::Result<Option<u8>> {
match e {
Some(e) => match e.eval(machine.get_vars(), machine.get_functions())? {
Value::Integer(i) if i >= 0 && i <= std::u8::MAX as i32 => Ok(Some(i as u8)),
Value::Integer(_) => exec::new_usage_error("Color out of range"),
_ => exec::new_usage_error("Color must be an integer"),
},
None => Ok(None),
}
}
let fg = get_color(fg_expr, machine)?;
let bg = get_color(bg_expr, machine)?;
self.console.borrow_mut().color(fg, bg)?;
Ok(())
}
}
pub struct InputCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl InputCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("INPUT", VarType::Void)
.with_syntax("[\"prompt\"] <;|,> variableref")
.with_category("Console manipulation")
.with_description(
"Obtains user input from the console.
The first expression to this function must be empty or evaluate to a string, and specifies \
the prompt to print. If this first argument is followed by the short `;` separator, the \
prompt is extended with a question mark.
The second expression to this function must be a bare variable reference and indicates the \
variable to update with the obtained input.",
)
.build(),
console,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for InputCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
if args.len() != 2 {
return exec::new_usage_error("INPUT requires two arguments");
}
let mut prompt = match &args[0].0 {
Some(e) => match e.eval(machine.get_vars(), machine.get_functions())? {
Value::Text(t) => t,
_ => return exec::new_usage_error("INPUT prompt must be a string"),
},
None => "".to_owned(),
};
if let ArgSep::Short = args[0].1 {
prompt += "? ";
}
let vref = match &args[1].0 {
Some(Expr::Symbol(vref)) => vref,
_ => return exec::new_usage_error("INPUT requires a variable reference"),
};
let mut console = self.console.borrow_mut();
let mut previous_answer = String::new();
loop {
match read_line(&mut *console, &prompt, &previous_answer).await {
Ok(answer) => match Value::parse_as(vref.ref_type(), answer.trim_end()) {
Ok(value) => {
machine.get_mut_vars().set(vref, value)?;
return Ok(());
}
Err(e) => {
console.print(&format!("Retry input: {}", e))?;
previous_answer = answer;
}
},
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
console.print(&format!("Retry input: {}", e))?
}
Err(e) => return Err(e.into()),
}
}
}
}
pub struct LocateCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl LocateCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("LOCATE", VarType::Void)
.with_syntax("row%, column%")
.with_category("Console manipulation")
.with_description("Moves the cursor to the given position.")
.build(),
console,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for LocateCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
if args.len() != 2 {
return exec::new_usage_error("LOCATE takes two arguments");
}
let (row_arg, column_arg) = (&args[0], &args[1]);
if row_arg.1 != ArgSep::Long {
return exec::new_usage_error("LOCATE expects arguments separated by a comma");
}
debug_assert!(column_arg.1 == ArgSep::End);
let row = match &row_arg.0 {
Some(arg) => match arg.eval(machine.get_vars(), machine.get_functions())? {
Value::Integer(i) => {
if i < 0 {
return exec::new_usage_error("Row cannot be negative");
}
i as usize
}
_ => return exec::new_usage_error("Row must be an integer"),
},
None => return exec::new_usage_error("Row cannot be empty"),
};
let column = match &column_arg.0 {
Some(arg) => match arg.eval(machine.get_vars(), machine.get_functions())? {
Value::Integer(i) => {
if i < 0 {
return exec::new_usage_error("Column cannot be negative");
}
i as usize
}
_ => return exec::new_usage_error("Column must be an integer"),
},
None => return exec::new_usage_error("Column cannot be empty"),
};
self.console.borrow_mut().locate(Position { row, column })?;
Ok(())
}
}
pub struct PrintCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl PrintCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("PRINT", VarType::Void)
.with_syntax("[expr1 [<;|,> .. exprN]]")
.with_category("Console manipulation")
.with_description(
"Prints a message to the console.
The expressions given as arguments are all evaluated and converted to strings. Arguments \
separated by the short `;` separator are concatenated with a single space, while arguments \
separated by the long `,` separator are concatenated with a tab character.",
)
.build(),
console,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for PrintCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
let mut text = String::new();
for arg in args.iter() {
if let Some(expr) = arg.0.as_ref() {
text += &expr.eval(machine.get_vars(), machine.get_functions())?.to_string();
}
match arg.1 {
ArgSep::End => break,
ArgSep::Short => text += " ",
ArgSep::Long => text += "\t",
}
}
self.console.borrow_mut().print(&text)?;
Ok(())
}
}
pub fn all_commands(console: Rc<RefCell<dyn Console>>) -> Vec<Rc<dyn BuiltinCommand>> {
vec![
ClsCommand::new(console.clone()),
ColorCommand::new(console.clone()),
InputCommand::new(console.clone()),
LocateCommand::new(console.clone()),
PrintCommand::new(console),
]
}
#[cfg(test)]
pub(crate) mod testutils {
use super::*;
use std::collections::VecDeque;
use std::io;
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum CapturedOut {
Clear(ClearType),
Color(Option<u8>, Option<u8>),
EnterAlt,
HideCursor,
LeaveAlt,
Locate(Position),
MoveWithinLine(i16),
Print(String),
ShowCursor,
Write(Vec<u8>),
}
pub(crate) struct MockConsole {
golden_in: VecDeque<Key>,
captured_out: Vec<CapturedOut>,
size: Position,
}
impl MockConsole {
pub(crate) fn captured_out(&self) -> &[CapturedOut] {
self.captured_out.as_slice()
}
}
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(())
}
}
pub(crate) struct MockConsoleBuilder {
golden_in: VecDeque<Key>,
size: Position,
}
impl MockConsoleBuilder {
pub(crate) fn new() -> Self {
Self {
golden_in: VecDeque::new(),
size: Position { row: usize::MAX, column: usize::MAX },
}
}
pub(crate) fn add_input_chars(mut self, s: &str) -> Self {
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)),
}
}
self
}
pub(crate) fn add_input_keys(mut self, keys: &[Key]) -> Self {
self.golden_in.extend(keys.iter().cloned());
self
}
pub(crate) fn with_size(mut self, size: Position) -> Self {
self.size = size;
self
}
pub(crate) fn build(self) -> MockConsole {
MockConsole { golden_in: self.golden_in, captured_out: vec![], size: self.size }
}
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
use crate::exec::MachineBuilder;
use futures_lite::future::block_on;
#[must_use]
struct ReadLineInteractiveTest {
keys: Vec<Key>,
prompt: &'static str,
previous: &'static str,
exp_line: &'static str,
exp_output: Vec<CapturedOut>,
}
impl ReadLineInteractiveTest {
fn new() -> Self {
Self {
keys: vec![],
prompt: "",
previous: "",
exp_line: "",
exp_output: vec![CapturedOut::Clear(ClearType::UntilNewLine)],
}
}
fn add_key(mut self, key: Key) -> Self {
self.keys.push(key);
self
}
fn add_key_chars(mut self, chars: &'static str) -> Self {
for ch in chars.chars() {
self.keys.push(Key::Char(ch));
}
self
}
fn add_output(mut self, output: CapturedOut) -> Self {
self.exp_output.push(output);
self
}
fn add_output_bytes(mut self, bytes: &'static [u8]) -> Self {
if bytes.is_empty() {
self.exp_output.push(CapturedOut::Write(vec![]))
} else {
for b in bytes.iter() {
self.exp_output.push(CapturedOut::Write(vec![*b]))
}
}
self
}
fn set_line(mut self, line: &'static str) -> Self {
self.exp_line = line;
self
}
fn set_prompt(mut self, prompt: &'static str) -> Self {
self.prompt = prompt;
self
}
fn set_previous(mut self, previous: &'static str) -> Self {
self.previous = previous;
self
}
fn accept(mut self) {
self.keys.push(Key::NewLine);
self.exp_output.push(CapturedOut::Write(vec![b'\r', b'\n']));
let mut console = MockConsoleBuilder::new()
.add_input_keys(&self.keys)
.with_size(Position { row: 5, column: 15 })
.build();
let line =
block_on(read_line_interactive(&mut console, self.prompt, self.previous)).unwrap();
assert_eq!(self.exp_line, &line);
assert_eq!(self.exp_output.as_slice(), console.captured_out());
}
}
#[test]
fn test_read_line_interactive_empty() {
ReadLineInteractiveTest::new().accept();
ReadLineInteractiveTest::new().add_key(Key::Backspace).accept();
ReadLineInteractiveTest::new().add_key(Key::ArrowLeft).accept();
ReadLineInteractiveTest::new().add_key(Key::ArrowRight).accept();
}
#[test]
fn test_read_line_with_prompt() {
ReadLineInteractiveTest::new()
.set_prompt("Ready> ")
.add_output(CapturedOut::Write(b"Ready> ".to_vec()))
.add_key_chars("hello")
.add_output_bytes(b"hello")
.set_line("hello")
.accept();
ReadLineInteractiveTest::new()
.set_prompt("Cannot delete")
.add_output(CapturedOut::Write(b"Cannot delete".to_vec()))
.add_key(Key::Backspace)
.accept();
}
#[test]
fn test_read_line_interactive_trailing_input() {
ReadLineInteractiveTest::new()
.add_key_chars("hello")
.add_output_bytes(b"hello")
.set_line("hello")
.accept();
ReadLineInteractiveTest::new()
.set_previous("123")
.add_output(CapturedOut::Write(b"123".to_vec()))
.add_key_chars("hello")
.add_output_bytes(b"hello")
.set_line("123hello")
.accept();
}
#[test]
fn test_read_line_interactive_middle_input() {
ReadLineInteractiveTest::new()
.add_key_chars("some text")
.add_output_bytes(b"some text")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key_chars(" ")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(b" ")
.add_output(CapturedOut::Write(b"xt".to_vec()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.add_key_chars(".")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(b".")
.add_output(CapturedOut::Write(b"xt".to_vec()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("some te .xt")
.accept();
}
#[test]
fn test_read_line_interactive_trailing_backspace() {
ReadLineInteractiveTest::new()
.add_key_chars("bar")
.add_output_bytes(b"bar")
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output_bytes(b"")
.add_output_bytes(b" ")
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("zar")
.add_output_bytes(b"zar")
.set_line("bazar")
.accept();
}
#[test]
fn test_read_line_interactive_middle_backspace() {
ReadLineInteractiveTest::new()
.add_key_chars("has a tYpo")
.add_output_bytes(b"has a tYpo")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::Backspace)
.add_output(CapturedOut::HideCursor)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_output(CapturedOut::Write(b"po".to_vec()))
.add_output_bytes(b" ")
.add_output(CapturedOut::MoveWithinLine(-3))
.add_output(CapturedOut::ShowCursor)
.add_key_chars("y")
.add_output(CapturedOut::HideCursor)
.add_output_bytes(b"y")
.add_output(CapturedOut::Write(b"po".to_vec()))
.add_output(CapturedOut::MoveWithinLine(-2))
.add_output(CapturedOut::ShowCursor)
.set_line("has a typo")
.accept();
}
#[test]
fn test_read_line_interactive_test_move_bounds() {
ReadLineInteractiveTest::new()
.set_previous("12")
.add_output(CapturedOut::Write(b"12".to_vec()))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowLeft)
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_output(CapturedOut::MoveWithinLine(1))
.add_key(Key::ArrowRight)
.add_key(Key::ArrowRight)
.add_key_chars("3")
.add_output_bytes(b"3")
.set_line("123")
.accept();
}
#[test]
fn test_read_line_interactive_horizontal_scrolling_not_implemented() {
ReadLineInteractiveTest::new()
.add_key_chars("1234567890123456789")
.add_output_bytes(b"12345678901234")
.set_line("12345678901234")
.accept();
ReadLineInteractiveTest::new()
.add_key_chars("1234567890123456789")
.add_output_bytes(b"12345678901234")
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key(Key::ArrowLeft)
.add_output(CapturedOut::MoveWithinLine(-1))
.add_key_chars("these will all be ignored")
.set_line("12345678901234")
.accept();
ReadLineInteractiveTest::new()
.set_prompt("12345")
.set_previous("67890")
.add_output(CapturedOut::Write(b"1234567890".to_vec()))
.add_key_chars("1234567890")
.add_output_bytes(b"1234")
.set_line("678901234")
.accept();
}
#[test]
fn test_read_line_interactive_history_not_implemented() {
ReadLineInteractiveTest::new().add_key(Key::ArrowUp).accept();
ReadLineInteractiveTest::new().add_key(Key::ArrowDown).accept();
}
#[test]
fn test_read_line_ignored_keys() {
ReadLineInteractiveTest::new()
.add_key_chars("not ")
.add_output_bytes(b"not ")
.add_key(Key::Escape)
.add_key_chars("affected")
.add_output_bytes(b"affected")
.set_line("not affected")
.accept();
}
fn do_control_ok_test(input: &str, golden_in: &'static str, expected_out: &[CapturedOut]) {
let console =
Rc::from(RefCell::from(MockConsoleBuilder::new().add_input_chars(golden_in).build()));
let mut machine =
MachineBuilder::default().add_commands(all_commands(console.clone())).build();
block_on(machine.exec(&mut input.as_bytes())).expect("Execution failed");
assert_eq!(expected_out, console.borrow().captured_out());
}
fn do_ok_test(input: &str, golden_in: &'static str, expected_out: &'static [&'static str]) {
let expected_out: Vec<CapturedOut> =
expected_out.iter().map(|x| CapturedOut::Print((*x).to_owned())).collect();
do_control_ok_test(input, golden_in, &expected_out)
}
fn do_error_test(
input: &str,
golden_in: &'static str,
expected_out: &'static [&'static str],
expected_err: &str,
) {
let console =
Rc::from(RefCell::from(MockConsoleBuilder::new().add_input_chars(golden_in).build()));
let mut machine =
MachineBuilder::default().add_commands(all_commands(console.clone())).build();
assert_eq!(
expected_err,
format!(
"{}",
block_on(machine.exec(&mut input.as_bytes())).expect_err("Execution did not fail")
)
);
let expected_out: Vec<CapturedOut> =
expected_out.iter().map(|x| CapturedOut::Print((*x).to_owned())).collect();
assert_eq!(expected_out, console.borrow().captured_out());
}
fn do_simple_error_test(input: &str, expected_err: &str) {
do_error_test(input, "", &[], expected_err);
}
#[test]
fn test_cls_ok() {
do_control_ok_test("CLS", "", &[CapturedOut::Clear(ClearType::All)]);
}
#[test]
fn test_cls_errors() {
do_simple_error_test("CLS 1", "CLS takes no arguments");
}
#[test]
fn test_color_ok() {
do_control_ok_test("COLOR", "", &[CapturedOut::Color(None, None)]);
do_control_ok_test("COLOR ,", "", &[CapturedOut::Color(None, None)]);
do_control_ok_test("COLOR 1", "", &[CapturedOut::Color(Some(1), None)]);
do_control_ok_test("COLOR 1,", "", &[CapturedOut::Color(Some(1), None)]);
do_control_ok_test("COLOR , 1", "", &[CapturedOut::Color(None, Some(1))]);
do_control_ok_test("COLOR 10, 5", "", &[CapturedOut::Color(Some(10), Some(5))]);
do_control_ok_test("COLOR 0, 0", "", &[CapturedOut::Color(Some(0), Some(0))]);
do_control_ok_test("COLOR 255, 255", "", &[CapturedOut::Color(Some(255), Some(255))]);
}
#[test]
fn test_color_errors() {
do_simple_error_test(
"COLOR 1, 2, 3",
"COLOR takes at most two arguments separated by a comma",
);
do_simple_error_test("COLOR 1000, 0", "Color out of range");
do_simple_error_test("COLOR 0, 1000", "Color out of range");
do_simple_error_test("COLOR TRUE, 0", "Color must be an integer");
do_simple_error_test("COLOR 0, TRUE", "Color must be an integer");
}
#[test]
fn test_input_ok() {
do_ok_test("INPUT ; foo\nPRINT foo", "9\n", &["9"]);
do_ok_test("INPUT ; foo\nPRINT foo", "-9\n", &["-9"]);
do_ok_test("INPUT , bar?\nPRINT bar", "true\n", &["TRUE"]);
do_ok_test("INPUT ; foo$\nPRINT foo", "\n", &[""]);
do_ok_test(
"INPUT \"With question mark\"; a$\nPRINT a$",
"some long text\n",
&["some long text"],
);
do_ok_test(
"prompt$ = \"Indirectly without question mark\"\nINPUT prompt$, b\nPRINT b * 2",
"42\n",
&["84"],
);
}
#[test]
fn test_input_retry() {
do_ok_test("INPUT ; b?", "\ntrue\n", &["Retry input: Invalid boolean literal "]);
do_ok_test("INPUT ; b?", "0\ntrue\n", &["Retry input: Invalid boolean literal 0"]);
do_ok_test("a = 3\nINPUT ; a", "\n7\n", &["Retry input: Invalid integer literal "]);
do_ok_test("a = 3\nINPUT ; a", "x\n7\n", &["Retry input: Invalid integer literal x"]);
}
#[test]
fn test_input_errors() {
do_simple_error_test("INPUT", "INPUT requires two arguments");
do_simple_error_test("INPUT ; ,", "INPUT requires two arguments");
do_simple_error_test("INPUT ;", "INPUT requires a variable reference");
do_simple_error_test("INPUT 3 ; a", "INPUT prompt must be a string");
do_simple_error_test("INPUT ; a + 1", "INPUT requires a variable reference");
do_simple_error_test("INPUT \"a\" + TRUE; b?", "Cannot add Text(\"a\") and Boolean(true)");
}
#[test]
fn test_locate_ok() {
do_control_ok_test(
"LOCATE 0, 0",
"",
&[CapturedOut::Locate(Position { row: 0, column: 0 })],
);
do_control_ok_test(
"LOCATE 1000, 2000",
"",
&[CapturedOut::Locate(Position { row: 1000, column: 2000 })],
);
}
#[test]
fn test_locate_errors() {
do_simple_error_test("LOCATE", "LOCATE takes two arguments");
do_simple_error_test("LOCATE 1", "LOCATE takes two arguments");
do_simple_error_test("LOCATE 1, 2, 3", "LOCATE takes two arguments");
do_simple_error_test("LOCATE 1; 2", "LOCATE expects arguments separated by a comma");
do_simple_error_test("LOCATE -1, 2", "Row cannot be negative");
do_simple_error_test("LOCATE TRUE, 2", "Row must be an integer");
do_simple_error_test("LOCATE , 2", "Row cannot be empty");
do_simple_error_test("LOCATE 1, -2", "Column cannot be negative");
do_simple_error_test("LOCATE 1, TRUE", "Column must be an integer");
do_simple_error_test("LOCATE 1,", "Column cannot be empty");
}
#[test]
fn test_print_ok() {
do_ok_test("PRINT", "", &[""]);
do_ok_test("PRINT ;", "", &[" "]);
do_ok_test("PRINT ,", "", &["\t"]);
do_ok_test("PRINT ;,;,", "", &[" \t \t"]);
do_ok_test("PRINT 3", "", &["3"]);
do_ok_test("PRINT 3 = 5", "", &["FALSE"]);
do_ok_test("PRINT true;123;\"foo bar\"", "", &["TRUE 123 foo bar"]);
do_ok_test("PRINT 6,1;3,5", "", &["6\t1 3\t5"]);
do_ok_test(
"word = \"foo\"\nPRINT word, word\nPRINT word + \"s\"",
"",
&["foo\tfoo", "foos"],
);
}
#[test]
fn test_print_errors() {
do_simple_error_test("PRINT a b", "Unexpected value in expression");
do_simple_error_test("PRINT 3 + TRUE", "Cannot add Integer(3) and Boolean(true)");
}
}