diff --git a/tvm_debugger/src/decode.rs b/tvm_debugger/src/decode.rs index f3f8c213..a74c80d3 100644 --- a/tvm_debugger/src/decode.rs +++ b/tvm_debugger/src/decode.rs @@ -52,7 +52,7 @@ pub(crate) fn decode_actions( .map_err(|e| anyhow::format_err!("SliceData::load_cell: {e}"))?, ) .map_err(|e| anyhow::format_err!("OutActions::construct_from: {e}"))?; - res.push("Output actions:\n----------------".to_string()); + res.log("Output actions:\n----------------".to_string()); let mut created_lt = 1; for act in actions { match act { @@ -63,7 +63,7 @@ pub(crate) fn decode_actions( created_lt += 1; } res.add_out_message(out_msg.clone()); - res.push(format!("Action(SendMsg):\n{}", msg_printer(&out_msg)?)); + res.log(format!("Action(SendMsg):\n{}", msg_printer(&out_msg)?)); if let Some(b) = out_msg.body() { if abi_file.is_some() && function_name.is_some() { decode_body( @@ -77,16 +77,16 @@ pub(crate) fn decode_actions( } } OutAction::SetCode { new_code: code } => { - res.push("Action(SetCode)".to_string()); + res.log("Action(SetCode)".to_string()); state.code = Some(code); } OutAction::ReserveCurrency { .. } => { - res.push("Action(ReserveCurrency)".to_string()); + res.log("Action(ReserveCurrency)".to_string()); } OutAction::ChangeLibrary { .. } => { - res.push("Action(ChangeLibrary)".to_string()); + res.log("Action(ChangeLibrary)".to_string()); } - _ => res.push("Action(Unknown)".to_string()), + _ => res.log("Action(Unknown)".to_string()), }; } } diff --git a/tvm_debugger/src/execute.rs b/tvm_debugger/src/execute.rs index f25c9f45..b36ec527 100644 --- a/tvm_debugger/src/execute.rs +++ b/tvm_debugger/src/execute.rs @@ -57,21 +57,19 @@ pub(crate) fn execute(args: &Args, res: &mut ExecutionResult) -> anyhow::Result< let exit_code = engine.execute().unwrap_or_else(|error| match tvm_exception(error) { Ok(exception) => { - println!("Unhandled exception: {}", exception); + res.log(format!("Unhandled exception: {}", exception)); exception.exception_or_custom_code() } _ => -1, }); - let is_vm_success = engine.get_committed_state().is_committed(); - res.push(format!("TVM terminated with exit code {}", exit_code)); - res.push(format!("Computing phase is success: {}", is_vm_success)); - res.push(format!("Gas used: {}", engine.get_gas().get_gas_used())); - res.push("".to_string()); - res.push(format!("{}", engine.dump_stack("Post-execution stack state", false))); - res.push(format!("{}", engine.dump_ctrls(false))); + res.exit_code(exit_code); + res.vm_success(engine.get_committed_state().is_committed()); + res.gas_used(engine.get_gas().get_gas_used()); + res.log(format!("{}", engine.dump_stack("Post-execution stack state", false))); + res.log(format!("{}", engine.dump_ctrls(false))); - if is_vm_success { + if res.is_vm_success { decode_actions(engine.get_actions(), &mut contract_state_init, args, res)?; contract_state_init.data = match engine.get_committed_state().get_root() { @@ -82,10 +80,10 @@ pub(crate) fn execute(args: &Args, res: &mut ExecutionResult) -> anyhow::Result< .write_to_file(&args.input_file) .map_err(|e| anyhow::format_err!("Failed to save state init after execution: {e}"))?; - res.push("Contract persistent data updated".to_string()); + res.log("Contract persistent data updated".to_string()); } - res.push("EXECUTION COMPLETED".to_string()); + res.log("EXECUTION COMPLETED".to_string()); Ok(()) } diff --git a/tvm_debugger/src/main.rs b/tvm_debugger/src/main.rs index 47616bfa..2b50967e 100644 --- a/tvm_debugger/src/main.rs +++ b/tvm_debugger/src/main.rs @@ -88,6 +88,61 @@ fn main() -> anyhow::Result<()> { let args: Args = Args::parse(); let mut res: ExecutionResult = ExecutionResult::new(args.json); execute(&args, &mut res)?; - res.print(); + println!("{}", res.output()); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::fs; + use std::path::PathBuf; + + fn create_temp_contract_file() -> PathBuf { + let temp_path = PathBuf::from("tests/temp_contract.tvc"); + fs::copy("tests/contract/contract.tvc", &temp_path).expect("Failed to copy contract file"); + temp_path + } + + fn cleanup_temp_contract_file(temp_path: &PathBuf) { + fs::remove_file(temp_path).expect("Failed to delete temporary contract file"); + } + + fn default_args(input_file: PathBuf, func: &str) -> Args { + Args { + input_file, + abi_file: Some(PathBuf::from("tests/contract/contract.abi.json")), + abi_header: None, + function_name: Some(func.to_string()), + call_parameters: None, + address: None, + sign: None, + internal: false, + message_value: None, + message_ecc: None, + message_source: None, + decode_out_messages: false, + json: true, + trace: false, + } + } + + #[test] + fn test_valid_input() { + let temp = create_temp_contract_file(); + let args = &default_args(temp.clone(), "counter"); + let mut res: ExecutionResult = ExecutionResult::new(args.json); + let result = execute(args, &mut res); + assert!(result.is_ok()); + let actual = res.to_json(); + let response = json!({ + "counter": "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), + }); + assert_eq!(actual["exit_code"], 0i32); + assert_eq!(actual["vm_success"], true); + assert_eq!(actual["gas_used"], 4065i64); + assert_eq!(actual["response"], response); + cleanup_temp_contract_file(&temp); + } +} diff --git a/tvm_debugger/src/result.rs b/tvm_debugger/src/result.rs index 434e6d05..371adf85 100644 --- a/tvm_debugger/src/result.rs +++ b/tvm_debugger/src/result.rs @@ -7,7 +7,10 @@ pub struct ExecutionResult { is_json: bool, log: Vec, messages: Vec, - response: String, + response: Value, + response_code: i32, + pub(crate) is_vm_success: bool, + gas_used: i64, } impl ExecutionResult { @@ -16,24 +19,48 @@ impl ExecutionResult { is_json, log: vec![], messages: vec![], - response: "{}".to_string(), + response: "{}".into(), + response_code: -1, + is_vm_success: false, + gas_used: 0, }; } + pub fn exit_code(&mut self, code: i32) { + self.response_code = code; + self.log(format!("TVM terminated with exit code {}", code)); + } + + pub fn vm_success(&mut self, is_vm_success: bool) { + self.is_vm_success = is_vm_success; + self.log(format!("Computing phase is success: {}", is_vm_success)); + } + + pub fn gas_used(&mut self, gas: i64) { + self.gas_used = gas; + self.log(format!("Gas used: {}", self.gas_used)); + self.log("".to_string()); + } + pub fn response(&mut self, data: String) { - self.response = data.clone(); - self.push(data); + self.response = serde_json::from_str(&*data.clone()).expect("Failed to parse JSON"); + self.log(data); } pub fn add_out_message(&mut self, message: Message) { match message.header() { CommonMsgInfo::IntMsgInfo(_) => { + let state_init = match message.state_init() { + None => None, + Some(state_init) => Some(base64_encode(state_init.write_to_bytes().unwrap())), + }; let destination = message.header().get_dst_address().unwrap_or_default().to_string(); let body = tree_of_cells_into_base64(message.body().map(|s| s.into_cell()).as_ref()); let boc = base64_encode(message.write_to_bytes().unwrap()); self.messages.push(json!({ + "state_init": state_init, "destination": destination, "body": body, "boc": boc, @@ -44,19 +71,21 @@ impl ExecutionResult { } } - pub fn push(&mut self, data: String) { + pub fn log(&mut self, data: String) { self.log.push(data); } - pub fn print(&mut self) { - if self.is_json { - let messages = - serde_json::to_string(&self.messages).unwrap_or_else(|_| "[]".to_string()); - println!(r#"{{"response":{},"messages":{}}}"#, self.response, messages); - } else { - for item in self.log.clone() { - println!("{}", item); - } - } + pub fn to_json(&self) -> Value { + json!({ + "exit_code": self.response_code, + "vm_success": self.is_vm_success, + "gas_used": self.gas_used, + "response": self.response, + "messages": self.messages, + }) + } + + pub fn output(&mut self) -> String { + return if self.is_json { self.to_json().to_string() } else { self.log.join("\n") }; } }