feat(cli): quiet default human output

This commit is contained in:
Nils Andreas Svee committed 2026-05-08 13:16:07 +07:00
1 parent e971e95693
commit 0c1ee79afc
3 files changed
+114 -11

No files matched your search

+42 -6
View File
@@ -87,6 +87,10 @@ pub struct CliArgs {
/// Output format: human, simple, machine, or rtt-us. /// Output format: human, simple, machine, or rtt-us.
#[arg(long, value_enum, default_value_t = OutputMode::Human)] #[arg(long, value_enum, default_value_t = OutputMode::Human)]
pub output: OutputMode, pub output: OutputMode,
/// Include extra fields in human output.
#[arg(long)]
pub verbose: bool,
} }
impl CliArgs { impl CliArgs {
@@ -322,6 +326,11 @@ pub struct HumanEventStats {
pub receive_ipdv: Option<Duration>, pub receive_ipdv: Option<Duration>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HumanOutputOptions {
pub verbose: bool,
}
#[cfg(feature = "stats")] #[cfg(feature = "stats")]
impl From<irtt_stats::EventStatsUpdate> for HumanEventStats { impl From<irtt_stats::EventStatsUpdate> for HumanEventStats {
fn from(value: irtt_stats::EventStatsUpdate) -> Self { fn from(value: irtt_stats::EventStatsUpdate) -> Self {
@@ -335,6 +344,14 @@ impl From<irtt_stats::EventStatsUpdate> for HumanEventStats {
} }
pub fn format_human_event(event: &ClientEvent, stats: Option<HumanEventStats>) -> String { pub fn format_human_event(event: &ClientEvent, stats: Option<HumanEventStats>) -> String {
format_human_event_with_options(event, stats, HumanOutputOptions::default())
}
pub fn format_human_event_with_options(
event: &ClientEvent,
stats: Option<HumanEventStats>,
options: HumanOutputOptions,
) -> String {
match event { match event {
ClientEvent::SessionStarted { remote, token, .. } => { ClientEvent::SessionStarted { remote, token, .. } => {
format!("session started remote={remote} token={token:#x}") format!("session started remote={remote} token={token:#x}")
@@ -355,16 +372,19 @@ pub fn format_human_event(event: &ClientEvent, stats: Option<HumanEventStats>) -
received_stats, received_stats,
.. ..
} => { } => {
let mut out = format!( let mut out = format!("seq={seq}");
"seq={seq} logical_seq={logical_seq} rtt={}", if options.verbose {
format_duration(rtt.effective) write!(out, " logical_seq={logical_seq}").unwrap();
); }
write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
write_human_one_way(&mut out, *one_way); write_human_one_way(&mut out, *one_way);
write!(out, " ipdv={}", format_human_ipdv(stats)).unwrap(); write!(out, " ipdv={}", format_human_ipdv(stats)).unwrap();
if let Some(processing) = server_timing.and_then(|timing| timing.processing) { if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
write!(out, " proc={}", format_duration(processing)).unwrap(); write!(out, " proc={}", format_duration(processing)).unwrap();
} }
write_human_received_stats(&mut out, *received_stats); if options.verbose {
write_human_received_stats(&mut out, *received_stats);
}
out out
} }
ClientEvent::EchoLoss { ClientEvent::EchoLoss {
@@ -1277,11 +1297,14 @@ mod tests {
#[test] #[test]
fn human_uses_readable_per_event_lines() { fn human_uses_readable_per_event_lines() {
let line = format_event(&reply_event(), OutputMode::Human).unwrap(); let line = format_event(&reply_event(), OutputMode::Human).unwrap();
assert!(line.starts_with("seq=7 logical_seq=8 rtt=1.2ms")); assert!(line.starts_with("seq=7 rtt=1.2ms"));
assert!(line.contains("rd=500.0µs")); assert!(line.contains("rd=500.0µs"));
assert!(line.contains("sd=400.0µs")); assert!(line.contains("sd=400.0µs"));
assert!(line.contains("ipdv=n/a")); assert!(line.contains("ipdv=n/a"));
assert!(line.contains("proc=300.0µs")); assert!(line.contains("proc=300.0µs"));
assert!(!line.contains("logical_seq="));
assert!(!line.contains("server_received="));
assert!(!line.contains("server_window="));
assert!(!line.contains("rtt_us=")); assert!(!line.contains("rtt_us="));
assert_ne!( assert_ne!(
line, line,
@@ -1293,6 +1316,19 @@ mod tests {
assert!(!OutputMode::RttUs.prints_summary()); assert!(!OutputMode::RttUs.prints_summary());
} }
#[test]
fn verbose_human_reply_includes_extra_fields() {
let line = format_human_event_with_options(
&reply_event(),
None,
HumanOutputOptions { verbose: true },
);
assert!(line.contains("logical_seq=8"));
assert!(line.contains("server_received=9"));
assert!(line.contains("server_window=0x7"));
}
#[test] #[test]
fn human_reply_uses_supplied_ipdv_update() { fn human_reply_uses_supplied_ipdv_update() {
let line = format_human_event( let line = format_human_event(
+21 -3
View File
@@ -10,7 +10,9 @@ use std::{
}; };
use clap::Parser; use clap::Parser;
use irtt_cli::{format_event, format_human_event, CliArgs, OutputMode}; use irtt_cli::{
format_event, format_human_event_with_options, CliArgs, HumanOutputOptions, OutputMode,
};
use irtt_client::{Client, ClientEvent, ClientTimestamp, OpenOutcome, RecvBudget}; use irtt_client::{Client, ClientEvent, ClientTimestamp, OpenOutcome, RecvBudget};
#[cfg(feature = "stats")] #[cfg(feature = "stats")]
use irtt_stats::{StatsCollector, StatsConfig}; use irtt_stats::{StatsCollector, StatsConfig};
@@ -51,6 +53,9 @@ fn run(args: CliArgs, shutdown_requested: &AtomicBool) -> Result<(), Box<dyn std
let mut stats = StatsCollector::new(stats_config(continuous)); let mut stats = StatsCollector::new(stats_config(continuous));
let mut output = EventOutput { let mut output = EventOutput {
mode, mode,
human_options: HumanOutputOptions {
verbose: args.verbose,
},
print_finite_summary: !continuous, print_finite_summary: !continuous,
out: &mut stdout, out: &mut stdout,
#[cfg(feature = "stats")] #[cfg(feature = "stats")]
@@ -178,6 +183,7 @@ fn sleep_until_next_send(deadline: Option<Instant>) {
struct EventOutput<'a, W: Write> { struct EventOutput<'a, W: Write> {
mode: irtt_cli::OutputMode, mode: irtt_cli::OutputMode,
human_options: HumanOutputOptions,
print_finite_summary: bool, print_finite_summary: bool,
out: &'a mut W, out: &'a mut W,
#[cfg(feature = "stats")] #[cfg(feature = "stats")]
@@ -201,7 +207,11 @@ impl<W: Write> EventOutput<'_, W> {
let line = let line =
if self.mode == OutputMode::Human && !matches!(event, ClientEvent::EchoSent { .. }) { if self.mode == OutputMode::Human && !matches!(event, ClientEvent::EchoSent { .. }) {
Some(format_human_event(event, Some(stats_update.into()))) Some(format_human_event_with_options(
event,
Some(stats_update.into()),
self.human_options,
))
} else { } else {
format_event(event, self.mode) format_event(event, self.mode)
}; };
@@ -222,7 +232,12 @@ impl<W: Write> EventOutput<'_, W> {
write!( write!(
self.out, self.out,
"{}", "{}",
irtt_cli::summary::format_summary(&self.stats.summary()) irtt_cli::summary::format_summary_with_options(
&self.stats.summary(),
irtt_cli::summary::SummaryFormatOptions {
verbose: self.human_options.verbose,
},
)
)?; )?;
} }
@@ -364,6 +379,7 @@ mod tests {
{ {
let mut output = EventOutput { let mut output = EventOutput {
mode: irtt_cli::OutputMode::RttUs, mode: irtt_cli::OutputMode::RttUs,
human_options: HumanOutputOptions::default(),
print_finite_summary: true, print_finite_summary: true,
out: &mut out, out: &mut out,
stats: &mut stats, stats: &mut stats,
@@ -386,6 +402,7 @@ mod tests {
{ {
let mut output = EventOutput { let mut output = EventOutput {
mode: irtt_cli::OutputMode::Human, mode: irtt_cli::OutputMode::Human,
human_options: HumanOutputOptions::default(),
print_finite_summary: false, print_finite_summary: false,
out: &mut out, out: &mut out,
stats: &mut stats, stats: &mut stats,
@@ -404,6 +421,7 @@ mod tests {
{ {
let mut output = EventOutput { let mut output = EventOutput {
mode: irtt_cli::OutputMode::Human, mode: irtt_cli::OutputMode::Human,
human_options: HumanOutputOptions::default(),
print_finite_summary: false, print_finite_summary: false,
out: &mut out, out: &mut out,
stats: &mut stats, stats: &mut stats,
+51 -2
View File
@@ -5,6 +5,18 @@ use irtt_stats::{
}; };
pub fn format_summary(summary: &FiniteSummary) -> String { pub fn format_summary(summary: &FiniteSummary) -> String {
format_summary_with_options(summary, SummaryFormatOptions::default())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SummaryFormatOptions {
pub verbose: bool,
}
pub fn format_summary_with_options(
summary: &FiniteSummary,
options: SummaryFormatOptions,
) -> String {
let mut out = String::new(); let mut out = String::new();
let packets = summary.packets; let packets = summary.packets;
let loss = summary.loss; let loss = summary.loss;
@@ -21,8 +33,10 @@ pub fn format_summary(summary: &FiniteSummary) -> String {
writeln!(out, " {}", "-".repeat(82)).unwrap(); writeln!(out, " {}", "-".repeat(82)).unwrap();
write_signed_duration_row(&mut out, "RTT", &summary.rtt.primary); write_signed_duration_row(&mut out, "RTT", &summary.rtt.primary);
write_duration_row(&mut out, "raw RTT", &summary.rtt.raw); if options.verbose {
write_signed_duration_row(&mut out, "adjusted RTT", &summary.rtt.adjusted); write_duration_row(&mut out, "raw RTT", &summary.rtt.raw);
write_signed_duration_row(&mut out, "adjusted RTT", &summary.rtt.adjusted);
}
write_duration_row(&mut out, "IPDV/jitter", &summary.ipdv.round_trip); write_duration_row(&mut out, "IPDV/jitter", &summary.ipdv.round_trip);
write_duration_row(&mut out, "send IPDV", &summary.ipdv.send); write_duration_row(&mut out, "send IPDV", &summary.ipdv.send);
write_duration_row(&mut out, "receive IPDV", &summary.ipdv.receive); write_duration_row(&mut out, "receive IPDV", &summary.ipdv.receive);
@@ -257,6 +271,8 @@ mod tests {
assert!(output.contains("bytes: sent=64 received=64")); assert!(output.contains("bytes: sent=64 received=64"));
assert!(output.contains("RTT")); assert!(output.contains("RTT"));
assert!(output.contains("1.2ms")); assert!(output.contains("1.2ms"));
assert!(!output.contains("raw RTT"));
assert!(!output.contains("adjusted RTT"));
assert!(output.contains("server processing")); assert!(output.contains("server processing"));
assert!(output.contains("300.0µs")); assert!(output.contains("300.0µs"));
assert!(output.contains("send call")); assert!(output.contains("send call"));
@@ -265,6 +281,39 @@ mod tests {
assert!(output.contains("2.0µs")); assert!(output.contains("2.0µs"));
} }
#[test]
fn verbose_summary_includes_raw_and_adjusted_rtt_rows() {
let mut collector = StatsCollector::new(StatsConfig::finite());
let received_at = test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500));
collector.process(&ClientEvent::EchoReply {
seq: 1,
logical_seq: 1,
remote: test_remote(),
sent_at: test_timestamp(Duration::from_secs(1)),
received_at,
rtt: RttSample {
raw: Duration::from_micros(1500),
adjusted: Some(Duration::from_micros(1200)),
effective: Duration::from_micros(1200),
adjusted_signed: Some(SignedDuration { ns: 1_200_000 }),
effective_signed: SignedDuration { ns: 1_200_000 },
},
server_timing: None,
one_way: None,
received_stats: None,
bytes: 64,
packet_meta: PacketMeta::default(),
});
let output = format_summary_with_options(
&collector.summary(),
SummaryFormatOptions { verbose: true },
);
assert!(output.contains("raw RTT"));
assert!(output.contains("adjusted RTT"));
}
#[test] #[test]
fn signed_stats_can_format_negative_values() { fn signed_stats_can_format_negative_values() {
let stats = SignedDurationStatsWithMedian { let stats = SignedDurationStatsWithMedian {