From f04da1a4dd62b909c3bd0409743273a874c8d044 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Tue, 29 Jul 2025 12:32:31 +0530 Subject: [PATCH 01/10] feat(forge-inspect): add option to wrap tables to terminal width --- crates/forge/src/cmd/inspect.rs | 198 +++++++++++++++++++------------- 1 file changed, 115 insertions(+), 83 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index c7ef0772f3cae..0bd54db7d93e1 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -40,11 +40,14 @@ pub struct InspectArgs { /// Whether to remove comments when inspecting `ir` and `irOptimized` artifact fields. #[arg(long, short, help_heading = "Display options")] pub strip_yul_comments: bool, + + #[arg(long, short, help_heading = "Display options")] + pub wrap: bool, } impl InspectArgs { pub fn run(self) -> Result<()> { - let Self { contract, field, build, strip_yul_comments } = self; + let Self { contract, field, build, strip_yul_comments, wrap } = self; trace!(target: "forge", ?field, ?contract, "running forge inspect"); @@ -86,7 +89,7 @@ impl InspectArgs { .abi .as_ref() .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?; - print_abi(abi)?; + print_abi(abi, wrap)?; } ContractArtifactField::Bytecode => { print_json_str(&artifact.bytecode, Some("object"))?; @@ -101,13 +104,13 @@ impl InspectArgs { print_json_str(&artifact.legacy_assembly, None)?; } ContractArtifactField::MethodIdentifiers => { - print_method_identifiers(&artifact.method_identifiers)?; + print_method_identifiers(&artifact.method_identifiers, wrap)?; } ContractArtifactField::GasEstimates => { print_json(&artifact.gas_estimates)?; } ContractArtifactField::StorageLayout => { - print_storage_layout(artifact.storage_layout.as_ref())?; + print_storage_layout(artifact.storage_layout.as_ref(), wrap)?; } ContractArtifactField::DevDoc => { print_json(&artifact.devdoc)?; @@ -129,11 +132,11 @@ impl InspectArgs { } ContractArtifactField::Errors => { let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors); - print_errors_events(&out, true)?; + print_errors_events(&out, true, wrap)?; } ContractArtifactField::Events => { let out = artifact.abi.as_ref().map_or(Map::new(), parse_events); - print_errors_events(&out, false)?; + print_errors_events(&out, false, wrap)?; } ContractArtifactField::StandardJson => { let standard_json = if let Some(version) = solc_version { @@ -187,66 +190,70 @@ fn parse_event_params(ev_params: &[EventParam]) -> String { .join(",") } -fn print_abi(abi: &JsonAbi) -> Result<()> { +fn print_abi(abi: &JsonAbi, should_wrap: bool) -> Result<()> { if shell::is_json() { return print_json(abi); } let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")]; - print_table(headers, |table| { - // Print events - for ev in abi.events.iter().flat_map(|(_, events)| events) { - let types = parse_event_params(&ev.inputs); - let selector = ev.selector().to_string(); - table.add_row(["event", &format!("{}({})", ev.name, types), &selector]); - } + print_table( + headers, + |table| { + // Print events + for ev in abi.events.iter().flat_map(|(_, events)| events) { + let types = parse_event_params(&ev.inputs); + let selector = ev.selector().to_string(); + table.add_row(["event", &format!("{}({})", ev.name, types), &selector]); + } - // Print errors - for er in abi.errors.iter().flat_map(|(_, errors)| errors) { - let selector = er.selector().to_string(); - table.add_row([ - "error", - &format!("{}({})", er.name, get_ty_sig(&er.inputs)), - &selector, - ]); - } + // Print errors + for er in abi.errors.iter().flat_map(|(_, errors)| errors) { + let selector = er.selector().to_string(); + table.add_row([ + "error", + &format!("{}({})", er.name, get_ty_sig(&er.inputs)), + &selector, + ]); + } - // Print functions - for func in abi.functions.iter().flat_map(|(_, f)| f) { - let selector = func.selector().to_string(); - let state_mut = func.state_mutability.as_json_str(); - let func_sig = if !func.outputs.is_empty() { - format!( - "{}({}) {state_mut} returns ({})", - func.name, - get_ty_sig(&func.inputs), - get_ty_sig(&func.outputs) - ) - } else { - format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs)) - }; - table.add_row(["function", &func_sig, &selector]); - } + // Print functions + for func in abi.functions.iter().flat_map(|(_, f)| f) { + let selector = func.selector().to_string(); + let state_mut = func.state_mutability.as_json_str(); + let func_sig = if !func.outputs.is_empty() { + format!( + "{}({}) {state_mut} returns ({})", + func.name, + get_ty_sig(&func.inputs), + get_ty_sig(&func.outputs) + ) + } else { + format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs)) + }; + table.add_row(["function", &func_sig, &selector]); + } - if let Some(constructor) = abi.constructor() { - let state_mut = constructor.state_mutability.as_json_str(); - table.add_row([ - "constructor", - &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)), - "", - ]); - } + if let Some(constructor) = abi.constructor() { + let state_mut = constructor.state_mutability.as_json_str(); + table.add_row([ + "constructor", + &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)), + "", + ]); + } - if let Some(fallback) = &abi.fallback { - let state_mut = fallback.state_mutability.as_json_str(); - table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]); - } + if let Some(fallback) = &abi.fallback { + let state_mut = fallback.state_mutability.as_json_str(); + table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]); + } - if let Some(receive) = &abi.receive { - let state_mut = receive.state_mutability.as_json_str(); - table.add_row(["receive", &format!("receive() {state_mut}"), ""]); - } - }) + if let Some(receive) = &abi.receive { + let state_mut = receive.state_mutability.as_json_str(); + table.add_row(["receive", &format!("receive() {state_mut}"), ""]); + } + }, + should_wrap, + ) } fn get_ty_sig(inputs: &[Param]) -> String { @@ -274,7 +281,10 @@ fn internal_ty(ty: &InternalType) -> String { } } -pub fn print_storage_layout(storage_layout: Option<&StorageLayout>) -> Result<()> { +pub fn print_storage_layout( + storage_layout: Option<&StorageLayout>, + should_wrap: bool, +) -> Result<()> { let Some(storage_layout) = storage_layout else { eyre::bail!("Could not get storage layout"); }; @@ -292,22 +302,29 @@ pub fn print_storage_layout(storage_layout: Option<&StorageLayout>) -> Result<() Cell::new("Contract"), ]; - print_table(headers, |table| { - for slot in &storage_layout.storage { - let storage_type = storage_layout.types.get(&slot.storage_type); - table.add_row([ - slot.label.as_str(), - storage_type.map_or("?", |t| &t.label), - &slot.slot, - &slot.offset.to_string(), - storage_type.map_or("?", |t| &t.number_of_bytes), - &slot.contract, - ]); - } - }) + print_table( + headers, + |table| { + for slot in &storage_layout.storage { + let storage_type = storage_layout.types.get(&slot.storage_type); + table.add_row([ + slot.label.as_str(), + storage_type.map_or("?", |t| &t.label), + &slot.slot, + &slot.offset.to_string(), + storage_type.map_or("?", |t| &t.number_of_bytes), + &slot.contract, + ]); + } + }, + should_wrap, + ) } -fn print_method_identifiers(method_identifiers: &Option>) -> Result<()> { +fn print_method_identifiers( + method_identifiers: &Option>, + should_wrap: bool, +) -> Result<()> { let Some(method_identifiers) = method_identifiers else { eyre::bail!("Could not get method identifiers"); }; @@ -318,14 +335,18 @@ fn print_method_identifiers(method_identifiers: &Option let headers = vec![Cell::new("Method"), Cell::new("Identifier")]; - print_table(headers, |table| { - for (method, identifier) in method_identifiers { - table.add_row([method, identifier]); - } - }) + print_table( + headers, + |table| { + for (method, identifier) in method_identifiers { + table.add_row([method, identifier]); + } + }, + should_wrap, + ) } -fn print_errors_events(map: &Map, is_err: bool) -> Result<()> { +fn print_errors_events(map: &Map, is_err: bool, should_wrap: bool) -> Result<()> { if shell::is_json() { return print_json(map); } @@ -335,17 +356,28 @@ fn print_errors_events(map: &Map, is_err: bool) -> Result<()> { } else { vec![Cell::new("Event"), Cell::new("Topic")] }; - print_table(headers, |table| { - for (method, selector) in map { - table.add_row([method, selector.as_str().unwrap()]); - } - }) + print_table( + headers, + |table| { + for (method, selector) in map { + table.add_row([method, selector.as_str().unwrap()]); + } + }, + should_wrap, + ) } -fn print_table(headers: Vec, add_rows: impl FnOnce(&mut Table)) -> Result<()> { +fn print_table( + headers: Vec, + add_rows: impl FnOnce(&mut Table), + should_wrap: bool, +) -> Result<()> { let mut table = Table::new(); table.apply_modifier(UTF8_ROUND_CORNERS); table.set_header(headers); + if should_wrap { + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + } add_rows(&mut table); sh_println!("\n{table}\n")?; Ok(()) From 40aa0faf344e5f824692b717b7330197aaff06b5 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Tue, 29 Jul 2025 12:41:15 +0530 Subject: [PATCH 02/10] chore: add doc comment --- crates/forge/src/cmd/inspect.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 0bd54db7d93e1..cb67ae6fb8849 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -41,6 +41,7 @@ pub struct InspectArgs { #[arg(long, short, help_heading = "Display options")] pub strip_yul_comments: bool, + /// Whether to wrap the table to the terminal width. #[arg(long, short, help_heading = "Display options")] pub wrap: bool, } From 7197ad4bc61344adada2f2c44f47eeba8d3345dd Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Sat, 9 Aug 2025 13:37:00 +0530 Subject: [PATCH 03/10] chore: add tests --- crates/forge/tests/cli/cmd.rs | 60 ++++++++++++++++++++++++++++++++++ crates/test-utils/src/util.rs | 61 ++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 0be5e77956fc5..9024ea70fa1f8 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3586,6 +3586,66 @@ forgetest!(inspect_custom_counter_method_identifiers, |prj, cmd| { ╰----------------------------+------------╯ +"#]]); +}); + +const CUSTOM_COUNTER_HUGE_METHOD_IDENTIFIERS: &str = r#" +contract Counter { + struct BigStruct { + uint256 a; + uint256 b; + uint256 c; + uint256 d; + uint256 e; + uint256 f; + } + + struct NestedBigStruct { + BigStruct a; + BigStruct b; + BigStruct c; + } + + function hugeIdentifier(NestedBigStruct[] calldata _bigStructs, NestedBigStruct calldata _bigStruct) external {} +} +"#; + +forgetest!(inspect_custom_counter_very_huge_method_identifiers_unwrapped, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER_HUGE_METHOD_IDENTIFIERS).unwrap(); + + cmd.args(["inspect", "Counter", "method-identifiers"]).assert_success().stdout_eq(str![[r#" + +╭-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------╮ +| Method | Identifier | ++================================================================================================================================================================================================================================================================================================================================================+ +| hugeIdentifier(((uint256,uint256,uint256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint256))[],((uint256,uint256,uint256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint256))) | f38dafbb | +╰-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------╯ + + +"#]]); +}); + +forgetest!(inspect_custom_counter_very_huge_method_identifiers_wrapped, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER_HUGE_METHOD_IDENTIFIERS).unwrap(); + + // Force a specific terminal width to test wrapping + cmd.args(["inspect", "--wrap", "Counter", "method-identifiers"]) + .assert_with_terminal_width(80) + .success() + .stdout_eq(str![[r#" + +╭-----------------------------------------------------------------+------------╮ +| Method | Identifier | ++==============================================================================+ +| hugeIdentifier(((uint256,uint256,uint256,uint256,uint256,uint25 | f38dafbb | +| 6),(uint256,uint256,uint256,uint256,uint256,uint256),(uint256,u | | +| int256,uint256,uint256,uint256,uint256))[],((uint256,uint256,ui | | +| nt256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256 | | +| ,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint | | +| 256))) | | +╰-----------------------------------------------------------------+------------╯ + + "#]]); }); diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 3f23acb75e7d8..31e1a9ae512cd 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -948,6 +948,18 @@ impl TestCommand { assert } + /// Runs the command with specific terminal width, returning a [`snapbox`] object to assert the + /// command output. + #[track_caller] + pub fn assert_with_terminal_width(&mut self, width: u16) -> OutputAssert { + let assert = + OutputAssert::new(self.try_execute_via_tty_with_size(Some((width, 24))).unwrap()); + if self.redact_output { + return assert.with_assert(test_assert()); + } + assert + } + /// Runs the command and asserts that it resulted in success. #[track_caller] pub fn assert_success(&mut self) -> OutputAssert { @@ -1017,7 +1029,6 @@ impl TestCommand { #[track_caller] pub fn try_execute(&mut self) -> std::io::Result { - println!("executing {:?}", self.cmd); let mut child = self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?; if let Some(fun) = self.stdin_fun.take() { @@ -1025,6 +1036,54 @@ impl TestCommand { } child.wait_with_output() } + + #[track_caller] + fn try_execute_via_tty_with_size( + &mut self, + size: Option<(u16, u16)>, + ) -> std::io::Result { + // Get the program and args from the current command + let program = self.cmd.get_program().to_string_lossy().to_string(); + let args: Vec = + self.cmd.get_args().map(|arg| arg.to_string_lossy().to_string()).collect(); + + // Build the command string + let mut cmd_str = program; + for arg in &args { + cmd_str.push(' '); + // Simple shell escaping - wrap in single quotes and escape any single quotes + if arg.contains(' ') || arg.contains('"') || arg.contains('\'') { + cmd_str.push('\''); + cmd_str.push_str(&arg.replace("'", "'\\'\''")); + cmd_str.push('\''); + } else { + cmd_str.push_str(arg); + } + } + + // If size is specified, wrap the command with stty to set terminal size + if let Some((cols, rows)) = size { + cmd_str = format!("stty cols {cols} rows {rows}; {cmd_str}"); + } + + // Use script command to run in a pseudo-terminal + let mut script_cmd = Command::new("script"); + script_cmd + .arg("-q") // quiet mode, no script started/done messages + .arg("-c") // command to run + .arg(&cmd_str) + .arg("/dev/null") // don't save typescript file + .current_dir(self.cmd.get_current_dir().unwrap_or(Path::new("."))); + + // Copy environment variables + for (key, val) in self.cmd.get_envs() { + if let (Some(key), Some(val)) = (key.to_str(), val) { + script_cmd.env(key, val); + } + } + + script_cmd.output() + } } fn test_assert() -> snapbox::Assert { From b09cffeaba768ad70990ada1c9fd5fc324f08ab4 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Sat, 9 Aug 2025 14:06:07 +0530 Subject: [PATCH 04/10] chore: use portable-pty for running tests that need specific terminal size --- Cargo.lock | 95 +++++++++++++++++++++++++++++++++-- crates/test-utils/Cargo.toml | 1 + crates/test-utils/src/util.rs | 81 +++++++++++++++++------------ 3 files changed, 140 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22b04c72fb333..5442304118d93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2603,6 +2603,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -3580,6 +3586,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dtoa" version = "1.0.10" @@ -3945,6 +3957,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4855,6 +4878,7 @@ dependencies = [ "foundry-config", "idna_adapter", "parking_lot", + "portable-pty", "rand 0.9.2", "regex", "serde_json", @@ -6581,6 +6605,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -6589,7 +6625,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.1", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", ] @@ -6601,7 +6637,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.1", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", ] @@ -7340,6 +7376,27 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.28.0", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -7641,7 +7698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -7681,7 +7738,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.5.10", @@ -8855,6 +8912,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serial2" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26e1e5956803a69ddd72ce2de337b577898801528749565def03515f82bad5bb" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + [[package]] name = "sha1" version = "0.10.6" @@ -8919,6 +8987,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -11164,6 +11242,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index c8f4184af73c0..65d6ec1c93df0 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -33,6 +33,7 @@ rand.workspace = true snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } tempfile.workspace = true ui_test = "0.30.2" +portable-pty = "0.9.0" # Pinned dependencies. See /Cargo.toml. [target.'cfg(any())'.dependencies] diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 31e1a9ae512cd..c1a49d75dfd60 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -1038,51 +1038,66 @@ impl TestCommand { } #[track_caller] - fn try_execute_via_tty_with_size( + pub fn try_execute_via_tty_with_size( &mut self, size: Option<(u16, u16)>, ) -> std::io::Result { - // Get the program and args from the current command - let program = self.cmd.get_program().to_string_lossy().to_string(); - let args: Vec = - self.cmd.get_args().map(|arg| arg.to_string_lossy().to_string()).collect(); - - // Build the command string - let mut cmd_str = program; - for arg in &args { - cmd_str.push(' '); - // Simple shell escaping - wrap in single quotes and escape any single quotes - if arg.contains(' ') || arg.contains('"') || arg.contains('\'') { - cmd_str.push('\''); - cmd_str.push_str(&arg.replace("'", "'\\'\''")); - cmd_str.push('\''); - } else { - cmd_str.push_str(arg); - } - } + use portable_pty::{CommandBuilder, PtySize, native_pty_system}; + + // Set default size or use provided size + let (cols, rows) = size.unwrap_or((120, 24)); - // If size is specified, wrap the command with stty to set terminal size - if let Some((cols, rows)) = size { - cmd_str = format!("stty cols {cols} rows {rows}; {cmd_str}"); + // Create a new pty with specified size + let pty_system = native_pty_system(); + let pty_size = PtySize { rows, cols, pixel_width: 0, pixel_height: 0 }; + + let pair = pty_system.openpty(pty_size).map_err(std::io::Error::other)?; + + // Build the command + let mut cmd = CommandBuilder::new(self.cmd.get_program()); + for arg in self.cmd.get_args() { + cmd.arg(arg); } - // Use script command to run in a pseudo-terminal - let mut script_cmd = Command::new("script"); - script_cmd - .arg("-q") // quiet mode, no script started/done messages - .arg("-c") // command to run - .arg(&cmd_str) - .arg("/dev/null") // don't save typescript file - .current_dir(self.cmd.get_current_dir().unwrap_or(Path::new("."))); + // Set current directory + if let Some(dir) = self.cmd.get_current_dir() { + cmd.cwd(dir); + } // Copy environment variables for (key, val) in self.cmd.get_envs() { - if let (Some(key), Some(val)) = (key.to_str(), val) { - script_cmd.env(key, val); + if let (Some(val), Some(key_str)) = (val, key.to_str()) { + cmd.env(key_str, val); } } - script_cmd.output() + // Spawn the command in the pty + let mut child = pair.slave.spawn_command(cmd).map_err(std::io::Error::other)?; + + // Close the slave end + drop(pair.slave); + + // Read output from the master end + let mut reader = pair.master.try_clone_reader().map_err(std::io::Error::other)?; + + let mut output = Vec::new(); + reader.read_to_end(&mut output)?; + + // Wait for the child to finish + let exit_status = child.wait().map_err(std::io::Error::other)?; + + // Construct the output + #[cfg(unix)] + let status = + std::os::unix::process::ExitStatusExt::from_raw(exit_status.exit_code() as i32); + #[cfg(windows)] + let status = std::process::ExitStatus::from_raw(exit_status.exit_code() as u32); + + Ok(Output { + status, + stdout: output.clone(), + stderr: Vec::new(), // PTY combines stdout and stderr + }) } } From b9d397381c4351365bb0d821fd40668833a4785e Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Sat, 9 Aug 2025 14:15:36 +0530 Subject: [PATCH 05/10] chore: fix test --- crates/test-utils/src/util.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index c1a49d75dfd60..11df6ceeb1756 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -1091,7 +1091,8 @@ impl TestCommand { let status = std::os::unix::process::ExitStatusExt::from_raw(exit_status.exit_code() as i32); #[cfg(windows)] - let status = std::process::ExitStatus::from_raw(exit_status.exit_code() as u32); + let status = + std::os::windows::process::ExitStatusExt::from_raw(exit_status.exit_code() as u32); Ok(Output { status, From 9c31e8e2609ec6d767617a350d14b8e330b4e604 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Wed, 20 Aug 2025 06:05:01 +0000 Subject: [PATCH 06/10] chore: remove portable-pty --- Cargo.lock | 95 ++--------------------------------- crates/forge/tests/cli/cmd.rs | 24 --------- crates/test-utils/Cargo.toml | 1 - crates/test-utils/src/util.rs | 76 ---------------------------- 4 files changed, 4 insertions(+), 192 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d87b968cf481..9d9694f83a7be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2603,12 +2603,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -3586,12 +3580,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "dtoa" version = "1.0.10" @@ -3957,17 +3945,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "fixed-hash" version = "0.8.0" @@ -4877,7 +4854,6 @@ dependencies = [ "foundry-config", "idna_adapter", "parking_lot", - "portable-pty", "rand 0.9.2", "regex", "serde_json", @@ -6594,18 +6570,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -6614,7 +6578,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.2", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -6626,7 +6590,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.2", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -7365,27 +7329,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix 0.28.0", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -7719,7 +7662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -7759,7 +7702,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", "socket2 0.5.10", @@ -8945,17 +8888,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serial2" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26e1e5956803a69ddd72ce2de337b577898801528749565def03515f82bad5bb" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - [[package]] name = "sha1" version = "0.10.6" @@ -9020,16 +8952,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - [[package]] name = "shell-words" version = "1.1.0" @@ -11274,15 +11196,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winsafe" version = "0.0.19" diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index e2c0b8f288cf5..0a581e3c23476 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3669,30 +3669,6 @@ forgetest!(inspect_custom_counter_very_huge_method_identifiers_unwrapped, |prj, ╰-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------╯ -"#]]); -}); - -forgetest!(inspect_custom_counter_very_huge_method_identifiers_wrapped, |prj, cmd| { - prj.add_source("Counter.sol", CUSTOM_COUNTER_HUGE_METHOD_IDENTIFIERS).unwrap(); - - // Force a specific terminal width to test wrapping - cmd.args(["inspect", "--wrap", "Counter", "method-identifiers"]) - .assert_with_terminal_width(80) - .success() - .stdout_eq(str![[r#" - -╭-----------------------------------------------------------------+------------╮ -| Method | Identifier | -+==============================================================================+ -| hugeIdentifier(((uint256,uint256,uint256,uint256,uint256,uint25 | f38dafbb | -| 6),(uint256,uint256,uint256,uint256,uint256,uint256),(uint256,u | | -| int256,uint256,uint256,uint256,uint256))[],((uint256,uint256,ui | | -| nt256,uint256,uint256,uint256),(uint256,uint256,uint256,uint256 | | -| ,uint256,uint256),(uint256,uint256,uint256,uint256,uint256,uint | | -| 256))) | | -╰-----------------------------------------------------------------+------------╯ - - "#]]); }); diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 65d6ec1c93df0..c8f4184af73c0 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -33,7 +33,6 @@ rand.workspace = true snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } tempfile.workspace = true ui_test = "0.30.2" -portable-pty = "0.9.0" # Pinned dependencies. See /Cargo.toml. [target.'cfg(any())'.dependencies] diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 11df6ceeb1756..c6f19120e5d72 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -948,18 +948,6 @@ impl TestCommand { assert } - /// Runs the command with specific terminal width, returning a [`snapbox`] object to assert the - /// command output. - #[track_caller] - pub fn assert_with_terminal_width(&mut self, width: u16) -> OutputAssert { - let assert = - OutputAssert::new(self.try_execute_via_tty_with_size(Some((width, 24))).unwrap()); - if self.redact_output { - return assert.with_assert(test_assert()); - } - assert - } - /// Runs the command and asserts that it resulted in success. #[track_caller] pub fn assert_success(&mut self) -> OutputAssert { @@ -1036,70 +1024,6 @@ impl TestCommand { } child.wait_with_output() } - - #[track_caller] - pub fn try_execute_via_tty_with_size( - &mut self, - size: Option<(u16, u16)>, - ) -> std::io::Result { - use portable_pty::{CommandBuilder, PtySize, native_pty_system}; - - // Set default size or use provided size - let (cols, rows) = size.unwrap_or((120, 24)); - - // Create a new pty with specified size - let pty_system = native_pty_system(); - let pty_size = PtySize { rows, cols, pixel_width: 0, pixel_height: 0 }; - - let pair = pty_system.openpty(pty_size).map_err(std::io::Error::other)?; - - // Build the command - let mut cmd = CommandBuilder::new(self.cmd.get_program()); - for arg in self.cmd.get_args() { - cmd.arg(arg); - } - - // Set current directory - if let Some(dir) = self.cmd.get_current_dir() { - cmd.cwd(dir); - } - - // Copy environment variables - for (key, val) in self.cmd.get_envs() { - if let (Some(val), Some(key_str)) = (val, key.to_str()) { - cmd.env(key_str, val); - } - } - - // Spawn the command in the pty - let mut child = pair.slave.spawn_command(cmd).map_err(std::io::Error::other)?; - - // Close the slave end - drop(pair.slave); - - // Read output from the master end - let mut reader = pair.master.try_clone_reader().map_err(std::io::Error::other)?; - - let mut output = Vec::new(); - reader.read_to_end(&mut output)?; - - // Wait for the child to finish - let exit_status = child.wait().map_err(std::io::Error::other)?; - - // Construct the output - #[cfg(unix)] - let status = - std::os::unix::process::ExitStatusExt::from_raw(exit_status.exit_code() as i32); - #[cfg(windows)] - let status = - std::os::windows::process::ExitStatusExt::from_raw(exit_status.exit_code() as u32); - - Ok(Output { - status, - stdout: output.clone(), - stderr: Vec::new(), // PTY combines stdout and stderr - }) - } } fn test_assert() -> snapbox::Assert { From 456a02b589bbfa97da22dd3f5d632dcb3a5bc9cf Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Wed, 20 Aug 2025 06:25:34 +0000 Subject: [PATCH 07/10] chore: fix clippy --- crates/common/src/contracts.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index dda35cffeace4..e777f87276ccc 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -332,9 +332,7 @@ impl ContractsByArtifact { /// Returns the abi and the contract name. pub fn find_abi_by_name_or_src_path(&self, name_or_path: &str) -> Option<(JsonAbi, String)> { self.iter() - .find(|(artifact, _)| { - artifact.name == name_or_path || artifact.source == PathBuf::from(name_or_path) - }) + .find(|(artifact, _)| artifact.name == name_or_path || artifact.source == name_or_path) .map(|(_, contract)| (contract.abi.clone(), contract.name.clone())) } From f13ef6a245c3c4e80dc6e15461efec74fb9076b0 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Wed, 20 Aug 2025 06:32:32 +0000 Subject: [PATCH 08/10] chore: revert clippy fix --- crates/common/src/contracts.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index e777f87276ccc..dda35cffeace4 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -332,7 +332,9 @@ impl ContractsByArtifact { /// Returns the abi and the contract name. pub fn find_abi_by_name_or_src_path(&self, name_or_path: &str) -> Option<(JsonAbi, String)> { self.iter() - .find(|(artifact, _)| artifact.name == name_or_path || artifact.source == name_or_path) + .find(|(artifact, _)| { + artifact.name == name_or_path || artifact.source == PathBuf::from(name_or_path) + }) .map(|(_, contract)| (contract.abi.clone(), contract.name.clone())) } From c2ca84483c000e64fa363a6cf2a2bad895c776bd Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Tue, 26 Aug 2025 11:38:25 +0000 Subject: [PATCH 09/10] chore: fix build --- crates/forge/src/cmd/inspect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 37b29b3eb1902..7783905c94279 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -86,7 +86,7 @@ impl InspectArgs { // Match on ContractArtifactFields and pretty-print match field { ContractArtifactField::Abi => { - let abi = artifact.abi.as_ref().ok_or_else(|| missing_error("ABI")); + let abi = artifact.abi.as_ref().ok_or_else(|| missing_error("ABI"))?; print_abi(abi, wrap)?; } ContractArtifactField::Bytecode => { From ca358324deccbb7a101d40dfe2945d9415f16ba6 Mon Sep 17 00:00:00 2001 From: 0xferrous <0xferrous@proton.me> Date: Tue, 26 Aug 2025 12:03:50 +0000 Subject: [PATCH 10/10] chore: nit --- crates/test-utils/src/util.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index c6f19120e5d72..3f23acb75e7d8 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -1017,6 +1017,7 @@ impl TestCommand { #[track_caller] pub fn try_execute(&mut self) -> std::io::Result { + println!("executing {:?}", self.cmd); let mut child = self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?; if let Some(fun) = self.stdin_fun.take() {