You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
10 KiB
335 lines
10 KiB
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<T>(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<Duration, humantime::DurationError> { |
|
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<read::DataReader, ()> { |
|
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<String> = 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::<bool>::new(); |
|
assert!(!assert_data(&v, 1)); |
|
} |
|
}
|
|
|