refactor(cli): unify target argument syntax

This commit is contained in:
Nils Andreas Svee committed 2026-08-15 20:56:12 +07:00
1 parent 79d3b9350c
commit a03004b22f
7 files changed
+276 -168

No files matched your search

+10 -4
View File
@@ -78,12 +78,18 @@ Probe several targets concurrently:
irtt-cli host-a:2112 host-b:2112
```
Assign stable labels to targets:
Every target argument accepts `[LABEL=]TARGET`. An optional `LABEL=` prefix assigns the logical
target name used in output and the TUI; without it, the target string itself is used. Assign
stable labels to targets:
```sh
irtt-cli \
--target ams=ams.example.com:2112 \
--target sg=sg.example.com:2112
irtt-cli ams=ams.example.com:2112 sg=sg.example.com:2112
```
Labeled and unlabeled targets can be freely mixed in one argument list:
```sh
irtt-cli host-a:2112 ams=ams.example.com:2112
```
Multi-target sessions use staggered pacing by default. To send each target's probes together:
+30 -31
View File
@@ -1,8 +1,8 @@
use std::time::Duration;
use crate::shared::client::{
parse_labelled_target, parse_test_duration, prepare_managed_run, CommonClientArgs,
GroupPacingArg, LabelledTargetArg, ManagedRunSetup, TargetSelection, TimestampArg,
parse_target, parse_test_duration, prepare_managed_run, CommonClientArgs, GroupPacingArg,
ManagedRunSetup, TargetArg, TargetSelection, TimestampArg,
};
use clap::{Parser, ValueEnum};
@@ -11,22 +11,14 @@ pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Parser)]
#[command(name = "irtt-cli", about = "Minimal IRTT-compatible stream client")]
pub struct ClientArgs {
/// Server address or host, with optional port. Repeat for multi-target mode.
/// Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.
#[arg(
value_name = "TARGET",
num_args = 0..,
long_help = "Server address or host, with optional port. Repeat the positional target for multi-target mode, for example: irtt-cli host-a:2112 host-b:2112."
value_parser = parse_target,
long_help = "Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.\n\nExamples:\n irtt-cli host-a:2112 host-b:2112\n irtt-cli eu=host-a:2112 us=host-b:2112\n irtt-cli host-a:2112 eu=host-b:2112"
)]
pub targets: Vec<String>,
/// Explicit labelled target, as label=host:port. Repeat for multi-target mode.
#[arg(
long = "target",
value_name = "LABEL=TARGET",
value_parser = parse_labelled_target,
long_help = "Explicit labelled target, as label=host:port. Repeat for multi-target mode. The label is used as the target column and must be unique."
)]
pub labelled_targets: Vec<LabelledTargetArg>,
pub targets: Vec<TargetArg>,
/// Managed group pacing for multi-target mode.
#[arg(
@@ -98,8 +90,7 @@ impl ClientArgs {
&self.common,
self.duration,
TargetSelection {
positional: &self.targets,
labelled: &self.labelled_targets,
targets: &self.targets,
pacing: self.pacing,
},
)
@@ -167,8 +158,9 @@ mod tests {
fn client_parser_keeps_finite_default_duration() {
let args = parse(&["127.0.0.1:2112"]).unwrap();
assert_eq!(args.targets, ["127.0.0.1:2112"]);
assert!(args.labelled_targets.is_empty());
assert_eq!(args.targets.len(), 1);
assert_eq!(args.targets[0].label, None);
assert_eq!(args.targets[0].addr, "127.0.0.1:2112");
assert_eq!(args.pacing, GroupPacingArg::Staggered);
assert_eq!(args.duration, DEFAULT_CLIENT_DURATION);
assert_eq!(args.interval, Duration::from_secs(1));
@@ -218,14 +210,8 @@ mod tests {
}
#[test]
fn repeated_labelled_targets_parse() {
let args = parse(&[
"--target",
"ams=ams.example.com:2112",
"--target",
"sg=sg.example.com:2112",
])
.unwrap();
fn repeated_labeled_targets_parse() {
let args = parse(&["ams=ams.example.com:2112", "sg=sg.example.com:2112"]).unwrap();
let specs = args.prepare().unwrap().targets;
assert_eq!(specs[0].label, "ams");
@@ -234,9 +220,18 @@ mod tests {
assert_eq!(specs[1].managed.server_addr, "sg.example.com:2112");
}
#[test]
fn mixed_labeled_and_unlabeled_targets_preserve_argument_order() {
let args = parse(&["local.example", "eu=a.example", "backup.example"]).unwrap();
let specs = args.prepare().unwrap().targets;
let labels: Vec<_> = specs.iter().map(|target| target.label.as_str()).collect();
assert_eq!(labels, ["local.example", "eu", "backup.example"]);
}
#[test]
fn duplicate_labels_are_rejected() {
let args = parse(&["host-a:2112", "--target", "host-a:2112=host-b:2112"]).unwrap();
let args = parse(&["host-a:2112", "host-a:2112=host-b:2112"]).unwrap();
let err = args.prepare().unwrap_err();
assert!(err.contains("duplicate target label"));
@@ -259,9 +254,13 @@ mod tests {
#[test]
fn invalid_labelled_target_syntax_is_rejected() {
assert!(parse(&["--target", "missing-equals"]).is_err());
assert!(parse(&["--target", "=127.0.0.1:2112"]).is_err());
assert!(parse(&["--target", "label="]).is_err());
assert!(parse(&["=127.0.0.1:2112"]).is_err());
assert!(parse(&["label="]).is_err());
}
#[test]
fn old_target_option_is_rejected() {
assert!(parse(&["--target", "eu=host.example"]).is_err());
}
#[test]
@@ -284,7 +283,7 @@ mod tests {
fn client_help_lists_shared_protocol_options() {
let help = ClientArgs::command().render_help().to_string();
assert!(help.contains("--format <FORMAT>"));
assert!(help.contains("--target <LABEL=TARGET>"));
assert!(!help.contains("--target "));
assert!(help.contains("--pacing <PACING>"));
assert!(help.contains("--columns <COLUMNS>"));
assert!(help.contains("--list-columns"));
+24 -34
View File
@@ -5,8 +5,8 @@ use clap::Parser;
#[cfg(test)]
use crate::shared::client::TimestampArg;
use crate::shared::client::{
parse_labelled_target, parse_test_duration, prepare_managed_run, CommonClientArgs,
GroupPacingArg, LabelledTargetArg, ManagedRunSetup, TargetSelection,
parse_target, parse_test_duration, prepare_managed_run, CommonClientArgs, GroupPacingArg,
ManagedRunSetup, TargetArg, TargetSelection,
};
pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
@@ -14,23 +14,15 @@ pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
#[derive(Debug, Clone, Parser)]
#[command(name = "irtt-tui", about = "Minimal IRTT-compatible TUI client")]
pub struct TuiArgs {
/// Server address or host, with optional port. Repeat for multi-target mode.
/// Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.
#[arg(
value_name = "TARGET",
num_args = 0..,
required_unless_present = "labelled_targets",
long_help = "Server address or host, with optional port. Repeat the positional target for multi-target mode, for example: irtt-tui host-a:2112 host-b:2112."
num_args = 1..,
required = true,
value_parser = parse_target,
long_help = "Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.\n\nExplicit labels are used in the legend and status table.\n\nExamples:\n irtt-tui host.example\n irtt-tui eu=host.example\n irtt-tui eu=host-a.example us=host-b.example"
)]
pub targets: Vec<String>,
/// Explicit labelled target, as label=host:port. Repeat for multi-target mode.
#[arg(
long = "target",
value_name = "LABEL=TARGET",
value_parser = parse_labelled_target,
long_help = "Explicit labelled target, as label=host:port. Repeat for multi-target mode. The label is used in the legend and status table and must be unique."
)]
pub labelled_targets: Vec<LabelledTargetArg>,
pub targets: Vec<TargetArg>,
/// Managed group pacing for multi-target mode.
#[arg(
@@ -64,8 +56,7 @@ impl TuiArgs {
&self.common,
self.duration,
TargetSelection {
positional: &self.targets,
labelled: &self.labelled_targets,
targets: &self.targets,
pacing: self.pacing,
},
)
@@ -115,8 +106,9 @@ mod tests {
#[test]
fn tui_parser_defaults_to_continuous_and_has_no_output_option() {
let args = parse(&["127.0.0.1:2112"]).unwrap();
assert_eq!(args.targets, ["127.0.0.1:2112"]);
assert!(args.labelled_targets.is_empty());
assert_eq!(args.targets.len(), 1);
assert_eq!(args.targets[0].label, None);
assert_eq!(args.targets[0].addr, "127.0.0.1:2112");
assert_eq!(args.pacing, GroupPacingArg::Staggered);
assert_eq!(args.duration, DEFAULT_TUI_DURATION);
assert!(args.is_continuous());
@@ -146,14 +138,8 @@ mod tests {
}
#[test]
fn repeated_labelled_targets_parse() {
let args = parse(&[
"--target",
"ams=ams.example.com:2112",
"--target",
"sg=sg.example.com:2112",
])
.unwrap();
fn repeated_labeled_targets_parse() {
let args = parse(&["ams=ams.example.com:2112", "sg=sg.example.com:2112"]).unwrap();
let specs = args.prepare().unwrap().targets;
assert_eq!(specs[0].label, "ams");
@@ -167,12 +153,12 @@ mod tests {
// The TUI has no target-free mode, so this is rejected at parse time
// rather than during preparation.
assert!(parse(&[]).is_err());
assert!(parse(&["--target", "ams=ams.example.com:2112"]).is_ok());
assert!(parse(&["ams=ams.example.com:2112"]).is_ok());
}
#[test]
fn duplicate_labels_are_rejected() {
let args = parse(&["host-a:2112", "--target", "host-a:2112=host-b:2112"]).unwrap();
let args = parse(&["host-a:2112", "host-a:2112=host-b:2112"]).unwrap();
let err = args.prepare().unwrap_err();
assert!(err.contains("duplicate target label"));
@@ -195,9 +181,13 @@ mod tests {
#[test]
fn invalid_labelled_target_syntax_is_rejected() {
assert!(parse(&["--target", "missing-equals"]).is_err());
assert!(parse(&["--target", "=127.0.0.1:2112"]).is_err());
assert!(parse(&["--target", "label="]).is_err());
assert!(parse(&["=127.0.0.1:2112"]).is_err());
assert!(parse(&["label="]).is_err());
}
#[test]
fn old_target_option_is_rejected() {
assert!(parse(&["--target", "eu=host.example"]).is_err());
}
#[test]
@@ -283,7 +273,7 @@ mod tests {
#[test]
fn tui_help_lists_multi_target_options() {
let help = TuiArgs::command().render_help().to_string();
assert!(help.contains("--target <LABEL=TARGET>"));
assert!(!help.contains("--target "));
assert!(help.contains("--pacing <PACING>"));
}
}
+2 -2
View File
@@ -12,6 +12,6 @@ pub use config::{expected_probe_count, DEFAULT_RECV_TIMEOUT};
pub use prepare::{prepare_managed_run, ManagedRunSetup, TargetSelection, MANAGED_EVENT_CAPACITY};
pub use session::is_shutdown_requested;
pub use targets::{
parse_labelled_target, prepare_managed_targets, target_specs, GroupPacingArg,
LabelledTargetArg, PreparedTarget, TargetSpec,
parse_target, prepare_managed_targets, target_specs, GroupPacingArg, PreparedTarget, TargetArg,
TargetSpec,
};
+35 -42
View File
@@ -16,7 +16,7 @@ use irtt_client::{
use super::{
args::CommonClientArgs,
targets::{prepare_managed_targets, target_specs, GroupPacingArg, LabelledTargetArg},
targets::{prepare_managed_targets, target_specs, GroupPacingArg, TargetArg},
PreparedTarget,
};
@@ -29,8 +29,7 @@ pub const MANAGED_EVENT_CAPACITY: usize = 16_384;
/// means and what its default is, so each keeps its own.
#[derive(Debug, Clone, Copy)]
pub struct TargetSelection<'a> {
pub positional: &'a [String],
pub labelled: &'a [LabelledTargetArg],
pub targets: &'a [TargetArg],
pub pacing: GroupPacingArg,
}
@@ -92,7 +91,7 @@ pub fn prepare_managed_run(
duration: Duration,
selection: TargetSelection<'_>,
) -> Result<ManagedRunSetup, String> {
let specs = target_specs(selection.positional, selection.labelled)?;
let specs = target_specs(selection.targets)?;
let targets = prepare_managed_targets(specs)?;
Ok(ManagedRunSetup {
targets,
@@ -119,29 +118,32 @@ mod tests {
CommonOnlyArgs::try_parse_from(argv).unwrap().common
}
fn unlabeled(addr: &str) -> TargetArg {
TargetArg {
label: None,
addr: addr.to_owned(),
}
}
fn labeled(label: &str, addr: &str) -> TargetArg {
TargetArg {
label: Some(label.to_owned()),
addr: addr.to_owned(),
}
}
fn prepare(
positional: &[&str],
labelled: &[LabelledTargetArg],
targets: &[TargetArg],
pacing: GroupPacingArg,
duration: Duration,
) -> Result<ManagedRunSetup, String> {
let positional: Vec<String> = positional.iter().map(|value| (*value).to_owned()).collect();
prepare_managed_run(
&common(&[]),
duration,
TargetSelection {
positional: &positional,
labelled,
pacing,
},
)
prepare_managed_run(&common(&[]), duration, TargetSelection { targets, pacing })
}
#[test]
fn a_single_target_prepares_one_managed_target_and_a_finite_duration() {
let setup = prepare(
&["127.0.0.1:2112"],
&[],
&[unlabeled("127.0.0.1:2112")],
GroupPacingArg::Staggered,
Duration::from_secs(30),
)
@@ -158,8 +160,7 @@ mod tests {
#[test]
fn a_zero_duration_prepares_a_continuous_client_config() {
let setup = prepare(
&["127.0.0.1:2112"],
&[],
&[unlabeled("127.0.0.1:2112")],
GroupPacingArg::Staggered,
Duration::ZERO,
)
@@ -170,13 +171,12 @@ mod tests {
#[test]
fn multiple_targets_keep_argument_order_and_share_one_client_config() {
let labelled = vec![LabelledTargetArg {
label: "sg".to_owned(),
addr: "sg.example.test:2112".to_owned(),
}];
let setup = prepare(
&["host-a:2112", "host-b:2112"],
&labelled,
&[
unlabeled("host-a:2112"),
unlabeled("host-b:2112"),
labeled("sg", "sg.example.test:2112"),
],
GroupPacingArg::Burst,
Duration::from_secs(10),
)
@@ -205,13 +205,12 @@ mod tests {
fn the_shared_client_config_carries_no_target_of_its_own() {
// The managed driver replaces the address, and the CLI's key, per
// target, so the template must not pin either to one target.
let positional = vec!["host-a:2112".to_owned(), "host-b:2112".to_owned()];
let targets = vec![unlabeled("host-a:2112"), unlabeled("host-b:2112")];
let setup = prepare_managed_run(
&common(&["--hmac", "secret"]),
Duration::from_secs(10),
TargetSelection {
positional: &positional,
labelled: &[],
targets: &targets,
pacing: GroupPacingArg::Staggered,
},
)
@@ -232,8 +231,7 @@ mod tests {
#[test]
fn an_empty_target_set_is_rejected_with_the_list_columns_hint() {
let err =
prepare(&[], &[], GroupPacingArg::Staggered, Duration::from_secs(10)).unwrap_err();
let err = prepare(&[], GroupPacingArg::Staggered, Duration::from_secs(10)).unwrap_err();
assert!(err.contains("at least one target is required"));
assert!(err.contains("--list-columns"));
@@ -241,17 +239,12 @@ mod tests {
#[test]
fn a_duplicate_label_is_rejected_before_any_configuration_is_built() {
let labelled = vec![LabelledTargetArg {
label: "host-a:2112".to_owned(),
addr: "host-b:2112".to_owned(),
}];
let err = prepare(
&["host-a:2112"],
&labelled,
GroupPacingArg::Staggered,
Duration::from_secs(10),
)
.unwrap_err();
let targets = vec![
unlabeled("host-a:2112"),
labeled("host-a:2112", "host-b:2112"),
];
let err =
prepare(&targets, GroupPacingArg::Staggered, Duration::from_secs(10)).unwrap_err();
assert!(err.contains("duplicate target label"));
}
+117 -34
View File
@@ -4,8 +4,8 @@ use clap::ValueEnum;
use irtt_client::managed::{ManagedPacing, ManagedTargetConfig, TargetId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LabelledTargetArg {
pub label: String,
pub struct TargetArg {
pub label: Option<String>,
pub addr: String,
}
@@ -36,45 +36,46 @@ impl From<GroupPacingArg> for ManagedPacing {
}
}
pub fn parse_labelled_target(input: &str) -> Result<LabelledTargetArg, String> {
let (label, addr) = input
.split_once('=')
.ok_or_else(|| "target must use LABEL=TARGET syntax".to_owned())?;
/// Parse one positional target argument, as `TARGET` or `LABEL=TARGET`.
pub fn parse_target(input: &str) -> Result<TargetArg, String> {
match input.split_once('=') {
None => Ok(TargetArg {
label: None,
addr: input.to_owned(),
}),
Some((label, addr)) => {
if label.is_empty() {
return Err("target label must not be empty".to_owned());
}
if addr.is_empty() {
return Err("target address must not be empty".to_owned());
}
Ok(LabelledTargetArg {
label: label.to_owned(),
Ok(TargetArg {
label: Some(label.to_owned()),
addr: addr.to_owned(),
})
}
}
}
pub fn target_specs(
positional_targets: &[String],
labelled_targets: &[LabelledTargetArg],
) -> Result<Vec<TargetSpec>, String> {
let mut specs = Vec::new();
let mut positional_counts = std::collections::HashMap::<&str, usize>::new();
for target in positional_targets {
let count = positional_counts.entry(target.as_str()).or_default();
pub fn target_specs(targets: &[TargetArg]) -> Result<Vec<TargetSpec>, String> {
let mut specs = Vec::with_capacity(targets.len());
let mut unlabeled_counts = std::collections::HashMap::<&str, usize>::new();
for target in targets {
let label = match &target.label {
Some(label) => label.clone(),
None => {
let count = unlabeled_counts.entry(target.addr.as_str()).or_default();
*count += 1;
let label = if *count == 1 {
target.clone()
if *count == 1 {
target.addr.clone()
} else {
format!("{target}#{}", *count)
format!("{}#{}", target.addr, *count)
}
}
};
specs.push(TargetSpec {
label,
addr: target.clone(),
});
}
for target in labelled_targets {
specs.push(TargetSpec {
label: target.label.clone(),
addr: target.addr.clone(),
});
}
@@ -108,19 +109,101 @@ pub fn prepare_managed_targets(specs: Vec<TargetSpec>) -> Result<Vec<PreparedTar
mod tests {
use super::*;
fn unlabeled(addr: &str) -> TargetArg {
TargetArg {
label: None,
addr: addr.to_owned(),
}
}
fn labeled(label: &str, addr: &str) -> TargetArg {
TargetArg {
label: Some(label.to_owned()),
addr: addr.to_owned(),
}
}
#[test]
fn target_specs_suffix_repeated_positionals_and_reject_duplicate_labels() {
let positionals = vec!["host-a:2112".to_owned(), "host-a:2112".to_owned()];
let specs = target_specs(&positionals, &[]).unwrap();
fn parse_target_without_equals_is_unlabeled() {
let target = parse_target("host.example").unwrap();
assert_eq!(target.label, None);
assert_eq!(target.addr, "host.example");
let target = parse_target("host.example:2112").unwrap();
assert_eq!(target.label, None);
assert_eq!(target.addr, "host.example:2112");
let target = parse_target("[::1]:2112").unwrap();
assert_eq!(target.label, None);
assert_eq!(target.addr, "[::1]:2112");
}
#[test]
fn parse_target_with_equals_splits_on_first() {
let target = parse_target("eu=host.example").unwrap();
assert_eq!(target.label, Some("eu".to_owned()));
assert_eq!(target.addr, "host.example");
let target = parse_target("eu=host.example:2112").unwrap();
assert_eq!(target.label, Some("eu".to_owned()));
assert_eq!(target.addr, "host.example:2112");
}
#[test]
fn parse_target_rejects_empty_label_or_address() {
assert!(parse_target("=host.example").is_err());
assert!(parse_target("eu=").is_err());
}
#[test]
fn target_specs_suffix_repeated_unlabeled_and_reject_duplicate_labels() {
let targets = vec![unlabeled("host-a:2112"), unlabeled("host-a:2112")];
let specs = target_specs(&targets).unwrap();
assert_eq!(specs[0].label, "host-a:2112");
assert_eq!(specs[1].label, "host-a:2112#2");
let labels = vec![LabelledTargetArg {
label: "host-a:2112".to_owned(),
addr: "host-b:2112".to_owned(),
}];
let err = target_specs(&positionals[..1], &labels).unwrap_err();
let targets = vec![
unlabeled("host-a:2112"),
labeled("host-a:2112", "host-b:2112"),
];
let err = target_specs(&targets).unwrap_err();
assert!(err.contains("duplicate target label"));
}
#[test]
fn target_specs_preserve_argument_order_with_mixed_labels() {
let targets = vec![
unlabeled("local"),
labeled("eu", "eu.example"),
unlabeled("backup"),
labeled("us", "us.example"),
];
let specs = target_specs(&targets).unwrap();
let labels: Vec<_> = specs.iter().map(|spec| spec.label.as_str()).collect();
assert_eq!(labels, ["local", "eu", "backup", "us"]);
let addrs: Vec<_> = specs.iter().map(|spec| spec.addr.as_str()).collect();
assert_eq!(addrs, ["local", "eu.example", "backup", "us.example"]);
}
#[test]
fn target_specs_count_unlabeled_occurrences_independently_of_explicit_labels() {
let targets = vec![unlabeled("foo"), labeled("eu", "foo"), unlabeled("foo")];
let specs = target_specs(&targets).unwrap();
let labels: Vec<_> = specs.iter().map(|spec| spec.label.as_str()).collect();
assert_eq!(labels, ["foo", "eu", "foo#2"]);
}
#[test]
fn target_specs_reject_generated_suffix_collisions() {
let targets = vec![
unlabeled("host.example"),
unlabeled("host.example"),
labeled("host.example#2", "other.example"),
];
let err = target_specs(&targets).unwrap_err();
assert!(err.contains("duplicate target label"));
}
+58 -21
View File
@@ -53,6 +53,18 @@ impl InterruptibleFakeServer {
}
}
#[test]
fn old_target_option_is_rejected_as_an_unknown_argument() {
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli"))
.args(["--target", "eu=127.0.0.1:2112"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("unexpected argument"), "{stderr}");
}
#[test]
fn list_columns_succeeds_without_target() {
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli"))
@@ -121,7 +133,6 @@ fn single_labeled_target_default_table_includes_target() {
"10ms",
"--header",
"always",
"--target",
&format!("eu={}", server.addr),
])
.output()
@@ -159,7 +170,6 @@ fn single_labeled_target_default_csv_includes_target_and_event_wall_ns() {
"csv",
"--header",
"always",
"--target",
&format!("eu={}", server.addr),
])
.output()
@@ -235,9 +245,7 @@ fn multi_target_default_table_includes_both_labels() {
"10ms",
"--header",
"always",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
])
.output()
@@ -269,6 +277,52 @@ fn multi_target_default_table_includes_both_labels() {
);
}
#[test]
fn mixed_labeled_and_unlabeled_positional_targets_both_appear_in_output() {
let a = InTreeServer::start();
let b = InTreeServer::start();
let unlabeled_addr = a.addr.to_string();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli"))
.args([
"--duration",
"40ms",
"--interval",
"10ms",
"--header",
"always",
&unlabeled_addr,
&format!("eu={}", b.addr),
])
.output()
.unwrap();
drop(a);
drop(b);
assert!(
output.status.success(),
"status={:?}\nstderr={}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
let mut lines = stdout.lines();
let header = lines.next().unwrap_or_default();
assert_eq!(header.split_whitespace().next(), Some("target"));
let rows: Vec<&str> = lines.collect();
assert!(
rows.iter()
.any(|line| line.split_whitespace().next() == Some(unlabeled_addr.as_str())),
"{stdout}"
);
assert!(
rows.iter()
.any(|line| line.split_whitespace().next() == Some("eu")),
"{stdout}"
);
}
#[test]
fn single_target_custom_target_column_renders_positional_label() {
let server = InTreeServer::start();
@@ -358,7 +412,6 @@ fn columns_default_keyword_includes_target_for_single_target() {
"default",
"--header",
"always",
"--target",
&format!("eu={}", server.addr),
])
.output()
@@ -391,7 +444,6 @@ fn single_labeled_target_default_jsonl_includes_target() {
"10ms",
"--format",
"jsonl",
"--target",
&format!("eu={}", server.addr),
])
.output()
@@ -523,9 +575,7 @@ fn multi_target_csv_emits_rows_for_both_labels() {
"target,seq,effective_rtt_us",
"--header",
"never",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
])
.output()
@@ -568,9 +618,7 @@ fn finite_multi_target_peer_close_is_accepted_as_completion() {
"target,seq",
"--header",
"never",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
]);
@@ -632,9 +680,7 @@ fn continuous_all_peer_closed_targets_exit_nonzero() {
"target,seq",
"--header",
"never",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
]);
@@ -684,9 +730,7 @@ fn continuous_partial_peer_close_preserves_queued_rows_and_reports_peer_close()
"target,seq",
"--header",
"never",
"--target",
&format!("healthy={}", healthy.addr),
"--target",
&format!("peer={}", peer_closed.addr),
]);
@@ -740,11 +784,8 @@ fn continuous_mixed_peer_close_and_open_failure_exits_nonzero() {
"target,seq",
"--header",
"never",
"--target",
&format!("failure={}", failure.addr),
"--target",
&format!("healthy={}", healthy.addr),
"--target",
&format!("peer={}", peer_closed.addr),
]);
@@ -795,9 +836,7 @@ fn explicit_group_interruption_succeeds_without_peer_close_error() {
"target,seq",
"--header",
"never",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
]);
@@ -851,9 +890,7 @@ fn all_open_failures_exit_nonzero_with_diagnostics() {
"0s",
"--interval",
"10ms",
"--target",
&format!("a={}", a.addr),
"--target",
&format!("b={}", b.addr),
]);