diff --git a/tests/test_utils.rs b/tests/test_utils.rs index 0eca61f..0644ba3 100644 --- a/tests/test_utils.rs +++ b/tests/test_utils.rs @@ -23,100 +23,74 @@ use tracing as _; use tracing_subscriber as _; /// Returns a command that sleeps for the specified number of seconds. -#[cfg(windows)] #[must_use] pub fn sleep_cmd(seconds: u32) -> (&'static str, Vec) { - // Use ping as a sleep alternative on Windows - // Pings localhost N+1 times with 1-second intervals (approximately N seconds total) - let pings = seconds.saturating_add(1); - ( - "ping", - vec!["-n".to_owned(), pings.to_string(), "127.0.0.1".to_owned()], - ) -} - -/// Returns a command that sleeps for the specified number of seconds. -#[cfg(not(windows))] -#[must_use] -pub fn sleep_cmd(seconds: u32) -> (&'static str, Vec) { - ("sleep", vec![seconds.to_string()]) -} - -/// Returns a command that echoes text to `stdout`, then sleeps. -#[cfg(windows)] -#[must_use] -pub fn echo_with_sleep_cmd(text: &str, seconds: u32) -> (&'static str, Vec) { - let pings = seconds.saturating_add(1); - ( - "cmd", - vec![ - "/C".to_owned(), - format!("echo {text} && ping -n {pings} 127.0.0.1 >nul"), - ], - ) + cfg_select! { + windows => { + // Use ping as a sleep alternative on Windows + // Pings localhost N+1 times with 1-second intervals (approximately N seconds total) + let pings = seconds.saturating_add(1); + ( + "ping", + vec!["-n".to_owned(), pings.to_string(), "127.0.0.1".to_owned()], + ) + } + _ => { + ("sleep", vec![seconds.to_string()]) + } + } } /// Returns a command that echoes text to `stdout`, then sleeps. -#[cfg(not(windows))] #[must_use] pub fn echo_with_sleep_cmd(text: &str, seconds: u32) -> (&'static str, Vec) { - ( - "bash", - vec!["-c".to_owned(), format!("echo '{text}' && sleep {seconds}")], - ) -} - -/// Returns a command that echoes text to `stderr`, then sleeps. -#[cfg(windows)] -#[must_use] -pub fn stderr_echo_with_sleep_cmd(text: &str, seconds: u32) -> (&'static str, Vec) { - let pings = seconds.saturating_add(1); - ( - "cmd", - vec![ - "/C".to_owned(), - format!("echo {text} 1>&2 && ping -n {pings} 127.0.0.1 >nul"), - ], - ) + cfg_select! { + windows => { + let pings = seconds.saturating_add(1); + ( + "cmd", + vec![ + "/C".to_owned(), + format!("echo {text} && ping -n {pings} 127.0.0.1 >nul"), + ], + ) + } + _ => { + ( + "bash", + vec!["-c".to_owned(), format!("echo '{text}' && sleep {seconds}")], + ) + } + } } /// Returns a command that echoes text to `stderr`, then sleeps. -#[cfg(not(windows))] #[must_use] pub fn stderr_echo_with_sleep_cmd(text: &str, seconds: u32) -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - format!("echo '{text}' >&2 && sleep {seconds}"), - ], - ) -} - -/// Returns a command that echoes to `stderr`, sleeps, echoes again, then sleeps more. -#[cfg(windows)] -#[must_use] -pub fn multi_echo_stderr_cmd( - buffered: &str, - sleep1: f32, - realtime: &str, - sleep2: u32, -) -> (&'static str, Vec) { - let pings = sleep2.saturating_add(1); - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!( - "[Console]::Error.WriteLine('{buffered}'); Start-Sleep -Seconds {sleep1}; [Console]::Error.WriteLine('{realtime}'); ping -n {pings} 127.0.0.1 >$null" - ), - ], - ) + cfg_select! { + windows => { + let pings = seconds.saturating_add(1); + ( + "cmd", + vec![ + "/C".to_owned(), + format!("echo {text} 1>&2 && ping -n {pings} 127.0.0.1 >nul"), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + format!("echo '{text}' >&2 && sleep {seconds}"), + ], + ) + } + } } /// Returns a command that echoes to `stderr`, sleeps, echoes again, then sleeps more. -#[cfg(not(windows))] #[must_use] pub fn multi_echo_stderr_cmd( buffered: &str, @@ -124,17 +98,33 @@ pub fn multi_echo_stderr_cmd( realtime: &str, sleep2: u32, ) -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - format!("echo '{buffered}' >&2; sleep {sleep1}; echo '{realtime}' >&2; sleep {sleep2}"), - ], - ) + cfg_select! { + windows => { + let pings = sleep2.saturating_add(1); + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!( + "[Console]::Error.WriteLine('{buffered}'); Start-Sleep -Seconds {sleep1}; [Console]::Error.WriteLine('{realtime}'); ping -n {pings} 127.0.0.1 >$null" + ), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + format!("echo '{buffered}' >&2; sleep {sleep1}; echo '{realtime}' >&2; sleep {sleep2}"), + ], + ) + } + } } /// Returns a command that echoes to `stdout`, sleeps, echoes again, then sleeps more. -#[cfg(windows)] #[must_use] pub fn multi_echo_stdout_cmd( buffered: &str, @@ -142,43 +132,42 @@ pub fn multi_echo_stdout_cmd( realtime: &str, sleep2: u32, ) -> (&'static str, Vec) { - let pings = sleep2.saturating_add(1); - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!( - "Write-Output '{buffered}'; Start-Sleep -Seconds {sleep1}; Write-Output '{realtime}'; ping -n {pings} 127.0.0.1 >$null" - ), - ], - ) + cfg_select! { + windows => { + let pings = sleep2.saturating_add(1); + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!( + "Write-Output '{buffered}'; Start-Sleep -Seconds {sleep1}; Write-Output '{realtime}'; ping -n {pings} 127.0.0.1 >$null" + ), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + format!("echo '{buffered}'; sleep {sleep1}; echo '{realtime}'; sleep {sleep2}",), + ], + ) + } + } } -/// Returns a command that echoes to `stdout`, sleeps, echoes again, then sleeps more. -#[cfg(not(windows))] -#[must_use] -pub fn multi_echo_stdout_cmd( - buffered: &str, - sleep1: f32, - realtime: &str, - sleep2: u32, -) -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - format!("echo '{buffered}'; sleep {sleep1}; echo '{realtime}'; sleep {sleep2}",), - ], - ) -} +// `cat_cmd` uses `#[cfg]` item-level guards instead of `cfg_select!` because the two branches have +// different constness: the non-Windows branch can be `const fn`, the Windows one cannot. `cfg_select!` +// inside a single function body cannot express per-branch constness. /// Returns a command that reads from `stdin` and echoes to `stdout` (like `cat`). +// Use Python for reliable line-by-line I/O on Windows. +// `-u` flag disables buffering for immediate output. #[cfg(windows)] #[must_use] pub fn cat_cmd() -> (&'static str, Vec) { - // Use Python for reliable line-by-line I/O on Windows. - // `-u` flag disables buffering for immediate output. ( python_cmd(), vec![ @@ -197,273 +186,256 @@ pub const fn cat_cmd() -> (&'static str, Vec) { } /// Returns a command that continuously reads from `stdin` and writes "response" to `stdout`. -#[cfg(windows)] -#[must_use] -pub fn loop_stdin_to_stdout_cmd() -> (&'static str, Vec) { - // PowerShell script that reads line by line and echoes - // Use [Console]::In to read from `stdin` and [Console]::WriteLine() for immediate flushing - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - "while($line = [Console]::In.ReadLine()) { [Console]::WriteLine('response') }" - .to_owned(), - ], - ) -} - -/// Returns a command that continuously reads from `stdin` and writes "response" to `stdout`. -#[cfg(not(windows))] #[must_use] pub fn loop_stdin_to_stdout_cmd() -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - "while true; do read line; echo response; done".to_owned(), - ], - ) -} - -/// Returns a command that continuously writes "error" to `stderr` in a loop. -#[cfg(windows)] -#[must_use] -pub fn continuous_stderr_loop_cmd() -> (&'static str, Vec) { - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - "while($true) { [Console]::Error.WriteLine('error'); Start-Sleep -Milliseconds 100 }" - .to_owned(), - ], - ) + cfg_select! { + windows => { + // PowerShell script that reads line by line and echoes + // Use [Console]::In to read from `stdin` and [Console]::WriteLine() for immediate flushing + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + "while($line = [Console]::In.ReadLine()) { [Console]::WriteLine('response') }" + .to_owned(), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + "while true; do read line; echo response; done".to_owned(), + ], + ) + } + } } /// Returns a command that continuously writes "error" to `stderr` in a loop. -#[cfg(not(windows))] #[must_use] pub fn continuous_stderr_loop_cmd() -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - "while true; do echo error >&2; sleep 0.1; done".to_owned(), - ], - ) -} - -/// Returns a command that generates a large block of output (repeated 'A' characters). -#[cfg(windows)] -#[must_use] -pub fn generate_large_output_cmd(size: usize) -> (&'static str, Vec) { - // Generate large output using PowerShell - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!("'A' * {size}; ping -n 11 127.0.0.1 >$null"), - ], - ) + cfg_select! { + windows => { + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + "while($true) { [Console]::Error.WriteLine('error'); Start-Sleep -Milliseconds 100 }" + .to_owned(), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + "while true; do echo error >&2; sleep 0.1; done".to_owned(), + ], + ) + } + } } /// Returns a command that generates a large block of output (repeated 'A' characters). -#[cfg(not(windows))] #[must_use] pub fn generate_large_output_cmd(size: usize) -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - format!("head -c {size} /dev/zero | tr '\\0' 'A'; sleep 10"), - ], - ) -} - -/// Returns a command that outputs numbered lines to both `stdout` and `stderr` with delays. -#[cfg(windows)] -#[must_use] -pub fn numbered_output_loop_cmd(count: u32, interval_ms: u32) -> (&'static str, Vec) { - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!( - "1..{count} | ForEach-Object {{ Write-Output \"stdout_line_$_\"; [Console]::Error.WriteLine(\"stderr_line_$_\"); Start-Sleep -Milliseconds {interval_ms} }}" - ), - ], - ) + cfg_select! { + windows => { + // Generate large output using PowerShell + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!("'A' * {size}; ping -n 11 127.0.0.1 >$null"), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + format!("head -c {size} /dev/zero | tr '\\0' 'A'; sleep 10"), + ], + ) + } + } } /// Returns a command that outputs numbered lines to both `stdout` and `stderr` with delays. -#[cfg(not(windows))] #[must_use] pub fn numbered_output_loop_cmd(count: u32, interval_ms: u32) -> (&'static str, Vec) { - #[expect( - clippy::integer_division, - reason = "Intentional conversion of milliseconds to seconds with fractional part" - )] - let seconds = interval_ms / 1000; - let millis = interval_ms % 1000; - let interval_sec = format!("{seconds}.{millis:03}"); - ( - "bash", - vec![ - "-c".to_owned(), - format!( - "for i in {{1..{count}}}; do echo \"stdout_line_$i\"; echo \"stderr_line_$i\" >&2; sleep {interval_sec}; done" - ), - ], - ) -} - -/// Returns a command that emits timed `stderr` output for testing reconnection scenarios. -#[cfg(windows)] -#[must_use] -pub fn complex_stderr_reconnect_cmd() -> (&'static str, Vec) { - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - concat!( - "[Console]::Error.WriteLine('before_connection'); Start-Sleep -Milliseconds 500; ", - "[Console]::Error.WriteLine('during_first_connection'); Start-Sleep -Milliseconds 1000; ", - "[Console]::Error.WriteLine('trigger_disconnect'); Start-Sleep -Milliseconds 1500; ", - "[Console]::Error.WriteLine('while_disconnected'); Start-Sleep -Milliseconds 2000; ", - "[Console]::Error.WriteLine('during_second_connection'); Start-Sleep -Seconds 10" - ).to_owned(), - ], - ) + cfg_select! { + windows => { + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!( + "1..{count} | ForEach-Object {{ Write-Output \"stdout_line_$_\"; [Console]::Error.WriteLine(\"stderr_line_$_\"); Start-Sleep -Milliseconds {interval_ms} }}" + ), + ], + ) + } + _ => { + #[expect( + clippy::integer_division, + reason = "Intentional conversion of milliseconds to seconds with fractional part" + )] + let seconds = interval_ms / 1000; + let millis = interval_ms % 1000; + let interval_sec = format!("{seconds}.{millis:03}"); + ( + "bash", + vec![ + "-c".to_owned(), + format!( + "for i in {{1..{count}}}; do echo \"stdout_line_$i\"; echo \"stderr_line_$i\" >&2; sleep {interval_sec}; done" + ), + ], + ) + } + } } /// Returns a command that emits timed `stderr` output for testing reconnection scenarios. -#[cfg(not(windows))] #[must_use] pub fn complex_stderr_reconnect_cmd() -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - concat!( - "echo 'before_connection' >&2; sleep 0.5; ", - "echo 'during_first_connection' >&2; sleep 1; ", - "echo 'trigger_disconnect' >&2; sleep 1.5; ", - "echo 'while_disconnected' >&2; sleep 2; ", - "echo 'during_second_connection' >&2; sleep 10", + cfg_select! { + windows => { + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + concat!( + "[Console]::Error.WriteLine('before_connection'); Start-Sleep -Milliseconds 500; ", + "[Console]::Error.WriteLine('during_first_connection'); Start-Sleep -Milliseconds 1000; ", + "[Console]::Error.WriteLine('trigger_disconnect'); Start-Sleep -Milliseconds 1500; ", + "[Console]::Error.WriteLine('while_disconnected'); Start-Sleep -Milliseconds 2000; ", + "[Console]::Error.WriteLine('during_second_connection'); Start-Sleep -Seconds 10" + ).to_owned(), + ], ) - .to_owned(), - ], - ) -} - -/// Returns a command that outputs to both `stdout` and `stderr`, then sleeps. -#[cfg(windows)] -#[must_use] -pub fn combined_output_cmd( - stdout_msg: &str, - stderr_msg: &str, - sleep_sec: u32, -) -> (&'static str, Vec) { - let pings = sleep_sec.saturating_add(1); - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!( - "Write-Output '{stdout_msg}'; [Console]::Error.WriteLine('{stderr_msg}'); ping -n {pings} 127.0.0.1 >$null" - ), - ], - ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + concat!( + "echo 'before_connection' >&2; sleep 0.5; ", + "echo 'during_first_connection' >&2; sleep 1; ", + "echo 'trigger_disconnect' >&2; sleep 1.5; ", + "echo 'while_disconnected' >&2; sleep 2; ", + "echo 'during_second_connection' >&2; sleep 10", + ) + .to_owned(), + ], + ) + } + } } /// Returns a command that outputs to both `stdout` and `stderr`, then sleeps. -#[cfg(not(windows))] #[must_use] pub fn combined_output_cmd( stdout_msg: &str, stderr_msg: &str, sleep_sec: u32, ) -> (&'static str, Vec) { - ( - "bash", - vec![ - "-c".to_owned(), - format!("echo '{stdout_msg}'; echo '{stderr_msg}' >&2; sleep {sleep_sec}"), - ], - ) -} - -/// Returns a command that echoes all provided arguments to `stdout`. -#[cfg(windows)] -#[must_use] -pub fn echo_args_cmd(args: &[&str]) -> (&'static str, Vec) { - let mut cmd_args = vec!["/C".to_owned()]; - // Use echo %* to print all arguments on Windows (requires a batch context) - // Alternative: build the echo command with all args - let echo_str = args.join(" "); - cmd_args.push(format!("echo {echo_str} && ping -n 6 127.0.0.1 >nul")); - ("cmd", cmd_args) + cfg_select! { + windows => { + let pings = sleep_sec.saturating_add(1); + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!( + "Write-Output '{stdout_msg}'; [Console]::Error.WriteLine('{stderr_msg}'); ping -n {pings} 127.0.0.1 >$null" + ), + ], + ) + } + _ => { + ( + "bash", + vec![ + "-c".to_owned(), + format!("echo '{stdout_msg}'; echo '{stderr_msg}' >&2; sleep {sleep_sec}"), + ], + ) + } + } } /// Returns a command that echoes all provided arguments to `stdout`. -#[cfg(not(windows))] #[must_use] pub fn echo_args_cmd(args: &[&str]) -> (&'static str, Vec) { - let mut script_args = vec![ - "-c".to_owned(), - "echo $@ && sleep 5".to_owned(), - "--".to_owned(), - ]; - script_args.extend(args.iter().map(ToString::to_string)); - ("bash", script_args) -} - -/// Returns a command that outputs a message and exits quickly after a brief delay. -#[cfg(windows)] -#[must_use] -pub fn short_lived_cmd(msg: &str, sleep_ms: u32) -> (&'static str, Vec) { - ( - "powershell", - vec![ - "-NoProfile".to_owned(), - "-Command".to_owned(), - format!("Write-Output '{msg}'; Start-Sleep -Milliseconds {sleep_ms}"), - ], - ) + cfg_select! { + windows => { + let mut cmd_args = vec!["/C".to_owned()]; + // Use echo %* to print all arguments on Windows (requires a batch context) + // Alternative: build the echo command with all args + let echo_str = args.join(" "); + cmd_args.push(format!("echo {echo_str} && ping -n 6 127.0.0.1 >nul")); + ("cmd", cmd_args) + } + _ => { + let mut script_args = vec![ + "-c".to_owned(), + "echo $@ && sleep 5".to_owned(), + "--".to_owned(), + ]; + script_args.extend(args.iter().map(ToString::to_string)); + ("bash", script_args) + } + } } /// Returns a command that outputs a message and exits quickly after a brief delay. -#[cfg(not(windows))] #[must_use] pub fn short_lived_cmd(msg: &str, sleep_ms: u32) -> (&'static str, Vec) { - #[expect( - clippy::integer_division, - reason = "Intentional conversion of milliseconds to seconds with fractional part" - )] - let seconds = sleep_ms / 1000; - let millis = sleep_ms % 1000; - let sleep_arg = format!("{seconds}.{millis:03}"); - ( - "bash", - vec!["-c".to_owned(), format!("echo {msg} && sleep {sleep_arg}")], - ) -} - -/// Returns the platform-specific Python command name. -#[cfg(windows)] -#[must_use] -pub const fn python_cmd() -> &'static str { - "python" + cfg_select! { + windows => { + ( + "powershell", + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!("Write-Output '{msg}'; Start-Sleep -Milliseconds {sleep_ms}"), + ], + ) + } + _ => { + #[expect( + clippy::integer_division, + reason = "Intentional conversion of milliseconds to seconds with fractional part" + )] + let seconds = sleep_ms / 1000; + let millis = sleep_ms % 1000; + let sleep_arg = format!("{seconds}.{millis:03}"); + ( + "bash", + vec!["-c".to_owned(), format!("echo {msg} && sleep {sleep_arg}")], + ) + } + } } /// Returns the platform-specific Python command name. -#[cfg(not(windows))] #[must_use] pub const fn python_cmd() -> &'static str { - "python3" + cfg_select! { + windows => { "python" } + _ => { "python3" } + } }