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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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,