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

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));
}
}