Add --duration, --csv, --quiet flags for automated testing
All checks were successful
CI / test (push) Successful in 1m27s
All checks were successful
CI / test (push) Successful in 1m27s
- --duration N: run client test for N seconds then exit - --csv <file>: append results to CSV (creates with headers if new) - --quiet/-q: suppress terminal output (for scripted/machine use) CSV columns: timestamp, host, port, protocol, direction, duration_s, tx_avg_mbps, rx_avg_mbps, tx_bytes, rx_bytes, lost_packets, auth_type Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,10 @@ pub fn print_status(
|
|||||||
elapsed: Duration,
|
elapsed: Duration,
|
||||||
lost_packets: Option<u64>,
|
lost_packets: Option<u64>,
|
||||||
) {
|
) {
|
||||||
|
if crate::csv_output::is_quiet() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let secs = elapsed.as_secs_f64();
|
let secs = elapsed.as_secs_f64();
|
||||||
let bits = bytes as f64 * 8.0;
|
let bits = bytes as f64 * 8.0;
|
||||||
let bw = if secs > 0.0 { bits / secs } else { 0.0 };
|
let bw = if secs > 0.0 { bits / secs } else { 0.0 };
|
||||||
|
|||||||
83
src/csv_output.rs
Normal file
83
src/csv_output.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//! CSV output for machine-readable test results.
|
||||||
|
//!
|
||||||
|
//! Appends a row per test to the specified CSV file.
|
||||||
|
//! Creates the file with headers if it doesn't exist.
|
||||||
|
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
static CSV_FILE: Mutex<Option<String>> = Mutex::new(None);
|
||||||
|
static QUIET: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
const HEADER: &str = "timestamp,host,port,protocol,direction,duration_s,tx_avg_mbps,rx_avg_mbps,tx_bytes,rx_bytes,lost_packets,auth_type";
|
||||||
|
|
||||||
|
/// Initialize CSV output. Creates file with headers if needed.
|
||||||
|
pub fn init(path: &str) -> std::io::Result<()> {
|
||||||
|
let needs_header = !Path::new(path).exists() || std::fs::metadata(path)?.len() == 0;
|
||||||
|
|
||||||
|
if needs_header {
|
||||||
|
let mut f = OpenOptions::new().create(true).write(true).open(path)?;
|
||||||
|
writeln!(f, "{}", HEADER)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
*CSV_FILE.lock().unwrap() = Some(path.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_quiet(q: bool) {
|
||||||
|
QUIET.store(q, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_quiet() -> bool {
|
||||||
|
QUIET.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a test result row to the CSV file.
|
||||||
|
pub fn write_result(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
protocol: &str,
|
||||||
|
direction: &str,
|
||||||
|
duration_secs: u64,
|
||||||
|
tx_bytes: u64,
|
||||||
|
rx_bytes: u64,
|
||||||
|
lost_packets: u64,
|
||||||
|
auth_type: &str,
|
||||||
|
) {
|
||||||
|
let guard = CSV_FILE.lock().unwrap();
|
||||||
|
if let Some(ref path) = *guard {
|
||||||
|
let tx_mbps = if duration_secs > 0 {
|
||||||
|
tx_bytes as f64 * 8.0 / duration_secs as f64 / 1_000_000.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let rx_mbps = if duration_secs > 0 {
|
||||||
|
rx_bytes as f64 * 8.0 / duration_secs as f64 / 1_000_000.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let row = format!(
|
||||||
|
"{},{},{},{},{},{},{:.2},{:.2},{},{},{},{}",
|
||||||
|
now, host, port, protocol, direction, duration_secs,
|
||||||
|
tx_mbps, rx_mbps, tx_bytes, rx_bytes, lost_packets, auth_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(mut f) = OpenOptions::new().append(true).open(path) {
|
||||||
|
let _ = writeln!(f, "{}", row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if CSV output is enabled.
|
||||||
|
pub fn is_enabled() -> bool {
|
||||||
|
CSV_FILE.lock().unwrap().is_some()
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod csv_output;
|
||||||
pub mod bandwidth;
|
pub mod bandwidth;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod ecsrp5;
|
pub mod ecsrp5;
|
||||||
|
|||||||
68
src/main.rs
68
src/main.rs
@@ -1,6 +1,7 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
mod bandwidth;
|
mod bandwidth;
|
||||||
mod client;
|
mod client;
|
||||||
|
pub mod csv_output;
|
||||||
mod ecsrp5;
|
mod ecsrp5;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod server;
|
mod server;
|
||||||
@@ -74,6 +75,18 @@ struct Cli {
|
|||||||
#[arg(short = 'n', long = "nat")]
|
#[arg(short = 'n', long = "nat")]
|
||||||
nat: bool,
|
nat: bool,
|
||||||
|
|
||||||
|
/// Test duration in seconds (client mode, 0=unlimited)
|
||||||
|
#[arg(short = 'd', long = "duration", default_value_t = 0)]
|
||||||
|
duration: u64,
|
||||||
|
|
||||||
|
/// Output results to CSV file (appends if exists)
|
||||||
|
#[arg(long = "csv")]
|
||||||
|
csv: Option<String>,
|
||||||
|
|
||||||
|
/// Suppress terminal output (use with --csv for machine-readable only)
|
||||||
|
#[arg(long = "quiet", short = 'q')]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
/// Send logs to remote syslog server (e.g., 192.168.1.1:514)
|
/// Send logs to remote syslog server (e.g., 192.168.1.1:514)
|
||||||
#[arg(long = "syslog")]
|
#[arg(long = "syslog")]
|
||||||
syslog: Option<String>,
|
syslog: Option<String>,
|
||||||
@@ -107,6 +120,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize CSV output if requested
|
||||||
|
if let Some(ref csv_path) = cli.csv {
|
||||||
|
if let Err(e) = csv_output::init(csv_path) {
|
||||||
|
eprintln!("Warning: failed to initialize CSV output to {}: {}", csv_path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
csv_output::set_quiet(cli.quiet);
|
||||||
|
|
||||||
if cli.server {
|
if cli.server {
|
||||||
// Server mode
|
// Server mode
|
||||||
let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) };
|
let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) };
|
||||||
@@ -143,18 +164,55 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
_ => (0, 0),
|
_ => (0, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
client::run_client(
|
let dir_str = match direction {
|
||||||
|
CMD_DIR_RX => "send",
|
||||||
|
CMD_DIR_TX => "receive",
|
||||||
|
CMD_DIR_BOTH => "both",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
let proto_str = if cli.udp { "UDP" } else { "TCP" };
|
||||||
|
|
||||||
|
// Run client with optional duration timeout
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let client_fut = client::run_client(
|
||||||
&host,
|
&host,
|
||||||
cli.port,
|
cli.port,
|
||||||
direction,
|
direction,
|
||||||
cli.udp,
|
cli.udp,
|
||||||
tx_speed,
|
tx_speed,
|
||||||
rx_speed,
|
rx_speed,
|
||||||
cli.auth_user,
|
cli.auth_user.clone(),
|
||||||
cli.auth_pass,
|
cli.auth_pass.clone(),
|
||||||
cli.nat,
|
cli.nat,
|
||||||
)
|
);
|
||||||
.await?;
|
|
||||||
|
if cli.duration > 0 {
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(cli.duration),
|
||||||
|
client_fut,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result?,
|
||||||
|
Err(_) => {
|
||||||
|
// Timeout — normal exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client_fut.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_secs();
|
||||||
|
|
||||||
|
// Write CSV if enabled
|
||||||
|
if csv_output::is_enabled() {
|
||||||
|
let auth_type = if cli.auth_user.is_some() { "auth" } else { "none" };
|
||||||
|
// For client mode we don't track detailed stats yet, but duration is useful
|
||||||
|
csv_output::write_result(
|
||||||
|
&host, cli.port, proto_str, dir_str,
|
||||||
|
elapsed, 0, 0, 0, auth_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Error: Must specify either -s (server) or -c <host> (client)");
|
eprintln!("Error: Must specify either -s (server) or -c <host> (client)");
|
||||||
eprintln!("Run with --help for usage information.");
|
eprintln!("Run with --help for usage information.");
|
||||||
|
|||||||
Reference in New Issue
Block a user