From 6a4dce68b3c2b852fdc827675b0aab53992c02ec Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 20 Aug 2026 10:42:31 +0700 Subject: [PATCH] fix(cli): preserve target order in summaries Final multi-target summaries were printed by iterating the label-keyed BTreeMap directly, so they came out in lexicographic label order instead of the order targets were given on the command line. Iterate setup.targets (the canonical prepared CLI order) instead, looking up each target's stats by label, and keep the BTreeMap only for event-time lookup. --- crates/irtt-cli/src/cmd/client/run.rs | 9 ++-- crates/irtt-cli/tests/client_multi_target.rs | 48 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/irtt-cli/src/cmd/client/run.rs b/crates/irtt-cli/src/cmd/client/run.rs index 75d82eb..016abd4 100644 --- a/crates/irtt-cli/src/cmd/client/run.rs +++ b/crates/irtt-cli/src/cmd/client/run.rs @@ -176,15 +176,18 @@ pub fn run_stream( stream_output.print_final_summary = should_print_final_summary(continuous, interrupted); stream_output.show_running_only_summary_note = continuous && interrupted && stream_output.print_final_summary; - for (label, stats) in &stats { + for target in &setup.targets { + let target_stats = stats.get(&target.label).expect( + "every prepared target must have a stats collector inserted before the run begins", + ); if multi_target && stream_output.print_final_summary && stream_output.config.prints_summary() { writeln!(stream_output.out)?; - writeln!(stream_output.out, "target: {label}")?; + writeln!(stream_output.out, "target: {}", target.label)?; } - stream_output.print_summary(stats)?; + stream_output.print_summary(target_stats)?; } stream_output.out.flush()?; if let Some(error) = terminal_error { diff --git a/crates/irtt-cli/tests/client_multi_target.rs b/crates/irtt-cli/tests/client_multi_target.rs index a8d9ef3..23a21b6 100644 --- a/crates/irtt-cli/tests/client_multi_target.rs +++ b/crates/irtt-cli/tests/client_multi_target.rs @@ -277,6 +277,54 @@ fn multi_target_default_table_includes_both_labels() { ); } +#[test] +fn multi_target_final_summary_preserves_cli_argument_order() { + // Labels are chosen so lexicographic (BTreeMap) order differs from the + // CLI argument order, to prove the final per-target summary headings + // follow the order targets were given on the command line rather than + // the internal label-keyed lookup map's iteration order. + let z = InTreeServer::start(); + let a = InTreeServer::start(); + let m = InTreeServer::start(); + + let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) + .args([ + "--duration", + "40ms", + "--interval", + "10ms", + &format!("z={}", z.addr), + &format!("a={}", a.addr), + &format!("m={}", m.addr), + ]) + .output() + .unwrap(); + + drop(z); + drop(a); + drop(m); + + assert!( + output.status.success(), + "status={:?}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let heading_position = |label: &str| { + stdout + .find(&format!("target: {label}")) + .unwrap_or_else(|| panic!("missing summary heading for target {label}\n{stdout}")) + }; + let z_pos = heading_position("z"); + let a_pos = heading_position("a"); + let m_pos = heading_position("m"); + assert!( + z_pos < a_pos && a_pos < m_pos, + "expected summary headings in CLI argument order z, a, m; got positions z={z_pos} a={a_pos} m={m_pos}\n{stdout}" + ); +} + #[test] fn mixed_labeled_and_unlabeled_positional_targets_both_appear_in_output() { let a = InTreeServer::start();