use std::{path::PathBuf, time::Duration};
use clap::{Parser, ValueEnum};
use crate::format::OutputFormat;
#[derive(Debug, Parser)]
#[command(
name = "tricorder",
version,
about = "Run a command and report its process tree's CPU, memory, and I/O usage.",
long_about = None,
after_help = "Example:\n tricorder --out timing.tsv -- bash -c 'samtools sort big.bam'",
)]
pub struct Args {
#[arg(long, value_name = "PATH")]
pub out: PathBuf,
#[arg(long, value_enum, default_value_t = Format::Tsv)]
pub format: Format,
#[arg(long, value_name = "SECONDS", default_value_t = 0.5)]
pub interval: f64,
#[arg(long)]
pub summary: bool,
#[arg(long, value_name = "PATH")]
pub trace: Option<PathBuf>,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(trailing_var_arg = true, num_args = 1.., required = true, value_name = "CMD")]
pub command: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
Tsv,
Json,
}
impl From<Format> for OutputFormat {
fn from(value: Format) -> Self {
match value {
Format::Tsv => Self::Tsv,
Format::Json => Self::Json,
}
}
}
impl Args {
#[must_use]
pub fn interval_duration(&self) -> Duration {
const MIN_MS: u64 = 10;
let ms = (self.interval * 1000.0).round();
let ms = if ms.is_finite() && ms >= MIN_MS as f64 { ms as u64 } else { MIN_MS };
Duration::from_millis(ms)
}
#[must_use]
pub fn log_level(&self) -> log::LevelFilter {
match self.verbose {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parses_basic_args() {
let args =
Args::parse_from(["tricorder", "--out", "/tmp/timing.tsv", "--", "echo", "hello"]);
assert_eq!(args.out, PathBuf::from("/tmp/timing.tsv"));
assert_eq!(args.format, Format::Tsv);
assert_eq!(args.command, vec!["echo".to_string(), "hello".to_string()]);
}
#[test]
fn parses_json_format_and_summary() {
let args = Args::parse_from([
"tricorder",
"--out",
"out.json",
"--format",
"json",
"--summary",
"--",
"true",
]);
assert_eq!(args.format, Format::Json);
assert!(args.summary);
}
#[test]
fn interval_clamps_to_minimum() {
let args = Args::parse_from(["tricorder", "--out", "x", "--interval", "0", "--", "true"]);
assert_eq!(args.interval_duration(), Duration::from_millis(10));
}
#[test]
fn interval_respects_user_value() {
let args =
Args::parse_from(["tricorder", "--out", "x", "--interval", "1.25", "--", "true"]);
assert_eq!(args.interval_duration(), Duration::from_millis(1250));
}
#[test]
fn parses_trace_path() {
let args = Args::parse_from([
"tricorder",
"--out",
"out.tsv",
"--trace",
"/tmp/trace.tsv",
"--",
"true",
]);
assert_eq!(args.trace, Some(PathBuf::from("/tmp/trace.tsv")));
}
#[test]
fn trace_defaults_to_none() {
let args = Args::parse_from(["tricorder", "--out", "out.tsv", "--", "true"]);
assert!(args.trace.is_none());
}
#[test]
fn missing_command_fails() {
let result = Args::try_parse_from(["tricorder", "--out", "x"]);
assert!(result.is_err());
}
#[test]
fn log_level_scales_with_verbosity() {
let args = Args::parse_from(["tricorder", "--out", "x", "-vv", "--", "true"]);
assert_eq!(args.log_level(), log::LevelFilter::Debug);
}
}