mod app; mod format; mod plot; mod read; mod stats; use std::env; #[macro_use] extern crate derive_builder; #[macro_use] extern crate log; use chrono::Duration; use clap::ArgMatches; use regex::Regex; use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; use yansi::Paint; /// True if vec has al least 'min' elements fn assert_data(vec: &[T], min: usize) -> bool { if vec.len() < min { warn!("Not enough data to process"); } vec.len() >= min } /// Sets up color choices and verbosity in the two libraries used for output: /// simplelog and yansi fn configure_output(option: &str, verbose: bool) { let mut color_choice = ColorChoice::Auto; match option { "no" => { Paint::disable(); color_choice = ColorChoice::Never; } "auto" => match env::var("TERM") { Ok(value) if value == "dumb" => Paint::disable(), _ => { if atty::isnt(atty::Stream::Stdout) { Paint::disable(); } } }, "yes" => { color_choice = ColorChoice::Always; } _ => (), }; if let Err(err) = TermLogger::init( if verbose { LevelFilter::Debug } else { LevelFilter::Info }, ConfigBuilder::new() .set_time_level(LevelFilter::Trace) .set_thread_level(LevelFilter::Trace) .set_target_level(LevelFilter::Trace) .build(), TerminalMode::Stderr, color_choice, ) { // We trigger this error when unit testing this fn eprintln!("Error: {err}"); } } fn parse_duration(duration: &str) -> Result { match humantime::parse_duration(duration) { Ok(d) => Ok(Duration::milliseconds(d.as_millis() as i64)), Err(error) => Err(error), } } /// Build a reader able to read floats (potentially capturing them with regex) /// from an input source. fn get_float_reader(matches: &ArgMatches) -> Result { let mut builder = read::DataReaderBuilder::default(); if matches.is_present("min") || matches.is_present("max") { let min = matches.value_of_t("min").unwrap_or(f64::NEG_INFINITY); let max = matches.value_of_t("max").unwrap_or(f64::INFINITY); if min > max { error!("Minimum should be smaller than maximum"); return Err(()); } builder.range(min..max); } if let Some(string) = matches.value_of("regex") { match Regex::new(string) { Ok(re) => { builder.regex(re); } _ => { error!("Failed to parse regex {}", string); return Err(()); } }; } Ok(builder.build().unwrap()) } /// Implements the hist cli-subcommand fn histogram(matches: &ArgMatches) -> i32 { let reader = match get_float_reader(matches) { Ok(r) => r, _ => return 2, }; let vec = reader.read(matches.value_of("input").unwrap()); if !assert_data(&vec, 1) { return 1; } let mut options = plot::HistogramOptions::default(); let precision_arg: i32 = matches.value_of_t("precision").unwrap(); if precision_arg > 0 { options.precision = Some(precision_arg as usize); }; options.log_scale = matches.is_present("log-scale"); options.intervals = matches.value_of_t("intervals").unwrap(); let width = matches.value_of_t("width").unwrap(); let histogram = plot::Histogram::new(&vec, options); print!("{histogram:width$}"); 0 } /// Implements the plot cli-subcommand fn plot(matches: &ArgMatches) -> i32 { let reader = match get_float_reader(matches) { Ok(r) => r, _ => return 2, }; let vec = reader.read(matches.value_of("input").unwrap()); if !assert_data(&vec, 1) { return 1; } let precision_arg: i32 = matches.value_of_t("precision").unwrap(); let precision = if precision_arg < 0 { None } else { Some(precision_arg as usize) }; let plot = plot::XyPlot::new( &vec, matches.value_of_t("width").unwrap(), matches.value_of_t("height").unwrap(), precision, ); print!("{plot}"); 0 } /// Implements the matches cli-subcommand fn matchbar(matches: &ArgMatches) -> i32 { let reader = read::DataReader::default(); let width = matches.value_of_t("width").unwrap(); print!( "{:width$}", reader.read_matches( matches.value_of("input").unwrap(), matches.values_of("match").unwrap().collect() ), width = width ); 0 } /// Implements the common-terms cli-subcommand fn common_terms(matches: &ArgMatches) -> i32 { let mut builder = read::DataReaderBuilder::default(); if let Some(string) = matches.value_of("regex") { match Regex::new(string) { Ok(re) => { builder.regex(re); } _ => { error!("Failed to parse regex {}", string); return 1; } }; } else { builder.regex(Regex::new("(.*)").unwrap()); }; let reader = builder.build().unwrap(); let width = matches.value_of_t("width").unwrap(); let lines = matches.value_of_t("lines").unwrap(); if lines < 1 { error!("You should specify a potitive number of lines"); return 2; }; print!( "{:width$}", reader.read_terms(matches.value_of("input").unwrap(), lines), width = width ); 0 } /// Implements the timehist cli-subcommand fn timehist(matches: &ArgMatches) -> i32 { let mut builder = read::TimeReaderBuilder::default(); if let Some(string) = matches.value_of("regex") { match Regex::new(string) { Ok(re) => { builder.regex(re); } _ => { error!("Failed to parse regex {}", string); return 2; } }; } if let Some(as_str) = matches.value_of("format") { builder.ts_format(as_str.to_string()); } builder.early_stop(matches.is_present("early-stop")); if let Some(duration) = matches.value_of("duration") { match parse_duration(duration) { Ok(d) => builder.duration(d), Err(err) => { error!("Failed to parse duration {}: {}", duration, err); return 2; } }; }; let width = matches.value_of_t("width").unwrap(); let reader = builder.build().unwrap(); let vec = reader.read(matches.value_of("input").unwrap()); if assert_data(&vec, 2) { let timehist = plot::TimeHistogram::new(matches.value_of_t("intervals").unwrap(), &vec); print!("{timehist:width$}"); }; 0 } /// Implements the timehist cli-subcommand fn splittime(matches: &ArgMatches) -> i32 { let mut builder = read::SplitTimeReaderBuilder::default(); let string_list: Vec = match matches.values_of("match") { Some(s) => s.map(|s| s.to_string()).collect(), None => { error!("At least a match is needed"); return 2; } }; if string_list.len() > 5 { error!("Only 5 different sub-groups are supported"); return 2; } if let Some(as_str) = matches.value_of("format") { builder.ts_format(as_str.to_string()); } builder.matches(string_list.iter().map(|s| s.to_string()).collect()); let width = matches.value_of_t("width").unwrap(); let reader = builder.build().unwrap(); let vec = reader.read(matches.value_of("input").unwrap()); if assert_data(&vec, 2) { let timehist = plot::SplitTimeHistogram::new( matches.value_of_t("intervals").unwrap(), string_list, &vec, ); print!("{timehist:width$}"); }; 0 } fn main() { let matches = app::get_app().get_matches(); configure_output( matches.value_of("color").unwrap(), matches.is_present("verbose"), ); std::process::exit(match matches.subcommand() { Some(("hist", subcommand_matches)) => histogram(subcommand_matches), Some(("plot", subcommand_matches)) => plot(subcommand_matches), Some(("matches", subcommand_matches)) => matchbar(subcommand_matches), Some(("timehist", subcommand_matches)) => timehist(subcommand_matches), Some(("common-terms", subcommand_matches)) => common_terms(subcommand_matches), Some(("split-timehist", subcommand_matches)) => splittime(subcommand_matches), _ => unreachable!("Invalid subcommand"), }); } #[cfg(test)] mod tests { use super::*; use yansi::Color::Blue; #[test] fn test_output_yes() { Paint::enable(); configure_output("yes", true); let display = format!("{}", Blue.paint("blue")); assert_eq!("\u{1b}[34mblue\u{1b}[0m", display); assert_eq!(LevelFilter::Debug, log::max_level()); } #[test] fn test_output_no() { Paint::enable(); configure_output("no", false); let display = format!("{}", Blue.paint("blue")); assert_eq!("blue", display); assert_eq!(LevelFilter::Info, log::max_level()); } #[test] fn test_output_auto() { Paint::enable(); env::set_var("TERM", "dumb"); configure_output("auto", false); let display = format!("{}", Blue.paint("blue")); assert_eq!("blue", display); } #[test] fn test_duration() { assert_eq!( parse_duration("2h 30m 5s 100ms"), Ok(Duration::milliseconds( 2 * 60 * 60000 + 30 * 60000 + 5000 + 100 )) ); assert_eq!(parse_duration("3days"), Ok(Duration::days(3))); assert!(parse_duration("bananas").is_err()); } #[test] fn test_assert_data() { let v = vec![true]; assert!(assert_data(&v, 1)); assert!(!assert_data(&v, 2)); let v = Vec::::new(); assert!(!assert_data(&v, 1)); } }