diff --git a/crates/irtt-cli/src/cmd/client/args.rs b/crates/irtt-cli/src/cmd/client/args.rs index f2bc7f9..11e7530 100644 --- a/crates/irtt-cli/src/cmd/client/args.rs +++ b/crates/irtt-cli/src/cmd/client/args.rs @@ -398,6 +398,57 @@ mod tests { assert_eq!(specs[1].label, "host-a:2112#2"); } + #[test] + fn duplicate_resolved_target_addresses_are_rejected() { + let args = parse(&["127.0.0.1:2112", "127.0.0.1"]).unwrap(); + let err = args.resolved_managed_targets().unwrap_err(); + + assert!(err.contains("duplicate resolved target address 127.0.0.1:2112")); + } + + #[test] + fn target_normalization_matches_single_target_forms() { + assert_eq!(normalize_target_addr("127.0.0.1"), "127.0.0.1:2112"); + assert_eq!(normalize_target_addr("127.0.0.1:1234"), "127.0.0.1:1234"); + assert_eq!(normalize_target_addr("example.com"), "example.com:2112"); + assert_eq!( + normalize_target_addr("example.com:1234"), + "example.com:1234" + ); + assert_eq!(normalize_target_addr("[::1]"), "[::1]:2112"); + assert_eq!(normalize_target_addr("[::1]:1234"), "[::1]:1234"); + assert_eq!(normalize_target_addr("::1"), "[::1]:2112"); + } + + #[test] + fn target_resolution_respects_family_filters() { + let config = ClientConfig::default(); + assert_eq!( + resolve_cli_target("127.0.0.1", &config).unwrap(), + "127.0.0.1:2112".parse().unwrap() + ); + assert_eq!( + resolve_cli_target("[::1]", &config).unwrap(), + "[::1]:2112".parse().unwrap() + ); + assert_eq!( + resolve_cli_target("::1", &config).unwrap(), + "[::1]:2112".parse().unwrap() + ); + + let mut ipv4_only = ClientConfig::default(); + ipv4_only.socket_config.ipv4_only = true; + assert!(resolve_cli_target("127.0.0.1", &ipv4_only) + .unwrap() + .is_ipv4()); + assert!(resolve_cli_target("::1", &ipv4_only).is_err()); + + let mut ipv6_only = ClientConfig::default(); + ipv6_only.socket_config.ipv6_only = true; + assert!(resolve_cli_target("::1", &ipv6_only).unwrap().is_ipv6()); + assert!(resolve_cli_target("127.0.0.1", &ipv6_only).is_err()); + } + #[test] fn invalid_labelled_target_syntax_is_rejected() { assert!(parse(&["--target", "missing-equals"]).is_err()); diff --git a/crates/irtt-cli/src/cmd/client/run.rs b/crates/irtt-cli/src/cmd/client/run.rs index 5dc9834..8fa61fa 100644 --- a/crates/irtt-cli/src/cmd/client/run.rs +++ b/crates/irtt-cli/src/cmd/client/run.rs @@ -285,7 +285,11 @@ fn run_group_stream( managed_targets, SubscriberConfig { capacity: 16_384, - overflow: SubscriberOverflow::Disconnect, + // CLI output is best-effort under sustained backpressure. Dropping + // stale rows keeps continuous runs attached to the running group + // instead of turning a full output queue into a disconnected + // subscriber and a potentially blocking join. + overflow: SubscriberOverflow::DropOldest, }, )?; diff --git a/crates/irtt-cli/tests/client_multi_target.rs b/crates/irtt-cli/tests/client_multi_target.rs index 4c32305..79b2051 100644 --- a/crates/irtt-cli/tests/client_multi_target.rs +++ b/crates/irtt-cli/tests/client_multi_target.rs @@ -28,6 +28,101 @@ impl FakeServer { } } +#[test] +fn list_columns_succeeds_without_target() { + let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) + .arg("--list-columns") + .output() + .unwrap(); + + assert!( + output.status.success(), + "status={:?}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Available event columns:"), "{stdout}"); + assert!(stdout.contains("target"), "{stdout}"); +} + +#[test] +fn single_target_default_table_omits_target_and_accepts_pacing() { + let server = start_echo_server(test_params( + Some(Duration::from_millis(30)), + Duration::from_millis(10), + )); + + let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) + .args([ + "--duration", + "30ms", + "--interval", + "10ms", + "--pacing", + "burst", + "--header", + "always", + &server.addr.to_string(), + ]) + .output() + .unwrap(); + + server.join(); + + assert!( + output.status.success(), + "status={:?}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let header = stdout.lines().next().unwrap_or_default(); + assert!(!header.split_whitespace().any(|column| column == "target")); +} + +#[test] +fn single_target_custom_target_column_renders_positional_label() { + let server = start_echo_server(test_params( + Some(Duration::from_millis(30)), + Duration::from_millis(10), + )); + let target = server.addr.to_string(); + + let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) + .args([ + "--duration", + "30ms", + "--interval", + "10ms", + "--format", + "csv", + "--columns", + "target,seq", + "--header", + "never", + &target, + ]) + .output() + .unwrap(); + + server.join(); + + assert!( + output.status.success(), + "status={:?}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout + .lines() + .any(|line| line.starts_with(&format!("{target},"))), + "{stdout}" + ); +} + #[test] fn multi_target_csv_emits_rows_for_both_labels() { let params = test_params(Some(Duration::from_millis(40)), Duration::from_millis(10));