From e31dc91151c830c79adbfe5080b5786029ad65bb Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Tue, 29 Jan 2019 18:02:19 -0500 Subject: [PATCH] Add Asciidoc export option Signed-off-by: Chloe Kudryavtsev --- src/hyperfine/app.rs | 7 ++ src/hyperfine/export/asciidoc.rs | 207 +++++++++++++++++++++++++++++++ src/hyperfine/export/mod.rs | 6 + src/main.rs | 3 + 4 files changed, 223 insertions(+) create mode 100644 src/hyperfine/export/asciidoc.rs diff --git a/src/hyperfine/app.rs b/src/hyperfine/app.rs index daa179055..32e092167 100644 --- a/src/hyperfine/app.rs +++ b/src/hyperfine/app.rs @@ -146,6 +146,13 @@ fn build_app() -> App<'static, 'static> { .possible_values(&["millisecond", "second"]) .help("Set the time unit used. Possible values: millisecond, second."), ) + .arg( + Arg::with_name("export-asciidoc") + .long("export-asciidoc") + .takes_value(true) + .value_name("FILE") + .help("Export the timing summary statistics as an Asciidoc table to the given FILE."), + ) .arg( Arg::with_name("export-csv") .long("export-csv") diff --git a/src/hyperfine/export/asciidoc.rs b/src/hyperfine/export/asciidoc.rs new file mode 100644 index 000000000..d42d86e3c --- /dev/null +++ b/src/hyperfine/export/asciidoc.rs @@ -0,0 +1,207 @@ +use super::Exporter; + +use hyperfine::format::format_duration_value; +use hyperfine::types::BenchmarkResult; +use hyperfine::units::Unit; + +use std::io::Result; + +#[derive(Default)] +pub struct AsciidocExporter {} + +impl Exporter for AsciidocExporter { + fn serialize(&self, results: &Vec, unit: Option) -> Result> { + let unit = if let Some(unit) = unit { + // Use the given unit for all entries. + unit + } else if let Some(first_result) = results.first() { + // Use the first BenchmarkResult entry to determine the unit for all entries. + format_duration_value(first_result.mean, None).1 + } else { + // Default to `Second`. + Unit::Second + }; + + let mut res: Vec = Vec::new(); + res.append(&mut table_open()); + res.append(&mut table_startend()); + res.append(&mut table_header(unit)); + for result in results { + res.push('\n' as u8); + res.append(&mut table_row(result, unit)); + } + res.append(&mut table_startend()); + + Ok(res) + } +} + +fn table_open() -> Vec { + "[cols=\"<,>,>\"]\n".bytes().collect() +} + +fn table_startend() -> Vec { + "|===\n".bytes().collect() +} + +fn table_header(unittype: Unit) -> Vec { + let unit_short_name = unittype.short_name(); + format!( + "| Command | Mean [{unit}] | Min…Max [{unit}]\n", + unit = unit_short_name + ).into_bytes() +} + +fn table_row(entry: &BenchmarkResult, unit: Unit) -> Vec { + let form = |val| format_duration_value(val, Some(unit)); + format!( + "| `{}`\n\ + | {} ± {}\n\ + | {}…{}\n", + entry.command.replace("|", "\\|"), + form(entry.mean).0, form(entry.stddev).0, + form(entry.min).0, form(entry.max).0 + ).into_bytes() +} + +/// Ensure various options for the header generate correct results +#[test] +fn test_asciidoc_header() { + let conms: Vec = "| Command | Mean [ms] | Min…Max [ms]\n".bytes().collect(); + let cons: Vec = "| Command | Mean [s] | Min…Max [s]\n".bytes().collect(); + let genms = table_header(Unit::MilliSecond); + let gens = table_header(Unit::Second); + + assert_eq!(conms, genms); + assert_eq!(cons, gens); +} + +/// Ensure each table row is generated properly +#[test] +fn test_asciidoc_table_row() { + let result = BenchmarkResult::new( + String::from("sleep 1"), // command + 0.10491992406666667, // mean + 0.00397851689425097, // stddev + 0.005182013333333333, // user + 0.0, // system + 0.1003342584, // min + 0.10745223440000001, // max + vec![ // times + 0.1003342584, + 0.10745223440000001, + 0.10697327940000001 + ], + None // param + ); + + let expms = format!( + "| `{}`\n\ + | {} ± {}\n\ + | {}…{}\n", + result.command, + Unit::MilliSecond.format(result.mean), Unit::MilliSecond.format(result.stddev), + Unit::MilliSecond.format(result.min), Unit::MilliSecond.format(result.max) + ).into_bytes(); + let exps = format!( + "| `{}`\n\ + | {} ± {}\n\ + | {}…{}\n", + result.command, + Unit::Second.format(result.mean), Unit::Second.format(result.stddev), + Unit::Second.format(result.min), Unit::Second.format(result.max) + ).into_bytes(); + + let genms = table_row(&result, Unit::MilliSecond); + let gens = table_row(&result, Unit::Second); + + assert_eq!(expms, genms); + assert_eq!(exps, gens); +} + +/// Ensure commands get properly escaped +#[test] +fn test_asciidoc_table_row_command_escape() { + let result = BenchmarkResult::new( + String::from("sleep 1|"), // command + 0.10491992406666667, // mean + 0.00397851689425097, // stddev + 0.005182013333333333, // user + 0.0, // system + 0.1003342584, // min + 0.10745223440000001, // max + vec![ // times + 0.1003342584, + 0.10745223440000001, + 0.10697327940000001 + ], + None // param + ); + let exps = format!( + "| `sleep 1\\|`\n\ + | {} ± {}\n\ + | {}…{}\n", + Unit::Second.format(result.mean), Unit::Second.format(result.stddev), + Unit::Second.format(result.min), Unit::Second.format(result.max) + ).into_bytes(); + let gens = table_row(&result, Unit::Second); + + assert_eq!(exps, gens); +} + +/// Integration test +#[test] +fn test_asciidoc() { + let exporter = AsciidocExporter::default(); + // NOTE: results are fabricated, unlike above + let results = vec![ + BenchmarkResult::new( + String::from("command | 1"), + 1.0, + 2.0, + 3.0, + 4.0, + 5.0, + 6.0, + vec![ + 7.0, + 8.0, + 9.0 + ], + None + ), + BenchmarkResult::new( + String::from("command | 2"), + 11.0, + 12.0, + 13.0, + 14.0, + 15.0, + 16.0, + vec![ + 17.0, + 18.0, + 19.0 + ], + None + ) + ]; + // NOTE: only testing with s, s/ms is tested elsewhere + let exps: String = String::from( + "[cols=\"<,>,>\"]\n\ + |===\n\ + | Command | Mean [s] | Min…Max [s]\n\ + \n\ + | `command \\| 1`\n\ + | 1.000 ± 2.000\n\ + | 5.000…6.000\n\ + \n\ + | `command \\| 2`\n\ + | 11.000 ± 12.000\n\ + | 15.000…16.000\n\ + |===\n\ + "); + let gens = String::from_utf8(exporter.serialize(&results, Some(Unit::Second)).unwrap()).unwrap(); + + assert_eq!(exps, gens); +} diff --git a/src/hyperfine/export/mod.rs b/src/hyperfine/export/mod.rs index b7ec0c9f1..f8c06fda5 100644 --- a/src/hyperfine/export/mod.rs +++ b/src/hyperfine/export/mod.rs @@ -1,7 +1,9 @@ +mod asciidoc; mod csv; mod json; mod markdown; +use self::asciidoc::AsciidocExporter; use self::csv::CsvExporter; use self::json::JsonExporter; use self::markdown::MarkdownExporter; @@ -15,6 +17,9 @@ use hyperfine::units::Unit; /// The desired form of exporter to use for a given file. #[derive(Clone)] pub enum ExportType { + /// Asciidoc Table + Asciidoc, + /// CSV (comma separated values) format Csv, @@ -52,6 +57,7 @@ impl ExportManager { /// Add an additional exporter to the ExportManager pub fn add_exporter(&mut self, export_type: ExportType, filename: &str) { let exporter: Box = match export_type { + ExportType::Asciidoc => Box::new(AsciidocExporter::default()), ExportType::Csv => Box::new(CsvExporter::default()), ExportType::Json => Box::new(JsonExporter::default()), ExportType::Markdown => Box::new(MarkdownExporter::default()), diff --git a/src/main.rs b/src/main.rs index 3dd6bfa3f..86dc922eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -216,6 +216,9 @@ fn build_hyperfine_options(matches: &ArgMatches) -> Result ExportManager { let mut export_manager = ExportManager::new(); + if let Some(filename) = matches.value_of("export-asciidoc") { + export_manager.add_exporter(ExportType::Asciidoc, filename); + } if let Some(filename) = matches.value_of("export-json") { export_manager.add_exporter(ExportType::Json, filename); }