fix(cli): report multi-target failures and return nonzero

Consume structured per-target outcomes in the streaming CLI and TUI so mixed failures remain visible while successful targets still produce output. Return a failure status when every target fails, and keep interruption behavior distinct.

Add strict-timeout integration coverage for mixed continuous completion and all-open-failure exit status, plus focused TUI outcome-state tests.
This commit is contained in:
Nils Andreas Svee committed 2026-07-28 01:04:42 +07:00
1 parent 97aac829ec
commit 4398d25d4b
5 files changed
+428 -18

No files matched your search

+50 -12
View File
@@ -10,7 +10,8 @@ use std::{
use irtt_client::{
ClientEvent, EventSubscriptionError, ManagedClientGroup, ManagedClientGroupConfig,
ManagedGroupEndReason, ManagedGroupEvent, SubscriberConfig, SubscriberOverflow, TargetEvent,
ManagedGroupEndReason, ManagedGroupEvent, ManagedTargetOutcome, SubscriberConfig,
SubscriberOverflow, TargetEvent,
};
#[cfg(all(test, feature = "stats"))]
use irtt_client::{ClientTimestamp, PacketMeta, RttSample, SignedDuration};
@@ -347,6 +348,7 @@ fn run_group_stream(
}
}
ManagedGroupEvent::TargetFinished(target) => {
report_target_failure(&target);
terminal_targets.insert(target.id.as_str().to_owned());
}
}
@@ -374,18 +376,25 @@ fn run_group_stream(
let outcome = session.join()?;
while let Ok(Some(group_event)) = events.try_recv() {
if let ManagedGroupEvent::Client(target_event) = group_event {
#[cfg(feature = "stats")]
{
let label = target_event.target.as_str();
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(&mut stream_output, &target_event, stats)?;
match group_event {
ManagedGroupEvent::Client(target_event) => {
#[cfg(feature = "stats")]
{
let label = target_event.target.as_str();
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(&mut stream_output, &target_event, stats)?;
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
}
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
ManagedGroupEvent::TargetFinished(target) => {
if terminal_targets.insert(target.id.as_str().to_owned()) {
report_target_failure(&target);
}
}
}
}
@@ -393,6 +402,26 @@ fn run_group_stream(
if outcome.end_reason == ManagedGroupEndReason::Cancelled && !interrupted {
return Err("managed client group was cancelled".into());
}
for target in &outcome.targets {
if terminal_targets.insert(target.id.as_str().to_owned()) {
report_target_failure(target);
}
}
let successful_targets = outcome
.targets
.iter()
.filter(|target| target.is_success())
.count();
let failed_targets = outcome
.targets
.iter()
.filter(|target| target.end_reason.failure().is_some())
.count();
if !interrupted && successful_targets == 0 && failed_targets > 0 {
return Err(
format!("no managed target completed successfully ({failed_targets} failed)").into(),
);
}
let print_final_summary = should_print_final_summary(continuous, interrupted);
stream_output.print_final_summary = print_final_summary;
@@ -415,6 +444,15 @@ fn run_group_stream(
Ok(())
}
fn report_target_failure(target: &ManagedTargetOutcome) {
if let Some(failure) = target.end_reason.failure() {
eprintln!(
"irtt-rs: target {} failed ({}): {}",
target.id, failure.kind, failure.message
);
}
}
fn estimated_group_completion_grace(args: &ClientArgs) -> Duration {
let open_timeout: Duration = args.to_client_config().open_timeouts.iter().sum();
open_timeout