Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
96f437f
feat: print optional opcodes in forge test
jaimebarrancos May 20, 2026
2be584d
improvements - suggested fixes; validate and parse input; fmt and cli…
jaimebarrancos May 29, 2026
39223bc
Merge branch 'foundry-rs:master' into master
jaimebarrancos May 29, 2026
deddadb
Merge branch 'master' into master
mablr May 29, 2026
d17140c
cargo lock fix; suggested fixes
jaimebarrancos May 30, 2026
43a4a9a
Merge branch 'foundry-rs:master' into master
jaimebarrancos May 30, 2026
d29a314
added tests
jaimebarrancos May 31, 2026
e65237f
Merge branch 'foundry-rs:master' into master
jaimebarrancos May 31, 2026
fe32c95
fix opcode swallowing test path
jaimebarrancos Jun 5, 2026
673308b
Merge branch 'master' into master
jaimebarrancos Jun 5, 2026
f9ca74c
fix: cargo file + fmt
mablr Jun 5, 2026
918dcff
print with hexadecimals; remove duplicate printing of storage changes
jaimebarrancos Jun 8, 2026
66214f8
test bugfix
jaimebarrancos Jun 8, 2026
383819e
Merge branch 'foundry-rs:master' into master
jaimebarrancos Jun 8, 2026
8851b0e
Merge branch 'master' into master
mablr Jun 8, 2026
55d5df9
add --no-dynamic-linting
jaimebarrancos Jun 9, 2026
3f7db4a
clone arena instead of nulling storage_change
jaimebarrancos Jun 10, 2026
55b5d7d
Merge branch 'master' into master
jaimebarrancos Jun 24, 2026
98941cf
bugfixes
jaimebarrancos Jun 24, 2026
a46f5c0
Merge branch 'master' into master
jaimebarrancos Jun 24, 2026
324232c
Merge branch 'master' into master
jaimebarrancos Jun 27, 2026
3bdf4fa
improvement
jaimebarrancos Jun 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 31 additions & 1 deletion crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
CallTrace, CallTraceArena, CallTraceNode, DecodedCallData,
CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, DecodedTraceStep,
debug::DebugTraceIdentifier,
identifier::{IdentifiedAddress, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier},
};
Expand All @@ -25,7 +25,9 @@ use foundry_evm_core::{
};
use foundry_evm_hardforks::TempoHardfork;
use itertools::Itertools;
use revm::bytecode::opcode::OpCode;
use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace};

use std::{collections::BTreeMap, sync::OnceLock};
use tempo_contracts::precompiles::{
IAccountKeychain, IAddressRegistry, IFeeManager, IReceivePolicyGuard, ISignatureVerifier,
Expand Down Expand Up @@ -194,6 +196,9 @@ pub struct CallTraceDecoder {
/// The chain ID, used to determine network-specific precompiles.
pub chain_id: Option<u64>,

/// Detailed opcodes for analysis
pub opcodes: Vec<OpCode>,

/// The Tempo hardfork, used to determine hardfork-specific precompiles.
pub tempo_hardfork: Option<TempoHardfork>,
}
Expand Down Expand Up @@ -320,6 +325,8 @@ impl CallTraceDecoder {

chain_id: None,

opcodes: Vec::new(),

tempo_hardfork: None,
}
}
Expand Down Expand Up @@ -513,6 +520,29 @@ impl CallTraceDecoder {
/// [CallTraceDecoder::decode_event] for more details.
pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
for node in traces {
if !self.opcodes.is_empty() {
for step in &mut node.trace.steps {
if step.decoded.is_some() {
continue;
}
for opcode in &self.opcodes {
if step.op == *opcode {
let res = match &step.storage_change {
Some(change) => format!(
Comment thread
jaimebarrancos marked this conversation as resolved.
"[{}] {} 0x{:x} → (0x{:x})",
step.gas_cost, opcode, change.key, change.value
Comment thread
jaimebarrancos marked this conversation as resolved.
),
None => format!("[{}] {}", step.gas_cost, opcode),
};

step.decoded = Some(Box::new(DecodedTraceStep::Line(res)));
step.storage_change = None; // show only step.decoded
Comment thread
jaimebarrancos marked this conversation as resolved.
Outdated
break;
}
}
}
}

node.trace.decoded = Some(Box::new(self.decode_function(&node.trace).await));
for log in &mut node.logs {
log.decoded = Some(Box::new(self.decode_event(&log.raw_log).await));
Expand Down
2 changes: 1 addition & 1 deletion crates/forge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ inferno = { version = "0.12", default-features = false }
itertools.workspace = true
parking_lot.workspace = true
regex = { workspace = true, default-features = false }
revm.workspace = true
revm = { workspace = true, features = ["parse"] }
semver.workspace = true
serde_json.workspace = true
similar = { version = "2", features = ["inline"] }
Expand Down
23 changes: 21 additions & 2 deletions crates/forge/src/cmd/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ use foundry_evm::{
};
use rand::Rng;
use regex::Regex;
use revm::context::Transaction;
use revm::{bytecode::opcode::OpCode, context::Transaction};
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Write,
Expand Down Expand Up @@ -320,6 +320,15 @@ pub struct TestArgs {
#[arg(long)]
pub rerun: bool,

/// Print the given opcodes in trace output, with their gas
/// cost and the storage slot and value, if available.
///
/// Accepts a comma-separated list of opcode names, e.g.
/// `--opcodes SLOAD,MLOAD,SSTORE`. Names are in uppercase.
/// Requires `-vvvvv` to render.
#[arg(long, value_parser = parse_opcode, value_delimiter(','))]
pub opcodes: Vec<OpCode>,

/// Print test summary table.
#[arg(long, help_heading = "Display options")]
pub summary: bool,
Expand Down Expand Up @@ -1251,6 +1260,11 @@ impl TestArgs {

let num_filtered = runner.matching_test_functions(filter).count();

if !self.opcodes.is_empty() && verbosity < 5 {
sh_eprintln!()?;
eyre::bail!("Not enough verbosity. Use -vvvvv to show opcodes.");
}

if num_filtered == 0 {
let total_tests = if filter.is_empty() {
num_filtered
Expand Down Expand Up @@ -1509,8 +1523,9 @@ impl TestArgs {
};

if should_include {
decode_trace_arena(arena, &decoder).await;
decoder.opcodes = self.opcodes.clone();

decode_trace_arena(arena, &decoder).await;
if let Some(trace_depth) = self.trace_depth {
prune_trace_depth(arena, trace_depth);
}
Expand Down Expand Up @@ -2004,6 +2019,10 @@ impl Provider for TestArgs {
}
}

fn parse_opcode(s: &str) -> Result<OpCode, String> {
OpCode::parse(s).ok_or_else(|| format!("invalid opcode: {s}"))
}

/// Lists all matching tests
fn list<FEN: FoundryEvmNetwork>(
runner: MultiContractRunner<FEN>,
Expand Down
120 changes: 120 additions & 0 deletions crates/forge/tests/cli/test_cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3338,6 +3338,126 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
"#]]);
});

// Tests that test traces display opcodes when verbosity level is 5
forgetest_init!(should_show_opcodes, |prj, cmd| {
prj.initialize_default_contracts();
cmd.args([
"test",
"--mt",
"test_Increment",
"-vvvvv",
"--opcodes",
"SLOAD,MLOAD",
"--no-dynamic-test-linking",
])
.assert_success()
.stdout_eq(str![[r#"
...
Ran 1 test for test/Counter.t.sol:CounterTest
[PASS] test_Increment() ([GAS])
Traces:
[..] CounterTest::setUp()
├─ [..] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 481 bytes of code
├─ [..] Counter::setNumber(0)
│ └─ ← [Stop]
└─ ← [Stop]

[..] CounterTest::test_Increment()
├─ [..] SLOAD 0x1f → (0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f01)
├─ [..] MLOAD
├─ [..] MLOAD
├─ [..] Counter::increment()
│ ├─ [..] SLOAD 0x0 → (0x0)
│ ├─ storage changes:
│ │ @ 0: 0 → 1
│ └─ ← [Stop]
├─ [..] SLOAD
├─ [..] MLOAD
├─ [..] MLOAD
├─ [..] Counter::number() [staticcall]
│ ├─ [..] SLOAD
│ ├─ [..] MLOAD
│ ├─ [..] MLOAD
│ └─ ← [Return] 1
├─ [..] MLOAD
├─ [..] MLOAD
└─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);
});

// Tests that --opcodes properly errors (fails early) with wrong opcode names
forgetest_init!(opcodes_invalid_name, |prj, cmd| {
prj.initialize_default_contracts();
cmd.args(["test", "--mt", "test_Increment", "-vvvvv", "--opcodes", "BOGUS"])
.assert_failure()
.stderr_eq(str![[r#"
...
error: invalid value 'BOGUS' for '--opcodes <OPCODES>': invalid opcode: BOGUS

For more information, try '--help'.

"#]]);
});

// Tests that --opcodes errors when not provided with -vvvvv
forgetest_init!(opcodes_not_enough_verbosity, |prj, cmd| {
prj.initialize_default_contracts();
cmd.args(["test", "--mt", "test_Increment", "-vvv", "--opcodes", "ADD"])
.assert_failure()
.stderr_eq(str![[r#"
...
Error: Not enough verbosity. Use -vvvvv to show opcodes.

"#]]);
});

// Tests that the test file path is not swallowed by the --opcodes flag
forgetest_init!(opcodes_path_after_flag, |prj, cmd| {
prj.initialize_default_contracts();
cmd.args([
"test",
"--mt",
"test_Increment",
"-vvvvv",
"--opcodes",
"SSTORE",
"test/Counter.t.sol",
"--no-dynamic-test-linking",
])
.assert_success()
.stdout_eq(str![[r#"
...
Ran 1 test for test/Counter.t.sol:CounterTest
[PASS] test_Increment() ([GAS])
Traces:
[..] CounterTest::setUp()
├─ [..] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 481 bytes of code
├─ [..] Counter::setNumber(0)
│ └─ ← [Stop]
└─ ← [Stop]

[..] CounterTest::test_Increment()
├─ [..] Counter::increment()
│ ├─ [..] SSTORE 0x0 → (0x1)
│ └─ ← [Stop]
├─ [..] Counter::number() [staticcall]
│ └─ ← [Return] 1
└─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);
});

// Tests that chained errors are properly displayed.
// <https://github.com/foundry-rs/foundry/issues/9161>
forgetest!(displays_chained_error, |prj, cmd| {
Expand Down
Loading