diff --git a/Cargo.toml b/Cargo.toml index 8e900539..f25a8576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,11 @@ alloy-sol-types = "0.6" alloy-primitives = "0.6" alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "b9dd518" } alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy", rev = "b9dd518" } - -# revm revm = { version = "5.0", default-features = false, features = ["std"] } +anstyle = "1.0" +colorchoice = "1.0" + # js-tracing-inspector boa_engine = { version = "0.17", optional = true } boa_gc = { version = "0.17", optional = true } @@ -37,6 +38,8 @@ serde = { version = "1", features = ["derive"] } thiserror = { version = "1", optional = true } serde_json = { version = "1", optional = true } +[dev-dependencies] +expect-test = "1.4" + [features] -default = [] js-tracer = ["boa_engine", "boa_gc", "thiserror", "serde_json"] diff --git a/src/lib.rs b/src/lib.rs index 1de5ec5f..3415fa9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,14 +11,6 @@ html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" )] -#![warn( - missing_copy_implementations, - missing_debug_implementations, - missing_docs, - unreachable_pub, - clippy::missing_const_for_fn, - rustdoc::all -)] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] @@ -28,9 +20,13 @@ pub mod access_list; /// implementation of an opcode counter for the EVM. pub mod opcode; + /// An inspector stack abstracting the implementation details of /// each inspector and allowing to hook on block/transaction execution, /// used in the main RETH executor. pub mod stack; + /// An inspector for recording traces pub mod tracing; + +pub use colorchoice::ColorChoice; diff --git a/src/tracing/builder/walker.rs b/src/tracing/builder/walker.rs index 4d88a2af..c9022077 100644 --- a/src/tracing/builder/walker.rs +++ b/src/tracing/builder/walker.rs @@ -1,14 +1,13 @@ use crate::tracing::types::CallTraceNode; use std::collections::VecDeque; -/// Traverses Reths internal tracing structure breadth-first +/// Traverses the internal tracing structure breadth-first. /// -/// This is a lazy iterator +/// This is a lazy iterator. pub(crate) struct CallTraceNodeWalkerBF<'trace> { - /// the entire arena + /// The entire arena. nodes: &'trace Vec, - - /// holds indexes of nodes to visit as we traverse + /// Indexes of nodes to visit as we traverse. queue: VecDeque, } @@ -16,7 +15,6 @@ impl<'trace> CallTraceNodeWalkerBF<'trace> { pub(crate) fn new(nodes: &'trace Vec) -> Self { let mut queue = VecDeque::with_capacity(nodes.len()); queue.push_back(0); - Self { nodes, queue } } } @@ -25,15 +23,10 @@ impl<'trace> Iterator for CallTraceNodeWalkerBF<'trace> { type Item = &'trace CallTraceNode; fn next(&mut self) -> Option { - match self.queue.pop_front() { - Some(idx) => { - let curr = self.nodes.get(idx).expect("there should be a node"); - - self.queue.extend(curr.children.iter()); - - Some(curr) - } - None => None, - } + self.queue.pop_front().map(|idx| { + let curr = &self.nodes[idx]; + self.queue.extend(curr.children.iter().copied()); + curr + }) } } diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index 52788944..1637f17a 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -1,5 +1,3 @@ -use std::ops::Range; - use self::parity::stack_push_count; use crate::tracing::{ arena::PushTraceKind, @@ -18,25 +16,34 @@ use revm::{ primitives::SpecId, Database, EvmContext, Inspector, JournalEntry, }; -use types::{CallTrace, CallTraceStep}; +use std::ops::Range; mod arena; +pub use arena::CallTraceArena; mod builder; -mod config; -mod fourbyte; -mod opcount; -pub mod types; -mod utils; -pub use arena::CallTraceArena; pub use builder::{ geth::{self, GethTraceBuilder}, parity::{self, ParityTraceBuilder}, }; + +mod config; pub use config::{StackSnapshotType, TracingInspectorConfig}; + +mod fourbyte; pub use fourbyte::FourByteInspector; + +mod opcount; pub use opcount::OpcodeCountInspector; +pub mod types; +use types::{CallTrace, CallTraceStep}; + +mod utils; + +mod writer; +pub use writer::TraceWriter; + #[cfg(feature = "js-tracer")] pub mod js; @@ -391,7 +398,8 @@ impl TracingInspector { // The gas cost is the difference between the recorded gas remaining at the start of the // step the remaining gas here, at the end of the step. - step.gas_cost = step.gas_remaining - self.gas_inspector.gas_remaining(); + // TODO: Figure out why this can overflow. https://github.com/paradigmxyz/evm-inspectors/pull/38 + step.gas_cost = step.gas_remaining.saturating_sub(self.gas_inspector.gas_remaining()); // set the status step.status = interp.instruction_result; diff --git a/src/tracing/utils.rs b/src/tracing/utils.rs index d13c8aa1..19b4b1c7 100644 --- a/src/tracing/utils.rs +++ b/src/tracing/utils.rs @@ -54,10 +54,8 @@ pub(crate) fn maybe_revert_reason(output: &[u8]) -> Option { let reason = match GenericRevertReason::decode(output)? { GenericRevertReason::ContractError(err) => { match err { - ContractError::Revert(revert) => { - // return the raw revert reason and don't use the revert's display message - revert.reason - } + // return the raw revert reason and don't use the revert's display message + ContractError::Revert(revert) => revert.reason, err => err.to_string(), } } @@ -75,12 +73,6 @@ mod tests { use super::*; use alloy_sol_types::{GenericContractError, SolInterface}; - #[test] - fn decode_empty_revert() { - let reason = GenericRevertReason::decode("".as_bytes()).map(|x| x.to_string()); - assert_eq!(reason, Some("".to_string())); - } - #[test] fn decode_revert_reason() { let err = GenericContractError::Revert("my revert".into()); diff --git a/src/tracing/writer.rs b/src/tracing/writer.rs new file mode 100644 index 00000000..a29e5999 --- /dev/null +++ b/src/tracing/writer.rs @@ -0,0 +1,292 @@ +use super::{ + types::{CallKind, CallTrace, CallTraceNode, LogCallOrder}, + CallTraceArena, +}; +use alloy_primitives::{address, hex, Address, LogData}; +use anstyle::{AnsiColor, Color, Style}; +use colorchoice::ColorChoice; +use std::io::{self, Write}; + +const CHEATCODE_ADDRESS: Address = address!("7109709ECfa91a80626fF3989D68f67F5b1DD12D"); + +const PIPE: &str = " │ "; +const EDGE: &str = " └─ "; +const BRANCH: &str = " ├─ "; +const CALL: &str = "→ "; +const RETURN: &str = "← "; + +const TRACE_KIND_STYLE: Style = AnsiColor::Yellow.on_default(); +const LOG_STYLE: Style = AnsiColor::Cyan.on_default(); + +/// Formats [call traces](CallTraceArena) to an [`Write`] writer. +/// +/// Will never write invalid UTF-8. +#[derive(Clone, Debug)] +pub struct TraceWriter { + writer: W, + use_colors: bool, + color_cheatcodes: bool, + indentation_level: u16, +} + +impl TraceWriter { + /// Create a new `TraceWriter` with the given writer. + #[inline] + pub fn new(writer: W) -> Self { + Self { + writer, + use_colors: use_colors(ColorChoice::global()), + color_cheatcodes: false, + indentation_level: 0, + } + } + + /// Sets the color choice. + #[inline] + pub fn use_colors(mut self, color_choice: ColorChoice) -> Self { + self.use_colors = use_colors(color_choice); + self + } + + /// Sets whether to color calls to the cheatcode address differently. + #[inline] + pub fn color_cheatcodes(mut self, yes: bool) -> Self { + self.color_cheatcodes = yes; + self + } + + /// Sets the starting indentation level. + #[inline] + pub fn with_indentation_level(mut self, level: u16) -> Self { + self.indentation_level = level; + self + } + + /// Returns a reference to the inner writer. + #[inline] + pub const fn writer(&self) -> &W { + &self.writer + } + + /// Returns a mutable reference to the inner writer. + #[inline] + pub fn writer_mut(&mut self) -> &mut W { + &mut self.writer + } + + /// Consumes the `TraceWriter` and returns the inner writer. + #[inline] + pub fn into_writer(self) -> W { + self.writer + } + + /// Writes a call trace arena to the writer. + pub fn write_arena(&mut self, arena: &CallTraceArena) -> io::Result<()> { + self.write_node(arena.nodes(), 0)?; + self.writer.flush() + } + + fn write_node(&mut self, nodes: &[CallTraceNode], idx: usize) -> io::Result<()> { + let node = &nodes[idx]; + + // Write header. + self.write_branch()?; + self.write_trace_header(&node.trace)?; + self.writer.write_all(b"\n")?; + + // Write logs and subcalls. + self.indentation_level += 1; + for child in &node.ordering { + match *child { + LogCallOrder::Log(index) => self.write_raw_log(&node.logs[index]), + LogCallOrder::Call(index) => self.write_node(nodes, node.children[index]), + }?; + } + + // Write return data. + self.write_edge()?; + self.write_trace_footer(&node.trace)?; + self.writer.write_all(b"\n")?; + + self.indentation_level -= 1; + + Ok(()) + } + + fn write_trace_header(&mut self, trace: &CallTrace) -> io::Result<()> { + write!(self.writer, "[{}] ", trace.gas_used)?; + + let trace_kind_style = self.trace_kind_style(); + let address = trace.address.to_checksum_buffer(None); + + if trace.kind.is_any_create() { + #[allow(clippy::write_literal)] // TODO + write!( + self.writer, + "{trace_kind_style}{CALL}new{trace_kind_style:#} {label}@{address}", + // TODO: trace.label.as_deref().unwrap_or("") + label = "", + )?; + } else { + let (func_name, inputs) = match None::<()> { + // TODO + // Some(DecodedCallData { signature, args }) => { + // let name = signature.split('(').next().unwrap(); + // (name.to_string(), args.join(", ")) + // } + Some(()) => unreachable!(), + None => { + if trace.data.len() < 4 { + ("fallback".to_string(), hex::encode(&trace.data)) + } else { + let (selector, data) = trace.data.split_at(4); + (hex::encode(selector), hex::encode(data)) + } + } + }; + + write!( + self.writer, + "{style}{addr}{style:#}::{style}{func_name}{style:#}", + style = self.trace_style(trace), + // TODO: trace.label + addr = None::.as_deref().unwrap_or(address.as_str()) + )?; + + if !trace.value.is_zero() { + write!(self.writer, "{{value: {}}}", trace.value)?; + } + + write!(self.writer, "({inputs})")?; + + let action = match trace.kind { + CallKind::Call => None, + CallKind::StaticCall => Some(" [staticcall]"), + CallKind::CallCode => Some(" [callcode]"), + CallKind::DelegateCall => Some(" [delegatecall]"), + CallKind::Create | CallKind::Create2 => unreachable!(), + }; + if let Some(action) = action { + write!(self.writer, "{trace_kind_style}{action}{trace_kind_style:#}")?; + } + } + + Ok(()) + } + + fn write_raw_log(&mut self, log: &LogData) -> io::Result<()> { + let log_style = self.log_style(); + self.write_branch()?; + + for (i, topic) in log.topics().iter().enumerate() { + if i == 0 { + self.writer.write_all(b" emit topic 0")?; + } else { + self.write_pipes()?; + write!(self.writer, " topic {i}")?; + } + writeln!(self.writer, ": {log_style}{topic}{log_style:#}")?; + } + + if !log.topics().is_empty() { + self.write_pipes()?; + } + writeln!(self.writer, " data: {log_style}{data}{log_style:#}", data = log.data) + } + + #[cfg(TODO)] + fn write_decoded_log(&mut self, name: &str, params: &[(String, String)]) -> io::Result<()> { + let log_style = self.log_style(); + self.write_left_prefix()?; + + write!(self.writer, "emit {name}({log_style}")?; + for (i, (name, value)) in params.iter().enumerate() { + if i > 0 { + self.writer.write_all(b", ")?; + } + write!(self.writer, "{name}: {value}")?; + } + write!(self.writer, "{log_style:#})") + } + + fn write_trace_footer(&mut self, trace: &CallTrace) -> io::Result<()> { + write!(self.writer, "{style}{RETURN}{style:#}", style = self.trace_style(trace))?; + // TODO: + // if let Some(decoded) = trace.decoded_return_data { + // return self.writer.write_all(decoded.as_bytes()); + // } + if trace.kind.is_any_create() { + write!(self.writer, "{} bytes of code", trace.output.len()) + } else if trace.output.is_empty() { + self.writer.write_all(b"()") + } else { + write!(self.writer, "{}", trace.output) + } + // TODO: Write `trace.status`? + } + + fn write_indentation(&mut self) -> io::Result<()> { + self.writer.write_all(b" ")?; + for _ in 1..self.indentation_level { + self.writer.write_all(PIPE.as_bytes())?; + } + Ok(()) + } + + // FKA left_prefix + fn write_branch(&mut self) -> io::Result<()> { + self.write_indentation()?; + if self.indentation_level != 0 { + self.writer.write_all(BRANCH.as_bytes())?; + } + Ok(()) + } + + // FKA right_prefix + fn write_pipes(&mut self) -> io::Result<()> { + self.write_indentation()?; + self.writer.write_all(PIPE.as_bytes()) + } + + fn write_edge(&mut self) -> io::Result<()> { + self.write_indentation()?; + self.writer.write_all(EDGE.as_bytes()) + } + + fn trace_style(&self, trace: &CallTrace) -> Style { + if !self.use_colors { + return Style::default(); + } + let color = if self.color_cheatcodes && trace.address == CHEATCODE_ADDRESS { + AnsiColor::Blue + } else if trace.success { + AnsiColor::Green + } else { + AnsiColor::Red + }; + Color::Ansi(color).on_default() + } + + fn trace_kind_style(&self) -> Style { + if !self.use_colors { + return Style::default(); + } + TRACE_KIND_STYLE + } + + fn log_style(&self) -> Style { + if !self.use_colors { + return Style::default(); + } + LOG_STYLE + } +} + +fn use_colors(choice: ColorChoice) -> bool { + use io::IsTerminal; + match choice { + ColorChoice::Auto => io::stdout().is_terminal(), + ColorChoice::AlwaysAnsi | ColorChoice::Always => true, + ColorChoice::Never => false, + } +} diff --git a/testdata/Counter.sol b/testdata/Counter.sol new file mode 100644 index 00000000..5e449483 --- /dev/null +++ b/testdata/Counter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + event Log(uint256 indexed number, bytes dump); + event Log2(uint256 indexed number, bytes dump); + + uint256 public number; + + function setNumber(uint256 newNumber) public returns (bool) { + number = newNumber; + return true; + } + + function increment() public { + number++; + } + + function nest1() public { + emit Log(number, "hi from 1"); + this.nest2(); + increment(); + } + + function nest2() public { + increment(); + this.nest3(); + emit Log2(number, "hi from 2"); + } + + function nest3() public { + emit Log(number, "hi from 3"); + } +} diff --git a/tests/it/main.rs b/tests/it/main.rs index 342f6a4d..baf8804c 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -1,2 +1,4 @@ -mod parity; pub mod utils; + +mod parity; +mod writer; diff --git a/tests/it/parity.rs b/tests/it/parity.rs index e8b56605..7e8867fc 100644 --- a/tests/it/parity.rs +++ b/tests/it/parity.rs @@ -1,6 +1,6 @@ //! Parity tests -use crate::utils::inspect; +use crate::utils::{inspect, print_traces}; use alloy_primitives::{hex, Address}; use alloy_rpc_types::TransactionInfo; use revm::{ @@ -122,7 +122,6 @@ fn test_parity_constructor_selfdestruct() { ); let mut insp = TracingInspector::new(TracingInspectorConfig::default_parity()); - let (res, _) = inspect(&mut db, env, &mut insp).unwrap(); let addr = match res.result { ExecutionResult::Success { output, .. } => match output { @@ -132,6 +131,7 @@ fn test_parity_constructor_selfdestruct() { _ => panic!("Execution failed"), }; db.commit(res.state); + print_traces(&insp); let mut insp = TracingInspector::new(TracingInspectorConfig::default_parity()); @@ -149,6 +149,7 @@ fn test_parity_constructor_selfdestruct() { let (res, _) = inspect(&mut db, env, &mut insp).unwrap(); assert!(res.result.is_success()); + print_traces(&insp); let traces = insp .with_transaction_gas_used(res.result.gas_used()) diff --git a/tests/it/utils.rs b/tests/it/utils.rs index 861e3ba0..980c5f81 100644 --- a/tests/it/utils.rs +++ b/tests/it/utils.rs @@ -1,8 +1,87 @@ +use alloy_primitives::{Address, Bytes, U256}; +use colorchoice::ColorChoice; use revm::{ + db::{CacheDB, EmptyDB}, inspector_handle_register, - primitives::{EVMError, EnvWithHandlerCfg, ResultAndState}, - Database, GetInspector, + primitives::{ + BlockEnv, CreateScheme, EVMError, Env, EnvWithHandlerCfg, ExecutionResult, Output, + ResultAndState, SpecId, TransactTo, TxEnv, + }, + Database, DatabaseCommit, GetInspector, }; +use revm_inspectors::tracing::TracingInspector; +use std::convert::Infallible; + +type TestDb = CacheDB; + +#[derive(Clone, Debug)] +pub struct TestEvm { + pub db: TestDb, + pub env: EnvWithHandlerCfg, +} + +impl Default for TestEvm { + fn default() -> Self { + Self::new() + } +} + +impl TestEvm { + pub fn new() -> Self { + let db = CacheDB::new(EmptyDB::default()); + let env = EnvWithHandlerCfg::new( + Box::new(Env { + block: BlockEnv { gas_limit: U256::MAX, ..Default::default() }, + tx: TxEnv { gas_limit: u64::MAX, gas_price: U256::ZERO, ..Default::default() }, + ..Default::default() + }), + SpecId::CANCUN, + ); + Self { db, env } + } + + pub fn deploy GetInspector<&'a mut TestDb>>( + &mut self, + data: Bytes, + inspector: I, + ) -> Result> { + self.env.tx.data = data; + self.env.tx.transact_to = TransactTo::Create(CreateScheme::Create); + + let (ResultAndState { result, state }, env) = self.inspect(inspector)?; + self.db.commit(state); + let address = match result { + ExecutionResult::Success { output, .. } => match output { + Output::Create(_, address) => address.unwrap(), + _ => panic!("Create failed"), + }, + _ => panic!("Execution failed"), + }; + self.env = env; + Ok(address) + } + + pub fn call GetInspector<&'a mut TestDb>>( + &mut self, + address: Address, + data: Bytes, + inspector: I, + ) -> Result> { + self.env.tx.data = data; + self.env.tx.transact_to = TransactTo::Call(address); + let (ResultAndState { result, state }, env) = self.inspect(inspector)?; + self.db.commit(state); + self.env = env; + Ok(result) + } + + pub fn inspect GetInspector<&'a mut TestDb>>( + &mut self, + inspector: I, + ) -> Result<(ResultAndState, EnvWithHandlerCfg), EVMError> { + inspect(&mut self.db, self.env.clone(), inspector) + } +} /// Executes the [EnvWithHandlerCfg] against the given [Database] without committing state changes. pub fn inspect( @@ -24,3 +103,18 @@ where let (_, env) = evm.into_db_and_env_with_handler_cfg(); Ok((res, env)) } + +pub fn write_traces(tracer: &TracingInspector) -> String { + write_traces_with(tracer, ColorChoice::Never) +} + +pub fn write_traces_with(tracer: &TracingInspector, color: ColorChoice) -> String { + let mut w = revm_inspectors::tracing::TraceWriter::new(Vec::::new()).use_colors(color); + w.write_arena(tracer.get_traces()).expect("failed to write traces to Vec"); + String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8") +} + +pub fn print_traces(tracer: &TracingInspector) { + // Use `println!` so that the output is captured by the test runner. + println!("{}", write_traces_with(tracer, ColorChoice::Auto)); +} diff --git a/tests/it/writer.rs b/tests/it/writer.rs new file mode 100644 index 00000000..7a76cfa6 --- /dev/null +++ b/tests/it/writer.rs @@ -0,0 +1,97 @@ +use crate::utils::{write_traces, TestEvm}; +use alloy_primitives::{bytes, Bytes, U256}; +use alloy_sol_types::{sol, SolCall}; +use expect_test::expect; +use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; + +#[test] +fn basic_trace_printing() { + // solc testdata/Counter.sol --via-ir --optimize --bin + sol!("testdata/Counter.sol"); + static BYTECODE: Bytes = bytes!("60808060405234610016576102e2908161001b8239f35b5f80fdfe608060408181526004361015610013575f80fd5b5f915f3560e01c9081633fb5c1cb146102475781638381f58a1461022e57508063943ee48c146101885780639db265eb1461012f578063d09de08a146101105763f267ce9e14610061575f80fd5b346100ff57816003193601126100ff57610079610287565b303b156100ff578051639db265eb60e01b81528290818160048183305af18015610103576100eb575b5060607f4544f35949a681d9e47cca4aa47bb4add2aad7bf475fac397d0eddc4efe69eda91549268343490333937b6901960b91b8151916020835260096020840152820152a280f35b6100f49061025f565b6100ff57815f6100a2565b5080fd5b50505051903d90823e3d90fd5b823461012c578060031936011261012c57610129610287565b80f35b80fd5b50346100ff57816003193601126100ff577f4ada34a03bac92ee05461fb68ac194ed75b2b3ac9c428a50c1240505512954d560608354926868692066726f6d203360b81b8151916020835260096020840152820152a280f35b503461022a575f36600319011261022a575f547f4ada34a03bac92ee05461fb68ac194ed75b2b3ac9c428a50c1240505512954d56060835160208152600960208201526868692066726f6d203160b81b85820152a2303b1561022a578051637933e74f60e11b8152905f8260048183305af19081156102215750610210575b50610129610287565b61021a915061025f565b5f80610207565b513d5f823e3d90fd5b5f80fd5b3461022a575f36600319011261022a576020905f548152f35b3461022a57602036600319011261022a576004355f55005b67ffffffffffffffff811161027357604052565b634e487b7160e01b5f52604160045260245ffd5b5f545f198114610298576001015f55565b634e487b7160e01b5f52601160045260245ffdfea2646970667358221220e2a4410c976bdf76baab910915ab68a6487152ba1ea5836d41a16ac8042a36c864736f6c63430008180033"); + + let mut evm = TestEvm::new(); + + let mut tracer = TracingInspector::new(TracingInspectorConfig::all()); + let address = evm.deploy(BYTECODE.clone(), &mut tracer).unwrap(); + let mut s = write_traces(&tracer); + patch_output(&mut s); + expect![[r#" + . [147802] → new @0xBd770416a3345F91E4B34576cb804a576fa48EB1 + └─ ← 738 bytes of code + "#]] + .assert_eq(&s); + + let mut call = |data: Vec| -> String { + let mut tracer = TracingInspector::new(TracingInspectorConfig::all()); + let r = evm.call(address, data.into(), &mut tracer).unwrap(); + assert!(r.is_success()); + write_traces(&tracer) + }; + + let mut s = call(Counter::numberCall {}.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [2277] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::8381f58a() + └─ ← 0x0000000000000000000000000000000000000000000000000000000000000000 + "#]] + .assert_eq(&s); + + let mut s = call(Counter::incrementCall {}.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [22390] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::d09de08a() + └─ ← () + "#]] + .assert_eq(&s); + + let mut s = call(Counter::numberCall {}.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [2277] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::8381f58a() + └─ ← 0x0000000000000000000000000000000000000000000000000000000000000001 + "#]] + .assert_eq(&s); + + let mut s = call(Counter::setNumberCall { newNumber: U256::from(69) }.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [5144] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::3fb5c1cb(0000000000000000000000000000000000000000000000000000000000000045) + └─ ← () + "#]] + .assert_eq(&s); + + let mut s = call(Counter::numberCall {}.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [2277] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::8381f58a() + └─ ← 0x0000000000000000000000000000000000000000000000000000000000000045 + "#]] + .assert_eq(&s); + + let mut s = call(Counter::nest1Call {}.abi_encode()); + patch_output(&mut s); + expect![[r#" + . [12917] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::943ee48c() + ├─ emit topic 0: 0x4ada34a03bac92ee05461fb68ac194ed75b2b3ac9c428a50c1240505512954d5 + │ topic 1: 0x0000000000000000000000000000000000000000000000000000000000000045 + │ data: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000968692066726f6d20310000000000000000000000000000000000000000000000 + ├─ [8035] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::f267ce9e() + │ ├─ [2277] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::9db265eb() + │ │ ├─ emit topic 0: 0x4ada34a03bac92ee05461fb68ac194ed75b2b3ac9c428a50c1240505512954d5 + │ │ │ topic 1: 0x0000000000000000000000000000000000000000000000000000000000000046 + │ │ │ data: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000968692066726f6d20330000000000000000000000000000000000000000000000 + │ │ └─ ← () + │ ├─ emit topic 0: 0x4544f35949a681d9e47cca4aa47bb4add2aad7bf475fac397d0eddc4efe69eda + │ │ topic 1: 0x0000000000000000000000000000000000000000000000000000000000000046 + │ │ data: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000968692066726f6d20320000000000000000000000000000000000000000000000 + │ └─ ← () + └─ ← () + "#]] + .assert_eq(&s); +} + +// Without this, `expect_test` fails on its own updated test output. +fn patch_output(s: &mut str) { + (unsafe { s[0..1].as_bytes_mut() })[0] = b'.'; +}