diff --git a/products/bluebell/core/src/lib.rs b/products/bluebell/core/src/lib.rs index 139ebbc36..dd00ed789 100644 --- a/products/bluebell/core/src/lib.rs +++ b/products/bluebell/core/src/lib.rs @@ -13,3 +13,4 @@ pub mod evm_bytecode_generator; // pub mod llvm_ir_generator; pub mod parser; pub mod support; +pub mod testing; diff --git a/products/bluebell/core/src/testing.rs b/products/bluebell/core/src/testing.rs new file mode 100644 index 000000000..9e9e7cca3 --- /dev/null +++ b/products/bluebell/core/src/testing.rs @@ -0,0 +1,173 @@ +use crate::support::evm::EvmCompiler; +use crate::support::modules::ScillaDebugBuiltins; +use crate::support::modules::ScillaDefaultBuiltins; +use crate::support::modules::ScillaDefaultTypes; +use evm_assembly::executable::EvmExecutable; +use evm_assembly::observable_machine::ObservableMachine; +use evm_assembly::types::EvmTypeValue; +use primitive_types::H256; +use std::str::FromStr; + +use serde_json; +use std::rc::Rc; + +pub fn create_vm_and_run_code( + function_name: &str, + args: &str, + source: String, + initial_storage: &str, +) -> (ObservableMachine, EvmExecutable) { + let mut compiler = EvmCompiler::new(); + compiler.pass_manager_mut().enable_debug_printer(); + + let default_types = ScillaDefaultTypes {}; + let default_builtins = ScillaDefaultBuiltins {}; + let debug = ScillaDebugBuiltins {}; + + compiler.attach(&default_types); + compiler.attach(&default_builtins); + compiler.attach(&debug); + + let executor = compiler + .executable_from_script(source) + .expect("Failed to compile source"); + + let code = executor.executable.bytecode.clone(); + let arguments: Vec = if args == "" { + [].to_vec() + } else { + serde_json::from_str(&args).expect("Failed to deserialize arguments") + }; + + let data: Rc> = { + executor + .context + .get_function(&function_name) + .expect(&format!("Function name {} not found", function_name).to_string()) + .generate_transaction_data(arguments) + .into() + }; + + let source_map = executor.executable.get_source_map(); + let mut vm = ObservableMachine::new(Rc::new(code), data, 1024, 10000, None); + vm.set_source_map(&source_map); + + let initial_storage: Vec<&str> = initial_storage.lines().collect(); + for record in initial_storage { + let record = record.trim_start(); + if record.len() == 0 { + continue; + } + let parts: Vec<&str> = record.split(":").map(|s| s.trim()).collect(); + if parts.len() != 2 { + panic!( + "Invalid storage record format. Expected 'key: value', got '{}'", + record + ); + } + + let key = H256::from_str(&format_hex_string(parts[0]).expect("Invalid key")) + .expect(&format!("Failed to parse key from '{}'", parts[0])); + let value = H256::from_str(&format_hex_string(parts[1]).expect("Invalid value")) + .expect(&format!("Failed to parse value from '{}'", parts[1])); + + vm.storage.insert(key, value); + } + + vm.run(); + + (vm, executor.executable) +} + +fn format_hex_string(input: &str) -> Result { + if !input.starts_with("0x") { + return Err("String does not start with 0x prefix"); + } + + if input.contains("...") { + // 66 = 2 for "0x" + 64 for H256 + let required_zeros = 66 - input.len() + 3; // +3 for the length of "..." + + if required_zeros < 0 { + return Err("Input string is too long to be a valid H256 value."); + } + + let zeros = "0".repeat(required_zeros); + Ok(input.replace("...", &zeros)) + } else { + Ok(input.to_string()) + } +} + +pub fn test_execution_path( + entry: &str, + args: &str, + source: &str, + initial_storage: &str, + storage_checks: &str, +) { + // 1. Extract lines with *:> and their line numbers + let lines: Vec<&str> = source.lines().collect(); + let mut expected_visited_lines = Vec::new(); + let mut expected_not_visited_lines = Vec::new(); + let mut cleaned_code = String::new(); + + for (index, line) in lines.iter().enumerate() { + if line.starts_with("--|") { + expected_not_visited_lines.push(index); + cleaned_code.push_str(&line.replace("--|", "")); + } else if line.starts_with("-->") { + expected_visited_lines.push(index); + cleaned_code.push_str(&line.replace("-->", "")); + } else { + cleaned_code.push_str(line); + } + cleaned_code.push('\n'); + } + + // 2. Compile and run the cleaned code + let (vm, _executable) = create_vm_and_run_code(entry, args, cleaned_code, initial_storage); + + // 3. Check if the expected lines were visited + for line in &expected_visited_lines { + if vm.did_not_visit_line((*line).try_into().unwrap()) { + println!("{:#?}", vm.lines_visited); + panic!("Expected line {} to be visited but it wasn't.", line); + } + } + + for line in &expected_not_visited_lines { + if vm.did_visit_line((*line).try_into().unwrap()) { + println!("{:#?}", vm.lines_visited); + panic!("Expected line {} to be not visited but it was.", line); + } + } + + // 4. Check if the VM's storage contains the expected key-value pairs + let storage_checks: Vec<&str> = storage_checks.lines().collect(); + for check in storage_checks { + let check = check.trim_start(); + if check.len() == 0 { + continue; + } + let parts: Vec<&str> = check.split(":").map(|s| s.trim()).collect(); + if parts.len() != 2 { + panic!( + "Invalid storage check format. Expected 'key: value', got '{}'", + check + ); + } + + let key = H256::from_str(&format_hex_string(parts[0]).expect("Invalid key")) + .expect(&format!("Failed to parse key from '{}'", parts[0])); + let value = H256::from_str(&format_hex_string(parts[1]).expect("Invalid value")) + .expect(&format!("Failed to parse value from '{}'", parts[1])); + + if !vm.has_record(&key, &value) { + panic!( + "Storage check failed. Key '{}' does not have expected value '{}'.", + key, value + ); + } + } +} diff --git a/products/bluebell/core/tests/evm_line_visit_test.rs b/products/bluebell/core/tests/evm_line_visit_test.rs new file mode 100644 index 000000000..a21d20d95 --- /dev/null +++ b/products/bluebell/core/tests/evm_line_visit_test.rs @@ -0,0 +1,67 @@ +#[cfg(test)] +mod tests { + use bluebell::testing::test_execution_path; + + #[test] + fn test_visiting() { + test_execution_path( + "HelloWorld::setHello", + "[42]", + r#" +--| scilla_version 0 +--| +--| contract HelloWorld() +--| +--> transition setHello () +--> msg = Uint64 12; +--> x = builtin print__impl msg; +--> y = builtin print__impl msg +--| end +"#, + "", + "", + ); + + test_execution_path( + "HelloWorld::setHello", + "[42]", + r#" +--| scilla_version 0 +--| +--| library HelloWorld +--| contract HelloWorld() +--| field welcome_msg : Uint64 = Uint64 0 +--| +--> transition setHello (x: Uint64) +--> welcome_msg := x; +--| y <- welcome_msg (* TODO: Source map not correctly generated here *) +--| end + +"#, + "0x00...1337:0x00...2c", + "0x00...1337:0x00...2a", + ); + + test_execution_path( + "HelloWorld::setHello", + "[42]", + r#" +--| scilla_version 0 +--| +--| library HelloWorld +--| contract HelloWorld() +--| field welcome_msg : Uint64 = Uint64 0 +--| +--> transition setHello (x: Uint64) +--> welcome_msg := x; +--| y <- welcome_msg (* TODO: Source map not correctly generated here *) +--| end + +"#, + "0x00...1338:0x00...2c", + r#"0x00...1338:0x00...2c + 0x00...1337:0x00...2a + "#, + ); + } +} diff --git a/products/bluebell/evm_assembly/src/observable_machine.rs b/products/bluebell/evm_assembly/src/observable_machine.rs index 301e61d1f..c01e3679b 100644 --- a/products/bluebell/evm_assembly/src/observable_machine.rs +++ b/products/bluebell/evm_assembly/src/observable_machine.rs @@ -277,4 +277,12 @@ impl ObservableMachine { pub fn did_not_visit_line(&self, pc: u32) -> bool { None == self.lines_visited.get(&pc) } + + pub fn has_record(&self, key: &H256, value: &H256) -> bool { + // Check if the key exists in storage and if its value matches the given value + match self.storage.get(&key) { + Some(stored_value) => *stored_value == *value, + None => false, + } + } }