From 0dda91cfdfcafd65f985fa83f59fe0183374c70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20HUBERT?= Date: Tue, 9 Apr 2024 15:51:27 +0000 Subject: [PATCH] add executor --- soft65c02_tester/src/commands.rs | 143 ++++++++++++------------- soft65c02_tester/src/executor.rs | 160 ++++++++++++++++++++++++++++ soft65c02_tester/src/lib.rs | 2 + soft65c02_tester/src/pest_parser.rs | 37 ++++++- soft65c02_tester/tests/executor.rs | 53 +++++++++ 5 files changed, 319 insertions(+), 76 deletions(-) create mode 100644 soft65c02_tester/src/executor.rs create mode 100644 soft65c02_tester/tests/executor.rs diff --git a/soft65c02_tester/src/commands.rs b/soft65c02_tester/src/commands.rs index c73efcc..4304cdc 100644 --- a/soft65c02_tester/src/commands.rs +++ b/soft65c02_tester/src/commands.rs @@ -1,10 +1,18 @@ -use anyhow::anyhow; -use soft65c02_lib::{execute_step, AddressableIO, Memory, Registers}; +use soft65c02_lib::{execute_step, AddressableIO, LogLine, Memory, Registers}; use crate::{until_condition::BooleanExpression, AppResult}; +#[derive(Debug)] +pub enum OutputToken { + Assertion { success: bool, description: String }, + Marker { description: String }, + None, + Run { loglines: Vec }, + Setup(Vec), +} + pub trait Command { - fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult>; + fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult; } #[derive(Debug)] @@ -18,12 +26,14 @@ pub enum CliCommand { } impl Command for CliCommand { - fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult> { + fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult { match self { Self::Assert(command) => command.execute(registers, memory), - Self::Marker(comment) => Ok(vec![comment.to_owned()]), + Self::Marker(comment) => Ok(OutputToken::Marker { + description: comment.to_owned(), + }), Self::Memory(command) => command.execute(registers, memory), - Self::None => Ok(Vec::new()), + Self::None => Ok(OutputToken::None), Self::Registers(command) => command.execute(registers, memory), Self::Run(command) => command.execute(registers, memory), } @@ -37,12 +47,13 @@ pub struct AssertCommand { } impl Command for AssertCommand { - fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult> { - if self.condition.solve(registers, memory) { - Ok(vec![self.comment.clone()]) - } else { - Err(anyhow!(self.comment.clone())) - } + fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult { + let token = OutputToken::Assertion { + success: self.condition.solve(registers, memory), + description: self.comment.to_owned(), + }; + + Ok(token) } } @@ -53,20 +64,16 @@ pub struct RunCommand { } impl Command for RunCommand { - fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult> { + fn execute(&self, registers: &mut Registers, memory: &mut Memory) -> AppResult { if let Some(addr) = self.start_address { registers.command_pointer = addr; } - let mut loglines: Vec = Vec::new(); + let mut loglines: Vec = Vec::new(); let mut cp = registers.command_pointer; loop { - loglines.push( - execute_step(registers, memory) - .map(|l| l.to_string()) - .map_err(|e| anyhow!(e))?, - ); + loglines.push(execute_step(registers, memory)?); if registers.command_pointer == cp || self.stop_condition.solve(registers, memory) { break; @@ -74,7 +81,9 @@ impl Command for RunCommand { cp = registers.command_pointer; } - Ok(loglines) + let token = OutputToken::Run { loglines }; + + Ok(token) } } @@ -84,10 +93,10 @@ pub enum RegisterCommand { } impl Command for RegisterCommand { - fn execute(&self, registers: &mut Registers, _memory: &mut Memory) -> AppResult> { + fn execute(&self, registers: &mut Registers, _memory: &mut Memory) -> AppResult { registers.initialize(0x0000); - Ok(Vec::new()) + Ok(OutputToken::Setup(vec!["registers flushed".to_string()])) } } @@ -99,7 +108,7 @@ pub enum MemoryCommand { } impl Command for MemoryCommand { - fn execute(&self, _registers: &mut Registers, memory: &mut Memory) -> AppResult> { + fn execute(&self, _registers: &mut Registers, memory: &mut Memory) -> AppResult { let output = match self { Self::Flush => { *memory = Memory::new_with_ram(); @@ -119,7 +128,7 @@ impl Command for MemoryCommand { _ => todo!(), }; - Ok(output) + Ok(OutputToken::Setup(output)) } } @@ -135,25 +144,27 @@ mod assert_command_tests { }; let mut registers = Registers::new(0x0000); let mut memory = Memory::new_with_ram(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - match command.execute(&mut registers, &mut memory) { - Ok(s) => assert_eq!("nice comment", s[0]), - Err(_) => panic!("This condition must be valid."), - }; + assert!( + matches!(token, OutputToken::Assertion { success, description } if success && description == *"nice comment") + ); } #[test] fn test_assert_command_fails() { let command = AssertCommand { condition: BooleanExpression::Value(false), - comment: "nice comment".to_string(), + comment: "failing assertion".to_string(), }; let mut registers = Registers::new(0x0000); let mut memory = Memory::new_with_ram(); - command - .execute(&mut registers, &mut memory) - .expect_err("This condition must fail."); + let token = command.execute(&mut registers, &mut memory).unwrap(); + + assert!( + matches!(token, OutputToken::Assertion { success, description } if ! success && description == *"failing assertion") + ); } } @@ -174,9 +185,9 @@ mod run_command_tests { let mut registers = Registers::new_initialized(0x1000); let mut memory = Memory::new_with_ram(); memory.write(0x1000, &[0xa9, 0xc0]).unwrap(); // LDA #0xc0 - let loglines = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!(1, loglines.len()); + assert!(matches!(token, OutputToken::Run { loglines } if loglines.len() == 1)); } #[test] @@ -188,9 +199,9 @@ mod run_command_tests { let mut registers = Registers::new_initialized(0x0000); let mut memory = Memory::new_with_ram(); memory.write(0x1234, &[0xa9, 0xc0]).unwrap(); // LDA #0xc0 - let loglines = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!(1, loglines.len()); + assert!(matches!(token, OutputToken::Run { loglines } if loglines.len() == 1)); } #[test] @@ -201,10 +212,10 @@ mod run_command_tests { }; let mut registers = Registers::new_initialized(0x1000); let mut memory = Memory::new_with_ram(); - //memory.write(0x1234, &[0xa9, 0xc0, 0xaa]).unwrap(); // LDA #0xc0; TXA - let loglines = command.execute(&mut registers, &mut memory).unwrap(); + memory.write(0x1234, &[0xa9, 0xc0, 0xaa]).unwrap(); // LDA #0xc0; TXA + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!(2, loglines.len()); + assert!(matches!(token, OutputToken::Run { loglines } if loglines.len() == 2)); } #[test] @@ -216,9 +227,9 @@ mod run_command_tests { let mut registers = Registers::new_initialized(0x1000); let mut memory = Memory::new_with_ram(); memory.write(0x1000, &[0xd0, 0b11111110]).unwrap(); // BNE -1 - let loglines = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!(1, loglines.len()); + assert!(matches!(token, OutputToken::Run { loglines } if loglines.len() == 1)) } } @@ -227,13 +238,13 @@ mod register_command_tests { use super::*; #[test] - fn test_show() { + fn test_flush() { let command = RegisterCommand::Flush; let mut registers = Registers::new_initialized(0xffff); let mut memory = Memory::new_with_ram(); - let output = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!(0, output.len()); + assert!(matches!(token, OutputToken::Setup(s) if s[0] == *"registers flushed")); assert_eq!(0x0000, registers.command_pointer); } } @@ -250,10 +261,10 @@ mod memory_command_tests { let mut registers = Registers::new_initialized(0x0000); let mut memory = Memory::new_with_ram(); memory.write(0x0000, &[0x01, 0x02, 0x03]).unwrap(); - let output = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); assert_eq!(vec![0x00, 0x00, 0x00], memory.read(0x000, 3).unwrap()); - assert_eq!(0, output.len()); + assert!(matches!(token, OutputToken::Setup(s) if s.len() == 0)); } #[test] @@ -264,9 +275,9 @@ mod memory_command_tests { }; let mut registers = Registers::new_initialized(0x0000); let mut memory = Memory::new_with_ram(); - let outputs = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!("3 bytes written", (outputs[0])); + assert!(matches!(token, OutputToken::Setup(v) if v[0] == *"3 bytes written")); assert_eq!( &[0x01, 0x02, 0x03], memory.read(0x1000, 3).unwrap().as_slice() @@ -281,9 +292,9 @@ mod memory_command_tests { }; let mut registers = Registers::new_initialized(0x0000); let mut memory = Memory::new_with_ram(); - let outputs = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!("nothing was written", (outputs[0])); + assert!(matches!(token, OutputToken::Setup(s) if s[0] == *"nothing was written")); assert_eq!( &[0x00, 0x00, 0x00], memory.read(0x1000, 3).unwrap().as_slice() @@ -298,9 +309,9 @@ mod memory_command_tests { }; let mut registers = Registers::new_initialized(0x0000); let mut memory = Memory::new_with_ram(); - let outputs = command.execute(&mut registers, &mut memory).unwrap(); + let token = command.execute(&mut registers, &mut memory).unwrap(); - assert_eq!("1 byte written", (outputs[0])); + assert!(matches!(token, OutputToken::Setup(s) if s[0] == *"1 byte written")); assert_eq!( &[0x01, 0x00, 0x00], memory.read(0x1000, 3).unwrap().as_slice() @@ -319,12 +330,14 @@ mod cli_command_tests { let mut registers = Registers::new(0x0000); let mut memory = Memory::new_with_ram(); - let output = CliCommandParser::from("assert #0x0000 = 0x00 $$The first byte is zero$$") + let token = CliCommandParser::from("assert #0x0000 = 0x00 $$The first byte is zero$$") .unwrap() .execute(&mut registers, &mut memory) .unwrap(); - assert_eq!("The first byte is zero".to_string(), output[0]); + assert!( + matches!(token, OutputToken::Assertion { success, description } if success && description == *"The first byte is zero") + ); } #[test] @@ -332,28 +345,14 @@ mod cli_command_tests { let mut registers = Registers::new(0x0000); let mut memory = Memory::new_with_ram(); - let output = + let token = CliCommandParser::from("assert #0x0000 = 0x01 $$The first byte is one, really?$$") .unwrap() .execute(&mut registers, &mut memory) - .unwrap_err(); + .unwrap(); - assert_eq!( - "The first byte is one, really?".to_string(), - output.to_string() + assert!( + matches!(token, OutputToken::Assertion { success, description } if !success && description == *"The first byte is one, really?") ); } - - #[test] - fn test_register_flush() { - let mut registers = Registers::new(0x1234); - let mut memory = Memory::new_with_ram(); - - let output = CliCommandParser::from("registers flush") - .unwrap() - .execute(&mut registers, &mut memory) - .unwrap(); - - assert_eq!(0, output.len()); - } } diff --git a/soft65c02_tester/src/executor.rs b/soft65c02_tester/src/executor.rs new file mode 100644 index 0000000..4ae2d90 --- /dev/null +++ b/soft65c02_tester/src/executor.rs @@ -0,0 +1,160 @@ +use std::sync::mpsc::Sender; + +use anyhow::anyhow; +use soft65c02_lib::{Memory, Registers}; + +use crate::{AppResult, CliCommand, CliCommandParser, Command, OutputToken}; + +#[derive(Debug)] +struct ExecutionRound { + registers: Registers, + memory: Memory, +} + +impl Default for ExecutionRound { + fn default() -> Self { + let registers = Registers::new_initialized(0); + let memory = Memory::new_with_ram(); + + Self { registers, memory } + } +} + +impl ExecutionRound { + fn get_mut(&mut self) -> (&mut Registers, &mut Memory) { + (&mut self.registers, &mut self.memory) + } +} + +#[derive(Debug, Default)] +pub struct Executor { + commands: Vec, +} + +impl Executor { + /// Constructor + pub fn new(lines: &[&str]) -> AppResult { + let commands: Vec = lines + .iter() + .map(|line| CliCommandParser::from(line).map_err(|e| anyhow!(e))) + .collect::>>()?; + + let myself = Self { commands }; + + Ok(myself) + } + + pub fn run(self, sender: Sender) -> AppResult<()> { + let mut round = ExecutionRound::default(); + + for command in self.commands { + if matches!(command, CliCommand::None) { + continue; + } + let (registers, memory) = round.get_mut(); + let token = command.execute(registers, memory)?; + + if matches!(token, OutputToken::Marker { description: _ }) { + round = ExecutionRound::default(); + } + + sender.send(token)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::mpsc::channel; + + use super::*; + + #[test] + fn test_constructor_err() { + let lines = &["marker $$first thing$$", "azerty"]; + let message = Executor::new(lines).unwrap_err().to_string(); + + assert!(message.contains("azerty")); + } + + #[test] + fn test_constructor_ok() { + let lines = &["marker $$first thing$$", "registers flush"]; + + let _executor = Executor::new(lines).unwrap(); + } + + #[test] + fn test_execution_ok_without_initial_marker() { + let lines = &[ + "memory write #0x0800 0x(a9,c0)", // LDA $c0 + "run #0x0800", + "assert A=0xc0 $$accumulator is loaded$$", + ]; + let (sender, receiver) = channel::(); + let executor = Executor::new(lines).unwrap(); + executor.run(sender).unwrap(); + + let output = receiver.recv().unwrap(); + assert!(matches!(output, OutputToken::Setup(_))); + + let output = receiver.recv().unwrap(); + assert!(matches!(output, OutputToken::Run { loglines } if loglines.len() == 1)); + + let output = receiver.recv().unwrap(); + assert!( + matches!(output, OutputToken::Assertion { success, description } if success && description == *"accumulator is loaded") + ); + + assert!(receiver.recv().is_err()); + } + + #[test] + fn test_execution_ok_with_initial_marker() { + let lines = &[ + "marker $$load accumulator$$", + "memory write #0x0800 0x(a9,c0)", // LDA $c0 + "run #0x0800", + "assert A=0xc0 $$accumulator is loaded$$", + ]; + let (sender, receiver) = channel::(); + let executor = Executor::new(lines).unwrap(); + executor.run(sender).unwrap(); + + let output = receiver.recv().unwrap(); + assert!( + matches!(output, OutputToken::Marker { description } if description == *"load accumulator") + ); + + let output = receiver.recv().unwrap(); + assert!(matches!(output, OutputToken::Setup(_))); + + let output = receiver.recv().unwrap(); + assert!(matches!(output, OutputToken::Run { loglines } if loglines.len() == 1)); + + let output = receiver.recv().unwrap(); + assert!( + matches!(output, OutputToken::Assertion { success, description } if success && description == *"accumulator is loaded") + ); + + assert!(receiver.recv().is_err()); + } + + #[test] + fn test_with_blank_lines() { + let lines = &[ + "memory write #0x0800 0x(a9,c0)", // LDA $c0 + "", + "run #0x0800", + " ", + "assert A=0xc0 $$accumulator is loaded$$", + ]; + let (sender, receiver) = channel::(); + let executor = Executor::new(lines).unwrap(); + executor.run(sender).unwrap(); + + assert_eq!(3, receiver.iter().count()); + } +} diff --git a/soft65c02_tester/src/lib.rs b/soft65c02_tester/src/lib.rs index eb83b53..9b00e66 100644 --- a/soft65c02_tester/src/lib.rs +++ b/soft65c02_tester/src/lib.rs @@ -1,8 +1,10 @@ mod commands; +mod executor; mod pest_parser; mod until_condition; pub use commands::*; +pub use executor::Executor; pub use pest_parser::CliCommandParser; pub type AppResult = anyhow::Result; diff --git a/soft65c02_tester/src/pest_parser.rs b/soft65c02_tester/src/pest_parser.rs index a687b84..d41c64c 100644 --- a/soft65c02_tester/src/pest_parser.rs +++ b/soft65c02_tester/src/pest_parser.rs @@ -121,7 +121,7 @@ pub struct RunCommandParser; impl RunCommandParser { pub fn from_pairs(pairs: Pairs<'_, Rule>) -> AppResult { let mut start_address = None; - let mut stop_condition = BooleanExpression::Value(false); + let mut stop_condition = BooleanExpression::Value(true); for pair in pairs { match pair.as_rule() { @@ -157,7 +157,7 @@ mod run_command_parser_tests { .into_inner(); let command = RunCommandParser::from_pairs(pairs).unwrap(); - assert!(matches!(command.stop_condition, BooleanExpression::Value(v) if !v)); + assert!(matches!(command.stop_condition, BooleanExpression::Value(v) if v)); assert!(command.start_address.is_none()); } @@ -167,7 +167,7 @@ mod run_command_parser_tests { let mut parser = PestParser::parse(Rule::run_instruction, input).unwrap(); let command = RunCommandParser::from_pairs(parser.next().unwrap().into_inner()).unwrap(); - assert!(matches!(command.stop_condition, BooleanExpression::Value(v) if !v)); + assert!(matches!(command.stop_condition, BooleanExpression::Value(v) if v)); assert!(matches!(command.start_address, Some(addr) if addr == 0x1234)); } @@ -227,9 +227,22 @@ pub struct CliCommandParser; impl CliCommandParser { pub fn from(line: &str) -> AppResult { + let line = line.trim(); + + if line.is_empty() { + return Ok(CliCommand::None); + } + let pair = PestParser::parse(Rule::sentence, line)? .next() - .expect("There is only one sentence per input.") + .expect("There is only one sentence per input."); + + // comments are ignored + if pair.as_rule() == Rule::EOI { + return Ok(CliCommand::None); + } + + let pair = pair .into_inner() .next() .expect("There is only one instruction per sentence."); @@ -267,6 +280,15 @@ impl CliCommandParser { mod cli_command_parser_test { use super::*; + #[test] + fn test_empty_input() { + let cli_command = CliCommandParser::from("").unwrap(); + assert!(matches!(cli_command, CliCommand::None)); + + let cli_command = CliCommandParser::from(" ").unwrap(); + assert!(matches!(cli_command, CliCommand::None)); + } + #[test] fn test_run_cli_parser() { let cli_command = CliCommandParser::from("run #0x1aff until X = 0xff").unwrap(); @@ -322,6 +344,13 @@ mod cli_command_parser_test { }) if address == 0x1234 && bytes == vec![0x12, 0x23, 0x34, 0x45] )); } + + #[test] + fn test_code_comments() { + let cli_command = CliCommandParser::from("// This is a comment").unwrap(); + + assert!(matches!(cli_command, CliCommand::None)); + } } fn parse_memory(addr: &str) -> AppResult { diff --git a/soft65c02_tester/tests/executor.rs b/soft65c02_tester/tests/executor.rs new file mode 100644 index 0000000..e96b901 --- /dev/null +++ b/soft65c02_tester/tests/executor.rs @@ -0,0 +1,53 @@ +use std::{sync::mpsc::channel, thread::spawn}; + +use soft65c02_tester::{Executor, OutputToken}; + +#[test] +fn test_script() { + let script = r#"marker $$first test$$ +// main program +memory write #0x0800 0x(a9,c0,aa,e8,69,14,00,3a,d5,20,d0,fe,db) + +// interrupt subroutine +memory write #0x8000 0x(95,20,40) + +// set init vector +memory write #0xfffc 0x(00,08) + +// set interrupt vector +memory write #0xfffe 0x(00,80) + +// test +run #0x1000 until CP=0x8000 +assert A=0xc0 $$accumulator is loaded$$ +run until CP=0x080A +run +assert CP=0x080C $$command pointer points at EOP$$"#; + let lines: Vec<&str> = script.split('\n').collect(); + let executor = Executor::new(&lines).unwrap(); + let (sender, receiver) = channel::(); + let handler = spawn(move || { + let mut i: u32 = 0; + + while let Ok(token) = receiver.recv() { + match token { + OutputToken::Assertion { + success, + description, + } => { + i += 1; + println!( + "{i:02} → {description} {}", + if success { "✅" } else { "❌" } + ); + } + OutputToken::Marker { description } => { + println!("♯ {description}") + } + _ => {} + } + } + }); + executor.run(sender).unwrap(); + handler.join().unwrap(); +}