From abe785cd89802176bd943e1a5f156a857803b8ed Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Sun, 22 Oct 2023 17:31:36 +0200 Subject: [PATCH] :bug: Integration tests using the interpreter --- src/interpreter/mod.rs | 34 +++++++++----- src/interpreter/test.rs | 2 +- src/lib.rs | 11 +++++ tests/common.rs | 24 ++++++++++ tests/examples.rs | 36 +++------------ tests/print-success/for-loop.mbas | 11 +++++ tests/print-success/gosub.mbas | 26 +++++++++++ tests/print-success/identity.mbas | 1 + tests/print-success/if-jump.mbas | 13 ++++++ tests/print-success/if-then-else.mbas | 7 +++ tests/print-success/while-loop.mbas | 11 +++++ tests/run.rs | 64 +++++++++++++++++++++++++++ 12 files changed, 199 insertions(+), 41 deletions(-) create mode 100644 tests/common.rs create mode 100644 tests/print-success/for-loop.mbas create mode 100644 tests/print-success/gosub.mbas create mode 100644 tests/print-success/identity.mbas create mode 100644 tests/print-success/if-jump.mbas create mode 100644 tests/print-success/if-then-else.mbas create mode 100644 tests/print-success/while-loop.mbas create mode 100644 tests/run.rs diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 8ab19d8..76abeed 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -149,7 +149,6 @@ pub fn run(program: &MindustryProgram, stop_condition: StopCondition) -> HashMap let mut steps = 0; while !stop_condition.should_stop(steps, &state) { - println!("{:?}", state); let _ = step(&compiled, &mut state); steps = steps.saturating_add(1); } @@ -193,15 +192,15 @@ pub fn step( } MindustryOperation::Operator(out_name, operation, lhs, rhs) => { let result = eval_operation(*operation, lhs, rhs, state); - state.variables.insert(out_name.clone(), result); + store_value(out_name, result, state); } MindustryOperation::UnaryOperator(out_name, operation, value) => { let result = eval_unary_operation(*operation, value, state); - state.variables.insert(out_name.clone(), result); + store_value(out_name, result, state); } MindustryOperation::Set(out_name, value) => { let value = eval_operand(value, state); - state.variables.insert(out_name.clone(), value); + store_value(out_name, value, state); } MindustryOperation::End => { state.counter = Counter(0); @@ -260,13 +259,13 @@ pub fn step( fn eval_operand(operand: &Operand, state: &ProgramState) -> Value { match operand { - Operand::Variable(name) => { - if name == "@counter" { - Value::Number(state.counter.0 as f64) - } else { - state.variables.get(name).cloned().unwrap_or_default() - } - } + Operand::Variable(name) => match name.as_str() { + "@counter" => Value::Number(state.counter.0 as f64), + "true" => Value::Number(1.0), + "false" => Value::Number(0.0), + "null" => Value::Null, + other => state.variables.get(other).cloned().unwrap_or_default(), + }, Operand::String(string) => Value::String(string.clone()), Operand::Integer(int) => Value::Number(*int as f64), Operand::Float(float) => Value::Number(*float), @@ -321,3 +320,16 @@ fn eval_unary_operation( UnaryOperator::Not => todo!(), }) } + +// TODO: implement as method? +fn store_value(name: &str, value: Value, state: &mut ProgramState) { + match name { + "@counter" => { + let value: f64 = value.into(); + state.counter = Counter(value as usize); + } + other => { + state.variables.insert(other.to_string(), value); + } + } +} diff --git a/src/interpreter/test.rs b/src/interpreter/test.rs index ef8abe7..68a0e4b 100644 --- a/src/interpreter/test.rs +++ b/src/interpreter/test.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn test_interpret_operation() { +fn test_interprete_operation() { fn run_test( instructions: impl IntoIterator, ) -> HashMap { diff --git a/src/lib.rs b/src/lib.rs index c38c287..cc32b8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,17 @@ pub mod parse; pub mod repr; pub mod translate; +pub fn parse_and_translate( + raw: &str, + config: &config::Config, +) -> Result { + let tokens = parse::tokenize(raw)?; + let ast = parse::build_ast(&tokens, config)?; + let translated = translate::translate_ast(&ast, &mut Default::default(), config); + + Ok(translated) +} + pub mod prelude { pub use crate::config::Config; pub use crate::cursor::Cursor; diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..cd7ec41 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +pub fn read_basic_files(path: impl AsRef) -> impl Iterator { + path.as_ref().read_dir().unwrap().filter_map(|entry| { + let Ok(entry) = entry else { + return None; + }; + + if entry + .file_name() + .into_string() + .map(|name| name.ends_with(".mbas")) + .unwrap_or(false) + { + let file_name = entry.file_name().into_string().unwrap(); + let file = std::fs::read_to_string(entry.path()).unwrap_or_else(|e| { + panic!("Error opening {:?}: {:?}", file_name, e); + }); + Some((file_name, file)) + } else { + None + } + }) +} diff --git a/tests/examples.rs b/tests/examples.rs index 3d7a77b..a7392f2 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,43 +1,21 @@ #![feature(fs_try_exists)] -use std::path::{Path, PathBuf}; +use std::path::PathBuf; + +#[path = "./common.rs"] +mod common; +use common::*; use basic_to_mindustry::optimize::{optimize_constant, optimize_jump_always, optimize_jump_op}; use basic_to_mindustry::parse::{build_ast, tokenize}; use basic_to_mindustry::prelude::*; use basic_to_mindustry::translate::translate_ast; -fn read_basic_examples() -> impl Iterator { - Path::new("./examples/") - .read_dir() - .unwrap() - .filter_map(|entry| { - let Ok(entry) = entry else { - return None; - }; - - if entry - .file_name() - .into_string() - .map(|name| name.ends_with(".mbas")) - .unwrap_or(false) - { - let file_name = entry.file_name().into_string().unwrap(); - let file = std::fs::read_to_string(entry.path()).unwrap_or_else(|e| { - panic!("Error opening {:?}: {:?}", file_name, e); - }); - Some((file_name, file)) - } else { - None - } - }) -} - #[test] fn test_examples() { let config = Config::default(); - for (file_name, file) in read_basic_examples() { + for (file_name, file) in read_basic_files("./examples/") { let tokenized = tokenize(&file).unwrap_or_else(|e| { panic!("Error tokenizing {:?}: {:?}", file_name, e); }); @@ -56,7 +34,7 @@ fn test_examples() { fn test_examples_opt() { let config = Config::default(); - for (file_name, file) in read_basic_examples() { + for (file_name, file) in read_basic_files("./examples/") { let Some(program_name) = PathBuf::from(file_name.clone()) .file_stem() .and_then(|stem| stem.to_str()) diff --git a/tests/print-success/for-loop.mbas b/tests/print-success/for-loop.mbas new file mode 100644 index 0000000..786d9f1 --- /dev/null +++ b/tests/print-success/for-loop.mbas @@ -0,0 +1,11 @@ +LET sum = 0 + +FOR i = 2 TO 5 + sum = sum + i +NEXT i + +IF sum == 14 THEN + PRINT "success" +ELSE + PRINT "fail" +END IF diff --git a/tests/print-success/gosub.mbas b/tests/print-success/gosub.mbas new file mode 100644 index 0000000..eafb51d --- /dev/null +++ b/tests/print-success/gosub.mbas @@ -0,0 +1,26 @@ +LET x = 0 + +GOSUB double +GOSUB add_one +GOSUB double +GOSUB double +GOSUB add_one + +IF x == 5 THEN + PRINT "success" +ELSE + PRINT "fail" +END IF + +END +PRINT "unreachable (after END)\n" + +double: + x = x * 2 + RETURN + PRINT "unreachable (after RETURN in double)\n" + +add_one: + x = x + 1 + RETURN + PRINT "unreachable (after RETURN in add_one)\n" diff --git a/tests/print-success/identity.mbas b/tests/print-success/identity.mbas new file mode 100644 index 0000000..ced4a6c --- /dev/null +++ b/tests/print-success/identity.mbas @@ -0,0 +1 @@ +PRINT "success" diff --git a/tests/print-success/if-jump.mbas b/tests/print-success/if-jump.mbas new file mode 100644 index 0000000..95fb427 --- /dev/null +++ b/tests/print-success/if-jump.mbas @@ -0,0 +1,13 @@ +LET x = 32 + +start: + x = x / 2 + IF x > 1 THEN + GOTO start + END IF + +IF x == 1 THEN + PRINT "success" +ELSE + PRINT "fail" +END IF diff --git a/tests/print-success/if-then-else.mbas b/tests/print-success/if-then-else.mbas new file mode 100644 index 0000000..7ff3f9e --- /dev/null +++ b/tests/print-success/if-then-else.mbas @@ -0,0 +1,7 @@ +LET x = 1 + +IF x > 0 THEN + PRINT "success" +ELSE + PRINT "fail" +END IF diff --git a/tests/print-success/while-loop.mbas b/tests/print-success/while-loop.mbas new file mode 100644 index 0000000..733012e --- /dev/null +++ b/tests/print-success/while-loop.mbas @@ -0,0 +1,11 @@ +LET x = 32 + +WHILE x > 1 + x = x / 2 +END WHILE + +IF x == 1 THEN + PRINT "success" +ELSE + PRINT "fail" +END IF diff --git a/tests/run.rs b/tests/run.rs new file mode 100644 index 0000000..b9fb89e --- /dev/null +++ b/tests/run.rs @@ -0,0 +1,64 @@ +#![feature(fs_try_exists)] + +#[path = "./common.rs"] +mod common; +use common::*; + +use basic_to_mindustry::{ + interpreter::{run, StopCondition, Value}, + optimize::{optimize_constant, optimize_jump_always, optimize_jump_op}, + parse_and_translate, + prelude::MindustryProgram, +}; + +/// Verifies that each test file in the `tests/print-success` test suite return `success` in the `@print` buffer. +#[test] +fn test_interprete_success() { + fn test(program: &MindustryProgram, file_name: &str, opt_name: &str) { + let result = run(&program, StopCondition::End); + + assert_eq!( + result.get("@print"), + Some(&Value::String(String::from("success"))), + "File {file_name} did not print 'success' in the {opt_name} configuration: {:?}", + result.get("@print").cloned().unwrap_or_default() + ); + } + + macro_rules! chain { + ( $( $optimizer:expr ),* ) => { + Box::new(|prog| { + let res = prog; + $( + let res = $optimizer(res); + )* + res + }) + } + } + + let optimizations: Vec<( + &'static str, + Box MindustryProgram>, + )> = vec![ + ("unoptimized", chain!()), + ("constant", chain!(optimize_constant)), + ( + "constant+jump_op", + chain!(optimize_constant, optimize_jump_op), + ), + ( + "level-1", + chain!(optimize_constant, optimize_jump_op, optimize_jump_always), + ), + ]; + + for (file_name, file) in read_basic_files("./tests/print-success/") { + let unoptimized = parse_and_translate(&file, &Default::default()).unwrap(); + + for (opt_name, optimizer) in optimizations.iter() { + let optimized = optimizer(unoptimized.clone()); + test(&optimized, &file_name, opt_name); + } + } +}