Browse Source

feature: Implement split-timehist visualization

pull/2/head
JuanLeon Lahoz 5 years ago
parent
commit
a89e39e54c
  1. 2
      Makefile
  2. 12
      README.md
  3. BIN
      resources/split-timehist-example.png
  4. 54
      src/app.rs
  5. 33
      src/main.rs
  6. 27
      src/plot/mod.rs
  7. 213
      src/plot/splittimehist.rs
  8. 20
      src/plot/timehist.rs
  9. 11
      src/read/dateparser.rs
  10. 2
      src/read/mod.rs
  11. 169
      src/read/splittimes.rs
  12. 9
      src/read/times.rs
  13. 34
      tests/integration_tests.rs

2
Makefile

@ -12,4 +12,4 @@ test:
# Sadly, this misses coverage for those integrations tests that use
# assert_cmd, as it does not follow forks
coverage:
cargo tarpaulin -o Html -- --test-threads 1
cargo tarpaulin -o Html --ignore-tests -- --test-threads 1

12
README.md

@ -22,7 +22,7 @@ terminal.
Type `lowcharts --help`, or `lowcharts PLOT-TYPE --help` for a complete list of
options.
Currently four basic types of plots are supported:
Currently five basic types of plots are supported:
#### Bar chart for matches in the input
@ -125,6 +125,16 @@ timezone part of the format string (the autodetection works fine with
timezones).
#### Split Time Histogram
This adds up the time histogram and bar chart in a single visualization.
This chart is generated using `strace -tt ls -lR 2>&1 | lowcharts split-timehist open mmap close read write --intervals 10`:
[![Sample plot with lowcharts](resources/split-timehist-example.png)](resources/split-timehist-example.png)
This graph depicts the relative frequency of search terms in time.
### Installing
#### Via release

BIN
resources/split-timehist-example.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

54
src/app.rs

@ -9,6 +9,16 @@ fn add_input(app: App) -> App {
)
}
fn add_input_as_option(app: App) -> App {
app.arg(
Arg::new("input")
.long("input")
.default_value("-")
.long_about("If not present or a single dash, standard input will be used")
.takes_value(true),
)
}
fn add_min_max(app: App) -> App {
app.arg(
Arg::new("max")
@ -109,7 +119,7 @@ pub fn get_app() -> App<'static> {
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::AllowMissingPositional)
.about("Plot barchar with counts of occurences of matches params");
matches = add_input(add_width(matches)).arg(
matches = add_input_as_option(add_width(matches)).arg(
Arg::new("match")
.about("Count maches for those strings")
.required(true)
@ -139,6 +149,25 @@ pub fn get_app() -> App<'static> {
));
timehist = add_input(add_width(add_non_capturing_regex(add_intervals(timehist))));
let mut splittimehist = App::new("split-timehist")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.about("Plot histogram of with amount of matches over time, split per match type")
.arg(
Arg::new("format")
.long("format")
.short('f')
.about("Use this string formatting")
.takes_value(true),
);
splittimehist = add_input_as_option(add_width(add_intervals(splittimehist))).arg(
Arg::new("match")
.about("Count maches for those strings")
.required(true)
.takes_value(true)
.multiple(true),
);
App::new("lowcharts")
.author(clap::crate_authors!())
.version(clap::crate_version!())
@ -166,6 +195,7 @@ pub fn get_app() -> App<'static> {
.subcommand(plot)
.subcommand(matches)
.subcommand(timehist)
.subcommand(splittimehist)
}
#[cfg(test)]
@ -210,15 +240,22 @@ mod tests {
#[test]
fn matches_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "matches", "-", "A", "B", "C"];
let arg_vec = vec!["lowcharts", "matches", "A", "B", "C"];
let m = get_app().get_matches_from(arg_vec);
let sub_m = m.subcommand_matches("matches").unwrap();
assert_eq!("-", sub_m.value_of("input").unwrap());
assert_eq!(
// vec![String::from("A"), String::from("B"), String::from("C")],
vec!["A", "B", "C"],
sub_m.values_of("match").unwrap().collect::<Vec<&str>>()
);
let arg_vec = vec!["lowcharts", "matches", "A", "--input", "B", "C"];
let m = get_app().get_matches_from(arg_vec);
let sub_m = m.subcommand_matches("matches").unwrap();
assert_eq!("B", sub_m.value_of("input").unwrap());
assert_eq!(
vec!["A", "C"],
sub_m.values_of("match").unwrap().collect::<Vec<&str>>()
);
}
#[test]
@ -229,4 +266,15 @@ mod tests {
assert_eq!("some", sub_m.value_of("input").unwrap());
assert_eq!("foo", sub_m.value_of("regex").unwrap());
}
#[test]
fn splittimehist_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "split-timehist", "foo", "bar"];
let m = get_app().get_matches_from(arg_vec);
let sub_m = m.subcommand_matches("split-timehist").unwrap();
assert_eq!(
vec!["foo", "bar"],
sub_m.values_of("match").unwrap().collect::<Vec<&str>>()
);
}
}

33
src/main.rs

@ -193,6 +193,38 @@ fn timehist(matches: &ArgMatches) -> i32 {
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!("{:width$}", timehist, width = width);
};
0
}
fn main() {
let matches = app::get_app().get_matches();
configure_output(
@ -204,6 +236,7 @@ fn main() {
Some(("plot", subcommand_matches)) => plot(subcommand_matches),
Some(("matches", subcommand_matches)) => matchbar(subcommand_matches),
Some(("timehist", subcommand_matches)) => timehist(subcommand_matches),
Some(("split-timehist", subcommand_matches)) => splittime(subcommand_matches),
_ => unreachable!("Invalid subcommand"),
});
}

27
src/plot/mod.rs

@ -1,9 +1,36 @@
pub use self::histogram::Histogram;
pub use self::matchbar::{MatchBar, MatchBarRow};
pub use self::splittimehist::SplitTimeHistogram;
pub use self::timehist::TimeHistogram;
pub use self::xy::XyPlot;
mod histogram;
mod matchbar;
mod splittimehist;
mod timehist;
mod xy;
/// Returns a datetime formating string with a resolution that makes sense for a
/// given number of seconds
fn date_fmt_string(seconds: i64) -> &'static str {
match seconds {
x if x > 86400 => "%Y-%m-%d %H:%M:%S",
x if x > 300 => "%H:%M:%S",
x if x > 1 => "%H:%M:%S%.3f",
_ => "%H:%M:%S%.6f",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fmt_strings() {
assert_eq!(date_fmt_string(100000), "%Y-%m-%d %H:%M:%S");
assert_eq!(date_fmt_string(1000), "%H:%M:%S");
assert_eq!(date_fmt_string(10), "%H:%M:%S%.3f");
assert_eq!(date_fmt_string(0), "%H:%M:%S%.6f");
}
}

213
src/plot/splittimehist.rs

@ -0,0 +1,213 @@
use std::fmt;
use chrono::{DateTime, Duration, FixedOffset};
use yansi::Color::{Blue, Cyan, Green, Magenta, Red};
use crate::plot::date_fmt_string;
const COLORS: &[yansi::Color] = &[Red, Blue, Magenta, Green, Cyan];
#[derive(Debug)]
struct TimeBucket {
start: DateTime<FixedOffset>,
count: Vec<usize>,
}
impl TimeBucket {
fn new(start: DateTime<FixedOffset>, counts: usize) -> TimeBucket {
TimeBucket {
start,
count: vec![0; counts],
}
}
fn inc(&mut self, index: usize) {
self.count[index] += 1;
}
fn total(&self) -> usize {
self.count.iter().sum::<usize>()
}
}
#[derive(Debug)]
pub struct SplitTimeHistogram {
vec: Vec<TimeBucket>,
strings: Vec<String>,
min: DateTime<FixedOffset>,
max: DateTime<FixedOffset>,
step: Duration,
last: usize,
nanos: u64,
}
impl SplitTimeHistogram {
pub fn new(
size: usize,
strings: Vec<String>,
ts: &[(DateTime<FixedOffset>, usize)],
) -> SplitTimeHistogram {
let mut vec = Vec::<TimeBucket>::with_capacity(size);
let min = ts.iter().min().unwrap().0;
let max = ts.iter().max().unwrap().0;
let step = max - min;
let inc = step / size as i32;
for i in 0..size {
vec.push(TimeBucket::new(min + (inc * i as i32), strings.len()));
}
let mut sth = SplitTimeHistogram {
vec,
strings,
min,
max,
step,
last: size - 1,
nanos: (max - min).num_microseconds().unwrap() as u64,
};
sth.load(ts);
sth
}
fn load(&mut self, vec: &[(DateTime<FixedOffset>, usize)]) {
for x in vec {
self.add(x.0, x.1);
}
}
fn add(&mut self, ts: DateTime<FixedOffset>, index: usize) {
if let Some(slot) = self.find_slot(ts) {
self.vec[slot].inc(index);
}
}
fn find_slot(&self, ts: DateTime<FixedOffset>) -> Option<usize> {
if ts < self.min || ts > self.max {
None
} else {
let x = (ts - self.min).num_microseconds().unwrap() as u64;
Some(((x * self.vec.len() as u64 / self.nanos) as usize).min(self.last))
}
}
// Clippy gets badly confused necause self.strings and COLORS may have
// different lengths
#[allow(clippy::needless_range_loop)]
fn fmt_row(
&self,
f: &mut fmt::Formatter,
row: &TimeBucket,
divisor: usize,
widths: &[usize],
ts_fmt: &str,
) -> fmt::Result {
write!(
f,
"[{}] [",
Blue.paint(format!("{}", row.start.format(ts_fmt)))
)?;
for i in 0..self.strings.len() {
write!(
f,
"{}",
COLORS[i].paint(format!("{:width$}", row.count[i], width = widths[i]))
)?;
if i < self.strings.len() - 1 {
write!(f, "/")?;
}
}
write!(f, "] ")?;
for i in 0..self.strings.len() {
write!(
f,
"{}",
COLORS[i].paint("∎".repeat(row.count[i] / divisor).to_string())
)?;
}
writeln!(f)
}
}
impl fmt::Display for SplitTimeHistogram {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let width = f.width().unwrap_or(100);
let total = self.vec.iter().map(|r| r.total()).sum::<usize>();
let top = self.vec.iter().map(|r| r.total()).max().unwrap_or(1);
let divisor = 1.max(top / width);
// These are the widths of every count column
let widths: Vec<usize> = (0..self.strings.len())
.map(|i| {
self.vec
.iter()
.map(|r| r.count[i].to_string().len())
.max()
.unwrap()
})
.collect();
writeln!(f, "Matches: {}.", total)?;
for (i, s) in self.strings.iter().enumerate() {
let total = self.vec.iter().map(|r| r.count[i]).sum::<usize>();
writeln!(f, "{}: {}.", COLORS[i].paint(s), total)?;
}
writeln!(
f,
"Each {} represents a count of {}",
Red.paint("∎"),
divisor
)?;
let ts_fmt = date_fmt_string(self.step.num_seconds());
for row in self.vec.iter() {
self.fmt_row(f, row, divisor, &widths, ts_fmt)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use yansi::Paint;
#[test]
fn test_big_time_interval() {
Paint::disable();
let mut vec = Vec::<(DateTime<FixedOffset>, usize)>::new();
vec.push((
DateTime::parse_from_rfc3339("2021-04-15T04:25:00+00:00").unwrap(),
1,
));
vec.push((
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(),
1,
));
vec.push((
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(),
0,
));
vec.push((
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(),
2,
));
for _ in 0..11 {
vec.push((
DateTime::parse_from_rfc3339("2023-04-15T04:25:00+00:00").unwrap(),
2,
));
}
let th = SplitTimeHistogram::new(
3,
vec!["one".to_string(), "two".to_string(), "three".to_string()],
&vec,
);
println!("{}", th);
let display = format!("{}", th);
assert!(display.contains("Matches: 15"));
assert!(display.contains("one: 1."));
assert!(display.contains("two: 2."));
assert!(display.contains("three: 12."));
assert!(display.contains("represents a count of 1"));
assert!(display.contains("[2021-04-15 04:25:00] [0/1/ 0] ∎\n"));
assert!(display.contains("[2021-12-14 12:25:00] [1/1/ 1] ∎∎∎\n"));
assert!(display.contains("[2022-08-14 20:25:00] [0/0/11] ∎∎∎∎∎∎∎∎∎∎∎\n"));
}
}

20
src/plot/timehist.rs

@ -3,13 +3,14 @@ use std::fmt;
use chrono::{DateTime, Duration, FixedOffset};
use yansi::Color::{Blue, Green, Red};
use crate::plot::date_fmt_string;
#[derive(Debug)]
struct TimeBucket {
start: DateTime<FixedOffset>,
count: usize,
}
// TODO: use trait for Bucket and TimeBucket
impl TimeBucket {
fn new(start: DateTime<FixedOffset>) -> TimeBucket {
TimeBucket { start, count: 0 }
@ -31,7 +32,6 @@ pub struct TimeHistogram {
nanos: u64,
}
// TODO: use trait for Histogram and TimeHistogram
impl TimeHistogram {
pub fn new(size: usize, ts: &[DateTime<FixedOffset>]) -> TimeHistogram {
let mut vec = Vec::<TimeBucket>::with_capacity(size);
@ -74,15 +74,6 @@ impl TimeHistogram {
Some(((x * self.vec.len() as u64 / self.nanos) as usize).min(self.last))
}
}
fn date_fmt_string(&self) -> &str {
match self.step.num_seconds() {
x if x > 86400 => "%Y-%m-%d %H:%M:%S",
x if x > 300 => "%H:%M:%S",
x if x > 1 => "%H:%M:%S%.3f",
_ => "%H:%M:%S%.6f",
}
}
}
impl fmt::Display for TimeHistogram {
@ -104,12 +95,12 @@ impl fmt::Display for TimeHistogram {
Red.paint("∎"),
Blue.paint(divisor.to_string()),
)?;
let fmt = self.date_fmt_string();
let ts_fmt = date_fmt_string(self.step.num_seconds());
for row in self.vec.iter() {
writeln!(
f,
"[{label}] [{count}] {bar}",
label = Blue.paint(format!("{}", row.start.format(fmt))),
label = Blue.paint(format!("{}", row.start.format(ts_fmt))),
count = Green.paint(format!("{:width$}", row.count, width = width_count)),
bar = Red.paint(format!("{:∎<width$}", "", width = row.count / divisor))
)?;
@ -134,7 +125,6 @@ mod tests {
vec.push(DateTime::parse_from_rfc3339("2023-04-15T04:25:00+00:00").unwrap());
let mut th = TimeHistogram::new(3, &vec);
th.load(&vec);
println!("{}", th);
let display = format!("{}", th);
assert!(display.contains("Matches: 5"));
assert!(display.contains("represents a count of 1"));
@ -152,8 +142,6 @@ mod tests {
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.006+00:00").unwrap());
let mut th = TimeHistogram::new(4, &vec);
th.load(&vec);
println!("{}", th);
println!("{:#?}", th);
let display = format!("{}", th);
assert!(display.contains("Matches: 3"));
assert!(display.contains("represents a count of 1"));

11
src/read/dateparser.rs

@ -28,7 +28,14 @@ pub struct LogDateParser {
}
impl LogDateParser {
pub fn new_with_guess(log_line: &str) -> Result<LogDateParser, String> {
pub fn new(log_line: &str, format_string: &Option<String>) -> Result<LogDateParser, String> {
match format_string {
Some(ts_format) => Self::new_with_format(&log_line, &ts_format),
None => Self::new_with_guess(&log_line),
}
}
fn new_with_guess(log_line: &str) -> Result<LogDateParser, String> {
// All the guess work assume that datetimes start with a digit, and that
// digit is the first digit in the log line. The approach is to locate
// the 1st digit and then try to parse as much text as possible with any
@ -50,7 +57,7 @@ impl LogDateParser {
Err(format!("Could not parse a timestamp in {}", log_line))
}
pub fn new_with_format(log_line: &str, format_string: &str) -> Result<LogDateParser, String> {
fn new_with_format(log_line: &str, format_string: &str) -> Result<LogDateParser, String> {
// We look for where the timestamp is in logs using a brute force
// approach with 1st log line, but capping the max length we scan for
for i in 0..log_line.len() {

2
src/read/mod.rs

@ -1,8 +1,10 @@
pub use self::buckets::{DataReader, DataReaderBuilder};
pub use self::splittimes::{SplitTimeReader, SplitTimeReaderBuilder};
pub use self::times::TimeReaderBuilder;
mod buckets;
mod dateparser;
mod splittimes;
mod times;
use std::fs::File;

169
src/read/splittimes.rs

@ -0,0 +1,169 @@
use std::io::BufRead;
use chrono::{DateTime, FixedOffset};
use crate::read::dateparser::LogDateParser;
use crate::read::open_file;
#[derive(Default, Builder)]
pub struct SplitTimeReader {
#[builder(setter(strip_option), default)]
matches: Vec<String>,
#[builder(setter(strip_option), default)]
ts_format: Option<String>,
}
impl SplitTimeReader {
pub fn read(&self, path: &str) -> Vec<(DateTime<FixedOffset>, usize)> {
let mut vec: Vec<(DateTime<FixedOffset>, usize)> = Vec::new();
let mut iterator = open_file(path).lines();
let first_line = match iterator.next() {
Some(Ok(as_string)) => as_string,
Some(Err(error)) => {
error!("{}", error);
return vec;
}
_ => return vec,
};
let parser = match LogDateParser::new(&first_line, &self.ts_format) {
Ok(p) => p,
Err(error) => {
error!("Could not figure out parsing strategy: {}", error);
return vec;
}
};
if let Ok(x) = parser.parse(&first_line) {
self.push_conditionally(x, &mut vec, &first_line);
}
for line in iterator {
match line {
Ok(string) => {
if let Ok(x) = parser.parse(&string) {
self.push_conditionally(x, &mut vec, &string);
}
}
Err(error) => error!("{}", error),
}
}
vec
}
fn push_conditionally(
&self,
d: DateTime<FixedOffset>,
vec: &mut Vec<(DateTime<FixedOffset>, usize)>,
line: &str,
) {
for (i, s) in self.matches.iter().enumerate() {
if line.contains(s) {
vec.push((d, i));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn split_time_reader_basic() {
let mut builder = SplitTimeReaderBuilder::default();
builder.matches(vec![
"foo".to_string(),
"bar".to_string(),
"gnat".to_string(),
]);
let reader = builder.build().unwrap();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap();
writeln!(file, "[2021-04-15T06:26:31+00:00] bar").unwrap();
writeln!(file, "[2021-04-15T06:27:31+00:00] foobar").unwrap();
writeln!(file, "[2021-04-15T06:28:31+00:00] none").unwrap();
writeln!(file, "[2021-04-15T06:29:31+00:00] foo").unwrap();
writeln!(file, "[2021-04-15T06:30:31+00:00] none again").unwrap();
writeln!(file, "not even a timestamp").unwrap();
let ts = reader.read(file.path().to_str().unwrap());
assert_eq!(ts.len(), 5);
assert_eq!(
ts[0].0,
DateTime::parse_from_rfc3339("2021-04-15T06:25:31+00:00").unwrap()
);
assert_eq!(
ts[1].0,
DateTime::parse_from_rfc3339("2021-04-15T06:26:31+00:00").unwrap()
);
assert_eq!(
ts[2].0,
DateTime::parse_from_rfc3339("2021-04-15T06:27:31+00:00").unwrap()
);
assert_eq!(
ts[3].0,
DateTime::parse_from_rfc3339("2021-04-15T06:27:31+00:00").unwrap()
);
assert_eq!(
ts[4].0,
DateTime::parse_from_rfc3339("2021-04-15T06:29:31+00:00").unwrap()
);
assert_eq!(ts[0].1, 0);
assert_eq!(ts[1].1, 1);
assert_eq!(ts[2].1, 0);
assert_eq!(ts[3].1, 1);
assert_eq!(ts[4].1, 0);
}
#[test]
fn split_time_no_matches() {
let reader = SplitTimeReader::default();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap();
writeln!(file, "[2021-04-15T06:26:31+00:00] bar").unwrap();
let ts = reader.read(file.path().to_str().unwrap());
assert_eq!(ts.len(), 0);
}
#[test]
fn split_time_zero_matches() {
let mut builder = SplitTimeReaderBuilder::default();
builder.matches(vec![
"foo".to_string(),
"bar".to_string(),
"gnat".to_string(),
]);
builder.ts_format(String::from("%Y_%m_%d %H:%M"));
let reader = builder.build().unwrap();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "_2021_04_15 06:25] none").unwrap();
writeln!(file, "_2021_04_15 06:26] none").unwrap();
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap();
let ts = reader.read(file.path().to_str().unwrap());
assert_eq!(ts.len(), 0);
}
#[test]
fn split_time_bad_guess() {
let mut builder = SplitTimeReaderBuilder::default();
builder.matches(vec![
"foo".to_string(),
"bar".to_string(),
"gnat".to_string(),
]);
let reader = builder.build().unwrap();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "XXX none").unwrap();
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap();
let ts = reader.read(file.path().to_str().unwrap());
assert_eq!(ts.len(), 0);
}
#[test]
fn split_time_bad_file() {
let reader = SplitTimeReader::default();
let file = NamedTempFile::new().unwrap();
let ts = reader.read(file.path().to_str().unwrap());
assert_eq!(ts.len(), 0);
}
}

9
src/read/times.rs

@ -30,7 +30,7 @@ impl TimeReader {
}
_ => return vec,
};
let parser = match self.build_parser(&first_line) {
let parser = match LogDateParser::new(&first_line, &self.ts_format) {
Ok(p) => p,
Err(error) => {
error!("Could not figure out parsing strategy: {}", error);
@ -69,13 +69,6 @@ impl TimeReader {
vec
}
fn build_parser(&self, line: &str) -> Result<LogDateParser, String> {
match &self.ts_format {
Some(ts_format) => LogDateParser::new_with_format(&line, &ts_format),
None => LogDateParser::new_with_guess(&line),
}
}
fn push_conditionally(
&self,
d: DateTime<FixedOffset>,

34
tests/integration_tests.rs

@ -103,6 +103,40 @@ fn test_matchbar() {
.stdout(predicate::str::contains("\n[bar ] [2] ∎∎\n"));
}
#[test]
fn test_splittime() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
cmd.arg("split-timehist")
.arg("1")
.arg("2")
.arg("3")
.arg("4")
.arg("5")
.arg("6")
.assert()
.failure()
.stderr(predicate::str::contains(
"Only 5 different sub-groups are supported",
));
let mut cmd = Command::cargo_bin("lowcharts").unwrap();
cmd.arg("split-timehist")
.arg("A")
.arg("B")
.arg("C")
.arg("--intervals")
.arg("2")
.write_stdin("1619655527.888165 A\n1619655528.888165 A\n1619655527.888165 B\n")
.assert()
.success()
.stdout(predicate::str::contains("Matches: 3."))
.stdout(predicate::str::contains("A: 2"))
.stdout(predicate::str::contains("B: 1."))
.stdout(predicate::str::contains("C: 0."))
.stdout(predicate::str::contains("Each ∎ represents a count of 1\n"))
.stdout(predicate::str::contains("[00:18:47.888165] [1/1/0] ∎∎\n"))
.stdout(predicate::str::contains("[00:18:48.388165] [1/0/0] ∎\n"));
}
#[test]
fn test_plot() {
let mut cmd = Command::cargo_bin("lowcharts").unwrap();

Loading…
Cancel
Save