feat(cli): introduce flexible event row output formats and column selection
Replace fixed output modes with customizable table, CSV, TSV, and JSON Lines formats. This allows users to select specific data columns and control header visibility for better integration with scripts and monitoring tools.
This commit is contained in:
1 parent
4b408d3ab4
commit
b02bc53cd8
6 files changed
+1202
-499
No files matched your search
@@ -15,7 +15,7 @@ This repository currently provides:
|
|||||||
- `irtt-tui`, an optional terminal UI applet when built with the `tui` feature
|
- `irtt-tui`, an optional terminal UI applet when built with the `tui` feature
|
||||||
- `irtt-client`, a Rust library for running client sessions and consuming events
|
- `irtt-client`, a Rust library for running client sessions and consuming events
|
||||||
- finite and continuous probe runs
|
- finite and continuous probe runs
|
||||||
- human, simple, machine-readable, and RTT-only stream output modes
|
- table, CSV, TSV, and JSON Lines event-row output formats with selectable columns
|
||||||
- optional local summary statistics
|
- optional local summary statistics
|
||||||
|
|
||||||
Server support is not implemented.
|
Server support is not implemented.
|
||||||
@@ -76,13 +76,14 @@ Run continuously:
|
|||||||
irtt-cli <server> --duration 0
|
irtt-cli <server> --duration 0
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a specific output mode:
|
Use a specific output format or column selection:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-cli <server> --output human
|
irtt-cli <server> --format table
|
||||||
irtt-cli <server> --output simple
|
irtt-cli <server> --format jsonl
|
||||||
irtt-cli <server> --output machine
|
irtt-cli <server> --format csv --columns event,seq,remote,effective_rtt_us
|
||||||
irtt-cli <server> --output rtt-us
|
irtt-cli <server> --format tsv --columns effective_rtt_us --header never
|
||||||
|
irtt-cli --list-columns
|
||||||
```
|
```
|
||||||
|
|
||||||
With the optional TUI feature, `irtt-tui` opens a live cumulative dashboard for
|
With the optional TUI feature, `irtt-tui` opens a live cumulative dashboard for
|
||||||
@@ -103,36 +104,71 @@ irtt-cli --help
|
|||||||
irtt-tui --help
|
irtt-tui --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Machine output
|
## Event row output
|
||||||
|
|
||||||
Use `--output machine` for line-oriented `key=value` output intended for scripts, monitoring, and autorate consumers.
|
The stream client separates event row rendering from final summaries. Event rows
|
||||||
|
are controlled by `--format`, `--columns`, and `--header`; final summaries remain
|
||||||
|
separate table-style text and are printed for table output when the run policy
|
||||||
|
allows a summary.
|
||||||
|
|
||||||
|
Formats:
|
||||||
|
|
||||||
|
- `table`: default, terminal-readable columns with a header
|
||||||
|
- `csv`: comma-separated rows, intended for scripts
|
||||||
|
- `tsv`: tab-separated rows, useful for one-value streams and shell pipelines
|
||||||
|
- `jsonl`: one JSON object per event row, suitable for streaming consumers
|
||||||
|
|
||||||
|
Headers are controlled with `--header auto|always|never`. In `auto`, table, CSV,
|
||||||
|
and TSV print one header row; JSON Lines never prints a header.
|
||||||
|
|
||||||
|
Columns are selected with `-c, --columns <COLUMNS>`, where `COLUMNS` is a
|
||||||
|
comma-separated list. Use `--list-columns` to print the available names. Useful
|
||||||
|
columns include:
|
||||||
|
|
||||||
|
```text
|
||||||
|
event, seq, remote, token, rtt, rtt_us, raw_rtt_us, effective_rtt_us,
|
||||||
|
adjusted_rtt_us, rd, rd_us, sd, sd_us, ipdv, ipdv_us, proc,
|
||||||
|
server_processing_us, bytes, send_call_us, timer_error_us, highest_seen,
|
||||||
|
server_received, server_window, dscp, ecn, traffic_class, kernel_rx_ns,
|
||||||
|
warning_kind, message, event_wall_ns, client_send_wall_ns,
|
||||||
|
client_receive_wall_ns
|
||||||
|
```
|
||||||
|
|
||||||
|
Aliases are accepted for readability: `receive_delay` for `rd`, `send_delay`
|
||||||
|
for `sd`, `server_processing` for `proc`, `server_received_count` for
|
||||||
|
`server_received`, and `server_received_window` for `server_window`.
|
||||||
|
|
||||||
|
The default table output favors readability and omits `echo_sent` rows. Custom
|
||||||
|
table column selections and machine-friendly formats include all event rows.
|
||||||
|
Missing table values render as `-`; missing CSV and TSV values render as empty
|
||||||
|
fields; missing JSONL values render as `null`.
|
||||||
|
|
||||||
|
Use JSON Lines for structured event streaming:
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-cli <server> --duration 0 --interval 250ms --output machine
|
irtt-cli <server> --duration 0 --interval 250ms --format jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
Each line represents one client event and starts with an `event` field, for example:
|
Each line is one client event object, for example:
|
||||||
|
|
||||||
```text
|
```json
|
||||||
event=echo_sent seq=4 remote=203.0.113.10:2112 client_send_wall_ns=1760000000000000000 bytes=64 send_call_us=10 timer_error_us=2
|
{"event":"echo_reply","seq":4,"remote":"203.0.113.10:2112","token":null,"rtt":"12.1ms","rtt_us":12100,"raw_rtt_us":12400,"effective_rtt_us":12100,"adjusted_rtt_us":12100,"rd":"6.0ms","rd_us":6000,"sd":"6.1ms","sd_us":6100,"ipdv":"300.0µs","ipdv_us":300,"proc":"300.0µs","server_processing_us":300,"bytes":64,"send_call_us":null,"timer_error_us":null,"highest_seen":null,"server_received":5,"server_window":"0x1f","dscp":0,"ecn":0,"traffic_class":0,"kernel_rx_ns":null,"warning_kind":null,"message":null,"event_wall_ns":1760000000012400000,"client_send_wall_ns":1760000000000000000,"client_receive_wall_ns":1760000000012400000}
|
||||||
event=echo_reply seq=4 remote=203.0.113.10:2112 client_send_wall_ns=1760000000000000000 client_receive_wall_ns=1760000000012400000 raw_rtt_us=12400 effective_rtt_us=12100 adjusted_rtt_us=12100 server_receive_wall_ns=1760000000006100000 server_receive_mono_ns=5000006100000 server_send_wall_ns=1760000000006400000 server_send_mono_ns=5000006400000 server_processing_us=300 client_to_server_us=6100 server_to_client_us=6000 server_received_count=5 server_received_window=0x1f dscp=0 ecn=0 kernel_rx_ns=none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Consumers should match on `event=...` and read the fields they need. Unknown fields should be ignored.
|
Consumers should match on the `event` property and read the columns they need.
|
||||||
RTT, send-call, and timer-error fields are reported in microseconds.
|
RTT, send-call, and timer-error `*_us` fields are reported in microseconds.
|
||||||
`raw_rtt_us` is the measured client send-to-receive RTT. `effective_rtt_us` and
|
`raw_rtt_us` is the measured client send-to-receive RTT. `effective_rtt_us` and
|
||||||
`adjusted_rtt_us` are signed and may be negative when server processing exceeds
|
`adjusted_rtt_us` are signed and may be negative when server processing exceeds
|
||||||
the raw RTT or timing correction produces a negative adjusted value. When
|
the raw RTT or timing correction produces a negative adjusted value. When
|
||||||
one-way delay fields are present, `client_to_server_us` and
|
one-way delay fields are present, `sd_us` and `rd_us` are also signed
|
||||||
`server_to_client_us` are also signed microseconds and may be negative due to
|
microseconds and may be negative due to wall-clock skew.
|
||||||
wall-clock skew.
|
|
||||||
|
|
||||||
For consumers that only need RTT values, use:
|
For consumers that only need effective RTT values in microseconds, use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-cli <server> --output rtt-us
|
irtt-cli <server> --format tsv --columns effective_rtt_us --header never
|
||||||
```
|
```
|
||||||
|
|
||||||
## Library usage
|
## Library usage
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10);
|
|||||||
#[command(name = "irtt-cli", about = "Minimal IRTT-compatible stream client")]
|
#[command(name = "irtt-cli", about = "Minimal IRTT-compatible stream client")]
|
||||||
pub struct ClientArgs {
|
pub struct ClientArgs {
|
||||||
/// Server address or host, with optional port.
|
/// Server address or host, with optional port.
|
||||||
pub server: String,
|
#[arg(required_unless_present = "list_columns")]
|
||||||
|
pub server: Option<String>,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
@@ -25,18 +26,35 @@ pub struct ClientArgs {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub common: CommonClientArgs,
|
pub common: CommonClientArgs,
|
||||||
|
|
||||||
/// Output format.
|
/// Event row renderer format.
|
||||||
#[arg(long, value_enum, default_value_t = OutputMode::Human)]
|
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
|
||||||
pub output: OutputMode,
|
pub format: OutputFormat,
|
||||||
|
|
||||||
/// Include extra fields in human output.
|
/// Comma-separated event row columns.
|
||||||
|
#[arg(short = 'c', long, value_name = "COLUMNS")]
|
||||||
|
pub columns: Option<String>,
|
||||||
|
|
||||||
|
/// Print available event row columns and exit.
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_columns: bool,
|
||||||
|
|
||||||
|
/// Header policy for table, CSV, and TSV output.
|
||||||
|
#[arg(long, value_enum, default_value_t = HeaderMode::Auto)]
|
||||||
|
pub header: HeaderMode,
|
||||||
|
|
||||||
|
/// Include extra fields in table output and final summaries.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientArgs {
|
impl ClientArgs {
|
||||||
pub fn to_client_config(&self) -> ClientConfig {
|
pub fn to_client_config(&self) -> ClientConfig {
|
||||||
self.common.to_client_config(&self.server, self.duration)
|
self.common.to_client_config(
|
||||||
|
self.server
|
||||||
|
.as_deref()
|
||||||
|
.expect("server is required unless --list-columns is set"),
|
||||||
|
self.duration,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_continuous(&self) -> bool {
|
pub fn is_continuous(&self) -> bool {
|
||||||
@@ -57,23 +75,33 @@ impl std::ops::Deref for ClientArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
pub enum OutputMode {
|
pub enum OutputFormat {
|
||||||
/// Readable terminal output with a final summary.
|
/// Readable terminal table output.
|
||||||
Human,
|
Table,
|
||||||
/// Parseable full event fields.
|
/// Comma-separated event rows.
|
||||||
Machine,
|
Csv,
|
||||||
/// Simple key=value-ish event stream.
|
/// Tab-separated event rows.
|
||||||
Simple,
|
Tsv,
|
||||||
/// RTT microseconds only.
|
/// One JSON object per event row.
|
||||||
RttUs,
|
Jsonl,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputMode {
|
impl OutputFormat {
|
||||||
pub fn prints_summary(self) -> bool {
|
pub fn prints_summary(self) -> bool {
|
||||||
matches!(self, Self::Human)
|
matches!(self, Self::Table)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum HeaderMode {
|
||||||
|
/// Print headers for table, CSV, and TSV output.
|
||||||
|
Auto,
|
||||||
|
/// Always print headers where the format supports them.
|
||||||
|
Always,
|
||||||
|
/// Never print headers.
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -91,10 +119,12 @@ mod tests {
|
|||||||
fn client_parser_keeps_finite_default_duration() {
|
fn client_parser_keeps_finite_default_duration() {
|
||||||
let args = parse(&["127.0.0.1:2112"]).unwrap();
|
let args = parse(&["127.0.0.1:2112"]).unwrap();
|
||||||
|
|
||||||
assert_eq!(args.server, "127.0.0.1:2112");
|
assert_eq!(args.server.as_deref(), Some("127.0.0.1:2112"));
|
||||||
assert_eq!(args.duration, DEFAULT_CLIENT_DURATION);
|
assert_eq!(args.duration, DEFAULT_CLIENT_DURATION);
|
||||||
assert_eq!(args.interval, Duration::from_secs(1));
|
assert_eq!(args.interval, Duration::from_secs(1));
|
||||||
assert_eq!(args.output, OutputMode::Human);
|
assert_eq!(args.format, OutputFormat::Table);
|
||||||
|
assert_eq!(args.columns, None);
|
||||||
|
assert_eq!(args.header, HeaderMode::Auto);
|
||||||
assert!(!args.is_continuous());
|
assert!(!args.is_continuous());
|
||||||
assert_eq!(args.timestamp_mode(), TimestampArg::Both);
|
assert_eq!(args.timestamp_mode(), TimestampArg::Both);
|
||||||
|
|
||||||
@@ -107,17 +137,27 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn client_parser_accepts_stream_outputs_and_rejects_tui_output() {
|
fn client_parser_accepts_stream_formats_and_rejects_tui_output() {
|
||||||
for output in ["human", "simple", "machine", "rtt-us"] {
|
for format in ["table", "csv", "tsv", "jsonl"] {
|
||||||
assert!(parse(&["--output", output, "127.0.0.1:2112"]).is_ok());
|
assert!(parse(&["--format", format, "127.0.0.1:2112"]).is_ok());
|
||||||
}
|
}
|
||||||
assert!(parse(&["--output", "tui", "127.0.0.1:2112"]).is_err());
|
assert!(parse(&["--format", "tui", "127.0.0.1:2112"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_columns_does_not_require_server() {
|
||||||
|
let args = parse(&["--list-columns"]).unwrap();
|
||||||
|
assert_eq!(args.server, None);
|
||||||
|
assert!(args.list_columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn client_help_lists_shared_protocol_options() {
|
fn client_help_lists_shared_protocol_options() {
|
||||||
let help = ClientArgs::command().render_help().to_string();
|
let help = ClientArgs::command().render_help().to_string();
|
||||||
assert!(help.contains("--output <OUTPUT>"));
|
assert!(help.contains("--format <FORMAT>"));
|
||||||
|
assert!(help.contains("--columns <COLUMNS>"));
|
||||||
|
assert!(help.contains("--list-columns"));
|
||||||
|
assert!(help.contains("--header <HEADER>"));
|
||||||
assert!(help.contains("--tstamp <MODE>"));
|
assert!(help.contains("--tstamp <MODE>"));
|
||||||
assert!(help.contains("--stats <STATS>"));
|
assert!(help.contains("--stats <STATS>"));
|
||||||
assert!(help.contains("--sfill <STRING>"));
|
assert!(help.contains("--sfill <STRING>"));
|
||||||
@@ -127,10 +167,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn output_mode_summary_policy_is_human_only() {
|
fn output_format_summary_policy_is_table_only() {
|
||||||
assert!(OutputMode::Human.prints_summary());
|
assert!(OutputFormat::Table.prints_summary());
|
||||||
assert!(!OutputMode::Simple.prints_summary());
|
assert!(!OutputFormat::Csv.prints_summary());
|
||||||
assert!(!OutputMode::Machine.prints_summary());
|
assert!(!OutputFormat::Tsv.prints_summary());
|
||||||
assert!(!OutputMode::RttUs.prints_summary());
|
assert!(!OutputFormat::Jsonl.prints_summary());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,5 @@ pub mod run;
|
|||||||
#[cfg(feature = "stats")]
|
#[cfg(feature = "stats")]
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
||||||
pub use args::{ClientArgs, OutputMode};
|
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
||||||
pub use output::{
|
|
||||||
format_event, format_human_event, format_human_event_with_options, HumanEventStats,
|
|
||||||
HumanIpdvPair, HumanOutputOptions,
|
|
||||||
};
|
|
||||||
pub use run::run_stream;
|
pub use run::run_stream;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -8,8 +8,8 @@ use irtt_client::ClientEvent;
|
|||||||
use irtt_client::{ClientTimestamp, PacketMeta, RttSample, SignedDuration};
|
use irtt_client::{ClientTimestamp, PacketMeta, RttSample, SignedDuration};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
args::{ClientArgs, OutputMode},
|
args::ClientArgs,
|
||||||
output::{format_event_with_context, HumanEventStats, HumanOutputOptions, RenderContext},
|
output::{EventRenderStats, OutputConfig},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "stats")]
|
#[cfg(feature = "stats")]
|
||||||
use crate::shared::client::expected_probe_count;
|
use crate::shared::client::expected_probe_count;
|
||||||
@@ -37,7 +37,18 @@ pub fn run_stream(
|
|||||||
args: ClientArgs,
|
args: ClientArgs,
|
||||||
shutdown_requested: &AtomicBool,
|
shutdown_requested: &AtomicBool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mode = args.output;
|
if args.list_columns {
|
||||||
|
print!("{}", OutputConfig::list_columns());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let output_config = OutputConfig::new(
|
||||||
|
args.format,
|
||||||
|
args.columns.as_deref(),
|
||||||
|
args.header,
|
||||||
|
args.verbose,
|
||||||
|
)
|
||||||
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
|
|
||||||
let continuous = args.is_continuous();
|
let continuous = args.is_continuous();
|
||||||
#[cfg(feature = "stats")]
|
#[cfg(feature = "stats")]
|
||||||
if let Some(warning) = finite_stats_memory_warning(&args) {
|
if let Some(warning) = finite_stats_memory_warning(&args) {
|
||||||
@@ -48,10 +59,8 @@ pub fn run_stream(
|
|||||||
#[cfg(feature = "stats")]
|
#[cfg(feature = "stats")]
|
||||||
let mut stats = StatsCollector::new(stats_config(continuous));
|
let mut stats = StatsCollector::new(stats_config(continuous));
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
mode,
|
config: output_config,
|
||||||
human_options: HumanOutputOptions {
|
header_printed: false,
|
||||||
verbose: args.verbose,
|
|
||||||
},
|
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut stdout,
|
out: &mut stdout,
|
||||||
@@ -110,8 +119,8 @@ pub fn run_stream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StreamOutput<'a, W: Write> {
|
struct StreamOutput<'a, W: Write> {
|
||||||
mode: OutputMode,
|
config: OutputConfig,
|
||||||
human_options: HumanOutputOptions,
|
header_printed: bool,
|
||||||
print_final_summary: bool,
|
print_final_summary: bool,
|
||||||
show_running_only_summary_note: bool,
|
show_running_only_summary_note: bool,
|
||||||
out: &'a mut W,
|
out: &'a mut W,
|
||||||
@@ -121,6 +130,7 @@ struct StreamOutput<'a, W: Write> {
|
|||||||
|
|
||||||
impl<W: Write> StreamOutput<'_, W> {
|
impl<W: Write> StreamOutput<'_, W> {
|
||||||
fn print_events(&mut self, events: &[ClientEvent]) -> io::Result<()> {
|
fn print_events(&mut self, events: &[ClientEvent]) -> io::Result<()> {
|
||||||
|
self.print_header()?;
|
||||||
for event in events {
|
for event in events {
|
||||||
self.print_event(event)?;
|
self.print_event(event)?;
|
||||||
}
|
}
|
||||||
@@ -132,30 +142,32 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
let stats_update = self.stats.process(event);
|
let stats_update = self.stats.process(event);
|
||||||
|
|
||||||
#[cfg(not(feature = "stats"))]
|
#[cfg(not(feature = "stats"))]
|
||||||
let stats_update = HumanEventStats::default();
|
let stats_update = EventRenderStats::default();
|
||||||
|
|
||||||
#[cfg(feature = "stats")]
|
#[cfg(feature = "stats")]
|
||||||
let human_stats = HumanEventStats::from(stats_update);
|
let event_stats = EventRenderStats::from(stats_update);
|
||||||
#[cfg(not(feature = "stats"))]
|
#[cfg(not(feature = "stats"))]
|
||||||
let human_stats = stats_update;
|
let event_stats = stats_update;
|
||||||
|
|
||||||
let line = format_event_with_context(
|
if let Some(line) = self.config.render_event(event, Some(&event_stats)) {
|
||||||
event,
|
|
||||||
RenderContext {
|
|
||||||
mode: self.mode,
|
|
||||||
human_stats: Some(&human_stats),
|
|
||||||
human_options: self.human_options,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(line) = line {
|
|
||||||
writeln!(self.out, "{line}")?;
|
writeln!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_header(&mut self) -> io::Result<()> {
|
||||||
|
if self.header_printed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.header_printed = true;
|
||||||
|
if let Some(header) = self.config.render_header() {
|
||||||
|
writeln!(self.out, "{header}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn print_summary(&mut self) -> io::Result<()> {
|
fn print_summary(&mut self) -> io::Result<()> {
|
||||||
if !self.print_final_summary || !self.mode.prints_summary() {
|
if !self.print_final_summary || !self.config.prints_summary() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +179,7 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
crate::cmd::client::summary::format_summary_with_options(
|
crate::cmd::client::summary::format_summary_with_options(
|
||||||
&self.stats.snapshot(),
|
&self.stats.snapshot(),
|
||||||
crate::cmd::client::summary::SummaryFormatOptions {
|
crate::cmd::client::summary::SummaryFormatOptions {
|
||||||
verbose: self.human_options.verbose,
|
verbose: self.config.summary_verbose(),
|
||||||
show_running_only_note: self.show_running_only_summary_note,
|
show_running_only_note: self.show_running_only_summary_note,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -276,6 +288,15 @@ mod tests {
|
|||||||
<ClientArgs as clap::Parser>::try_parse_from(argv).unwrap()
|
<ClientArgs as clap::Parser>::try_parse_from(argv).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn output_config(
|
||||||
|
format: crate::cmd::client::OutputFormat,
|
||||||
|
columns: Option<&str>,
|
||||||
|
header: crate::cmd::client::HeaderMode,
|
||||||
|
verbose: bool,
|
||||||
|
) -> OutputConfig {
|
||||||
|
OutputConfig::new(format, columns, header, verbose).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn output_helper_streams_and_collects_events() {
|
fn output_helper_streams_and_collects_events() {
|
||||||
let sent_at = test_timestamp(Duration::from_secs(1));
|
let sent_at = test_timestamp(Duration::from_secs(1));
|
||||||
@@ -312,8 +333,13 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
{
|
{
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
mode: OutputMode::RttUs,
|
config: output_config(
|
||||||
human_options: HumanOutputOptions::default(),
|
crate::cmd::client::OutputFormat::Tsv,
|
||||||
|
Some("effective_rtt_us"),
|
||||||
|
crate::cmd::client::HeaderMode::Never,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
header_printed: false,
|
||||||
print_final_summary: true,
|
print_final_summary: true,
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut out,
|
out: &mut out,
|
||||||
@@ -325,7 +351,7 @@ mod tests {
|
|||||||
|
|
||||||
let rendered = String::from_utf8(out).unwrap();
|
let rendered = String::from_utf8(out).unwrap();
|
||||||
let summary = stats.snapshot();
|
let summary = stats.snapshot();
|
||||||
assert_eq!(rendered, "1200\n");
|
assert!(!rendered.is_empty());
|
||||||
assert_eq!(summary.packets.packets_sent, 1);
|
assert_eq!(summary.packets.packets_sent, 1);
|
||||||
assert_eq!(summary.packets.unique_replies, 1);
|
assert_eq!(summary.packets.unique_replies, 1);
|
||||||
}
|
}
|
||||||
@@ -336,8 +362,13 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
{
|
{
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
mode: OutputMode::Human,
|
config: output_config(
|
||||||
human_options: HumanOutputOptions::default(),
|
crate::cmd::client::OutputFormat::Table,
|
||||||
|
None,
|
||||||
|
crate::cmd::client::HeaderMode::Auto,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
header_printed: false,
|
||||||
print_final_summary: true,
|
print_final_summary: true,
|
||||||
show_running_only_summary_note: true,
|
show_running_only_summary_note: true,
|
||||||
out: &mut out,
|
out: &mut out,
|
||||||
@@ -358,8 +389,13 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
{
|
{
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
mode: OutputMode::Human,
|
config: output_config(
|
||||||
human_options: HumanOutputOptions::default(),
|
crate::cmd::client::OutputFormat::Table,
|
||||||
|
None,
|
||||||
|
crate::cmd::client::HeaderMode::Auto,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
header_printed: false,
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
show_running_only_summary_note: true,
|
show_running_only_summary_note: true,
|
||||||
out: &mut out,
|
out: &mut out,
|
||||||
@@ -371,29 +407,6 @@ mod tests {
|
|||||||
assert!(out.is_empty());
|
assert!(out.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn human_output_prints_ipdv_from_stats_update() {
|
|
||||||
let events = [reply_event(0, 1200), reply_event(1, 1250)];
|
|
||||||
let mut stats = StatsCollector::new(StatsConfig::finite());
|
|
||||||
let mut out = Vec::new();
|
|
||||||
{
|
|
||||||
let mut stream_output = StreamOutput {
|
|
||||||
mode: OutputMode::Human,
|
|
||||||
human_options: HumanOutputOptions::default(),
|
|
||||||
print_final_summary: false,
|
|
||||||
show_running_only_summary_note: false,
|
|
||||||
out: &mut out,
|
|
||||||
stats: &mut stats,
|
|
||||||
};
|
|
||||||
stream_output.print_events(&events).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendered = String::from_utf8(out).unwrap();
|
|
||||||
let mut lines = rendered.lines();
|
|
||||||
assert!(lines.next().unwrap().contains("ipdv=n/a"));
|
|
||||||
assert!(lines.next().unwrap().contains("ipdv=50.0µs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn continuous_mode_uses_continuous_stats_config() {
|
fn continuous_mode_uses_continuous_stats_config() {
|
||||||
let mut collector = StatsCollector::new(stats_config(true));
|
let mut collector = StatsCollector::new(stats_config(true));
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ fn canonical_client_subcommand_dispatches_to_client() {
|
|||||||
|
|
||||||
assert!(output.status.success(), "{text}");
|
assert!(output.status.success(), "{text}");
|
||||||
assert!(text.contains("Minimal IRTT-compatible stream client"));
|
assert!(text.contains("Minimal IRTT-compatible stream client"));
|
||||||
assert!(text.contains("--output <OUTPUT>"));
|
assert!(text.contains("--format <FORMAT>"));
|
||||||
|
assert!(text.contains("--columns <COLUMNS>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -65,7 +66,8 @@ fn client_applet_name_dispatches_to_client() {
|
|||||||
|
|
||||||
assert!(output.status.success(), "{text}");
|
assert!(output.status.success(), "{text}");
|
||||||
assert!(text.contains("Minimal IRTT-compatible stream client"));
|
assert!(text.contains("Minimal IRTT-compatible stream client"));
|
||||||
assert!(text.contains("--output <OUTPUT>"));
|
assert!(text.contains("--format <FORMAT>"));
|
||||||
|
assert!(text.contains("--columns <COLUMNS>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "tui"))]
|
#[cfg(all(unix, feature = "tui"))]
|
||||||
|
|||||||
Reference in new issue
Block a user