From 5e655032bcf7bd37dbd68ce6aa102f8194c53a8b Mon Sep 17 00:00:00 2001 From: Philipp Paulweber Date: Sun, 22 May 2022 14:10:39 +0200 Subject: [PATCH] Refactor AsciiDocExporter emitting using the MarkupExporter trait --- src/export/asciidoc.rs | 370 ++++++++++++++++++----------------------- src/export/markup.rs | 39 +++-- 2 files changed, 190 insertions(+), 219 deletions(-) diff --git a/src/export/asciidoc.rs b/src/export/asciidoc.rs index f5c65da..3b5be53 100644 --- a/src/export/asciidoc.rs +++ b/src/export/asciidoc.rs @@ -1,209 +1,96 @@ -use crate::benchmark::benchmark_result::BenchmarkResult; -use crate::benchmark::relative_speed::{self, BenchmarkResultWithRelativeSpeed}; -use crate::output::format::format_duration_value; -use crate::util::units::Unit; - -use super::Exporter; - -use anyhow::{anyhow, Result}; +use super::markup::Alignment; +use crate::export::markup::MarkupExporter; #[derive(Default)] pub struct AsciidocExporter {} -impl Exporter for AsciidocExporter { - fn serialize(&self, results: &[BenchmarkResult], 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 - }; +impl MarkupExporter for AsciidocExporter { + fn table_header(&self, cell_aligmnents: &[Alignment]) -> String { + format!( + "[cols=\"{}\"]\n|===", + cell_aligmnents + .iter() + .map(|a| match a { + Alignment::Left => "<", + Alignment::Right => ">", + }) + .collect::>() + .join(",") + ) + } - if let Some(annotated_results) = relative_speed::compute(results) { - 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 annotated_results { - res.push(b'\n'); - res.append(&mut table_row(&result, unit)); - } - res.append(&mut table_startend()); + fn table_footer(&self, _cell_aligmnents: &[Alignment]) -> String { + "|===\n".to_string() + } - Ok(res) - } else { - Err(anyhow!( - "Relative speed comparison is not available for Asciidoctor export." - )) - } + fn table_row(&self, cells: &[&str]) -> String { + format!("\n| {} \n", cells.join(" \n| ")) + } + + fn table_divider(&self, _cell_aligmnents: &[Alignment]) -> String { + "".to_string() + } + + fn command(&self, cmd: &str) -> String { + format!("`{}`", cmd) } } -fn table_open() -> Vec { - "[cols=\"<,>,>,>,>\"]\n".bytes().collect() +/// Check Asciidoc-based data row formatting +#[test] +fn test_asciidoc_exporter_table_data() { + let exporter = AsciidocExporter::default(); + let data = vec!["a", "b", "c"]; + + let actual = exporter.table_row(&data); + let expect = "\n| a \n| b \n| c \n"; + + assert_eq!(expect, actual); } -fn table_startend() -> Vec { - "|===\n".bytes().collect() +/// Check Asciidoc-based table header formatting +#[test] +fn test_asciidoc_exporter_table_header() { + let exporter = AsciidocExporter::default(); + let cells_alignment = [ + Alignment::Left, + Alignment::Right, + Alignment::Right, + Alignment::Right, + Alignment::Right, + ]; + + let actual = exporter.table_header(&cells_alignment); + let expect = "[cols=\"<,>,>,>,>\"]\n|==="; + + assert_eq!(expect, actual); } -fn table_header(unittype: Unit) -> Vec { - let unit_short_name = unittype.short_name(); +/// Test helper function to create unit-based header and horizontal line +/// independently from the markup functionality for Asciidoc. +#[cfg(test)] +fn cfg_test_table_header(unit_short_name: &str) -> String { format!( - "| Command \n| Mean [{unit}] \n| Min [{unit}] \n| Max [{unit}] \n| Relative \n", + "[cols=\"<,>,>,>,>\"]\n|===\n| Command \n| Mean [{unit}] \n| Min [{unit}] \n| Max [{unit}] \n| Relative \n", unit = unit_short_name ) - .into_bytes() } -fn table_row(entry: &BenchmarkResultWithRelativeSpeed, unit: Unit) -> Vec { - let result = &entry.result; - let mean_str = format_duration_value(result.mean, Some(unit)).0; - let stddev_str = if let Some(stddev) = result.stddev { - format!(" ± {}", format_duration_value(stddev, Some(unit)).0) - } else { - "".into() - }; - let min_str = format_duration_value(result.min, Some(unit)).0; - let max_str = format_duration_value(result.max, Some(unit)).0; - let rel_str = format!("{:.2}", entry.relative_speed); - let rel_stddev_str = if entry.is_fastest { - "".into() - } else if let Some(stddev) = entry.relative_speed_stddev { - format!(" ± {:.2}", stddev) - } else { - "".into() - }; +#[cfg(test)] +use crate::util::units::Unit; - format!( - "| `{command}` \n\ - | {mean}{stddev} \n\ - | {min} \n\ - | {max} \n\ - | {rel}{rel_stddev} \n", - command = result.command.replace('|', "\\|"), - mean = mean_str, - stddev = stddev_str, - min = min_str, - max = max_str, - rel = rel_str, - rel_stddev = rel_stddev_str - ) - .into_bytes() -} +#[cfg(test)] +use crate::export::BenchmarkResult; -/// Ensure various options for the header generate correct results -#[test] -fn test_asciidoc_header() { - let conms: Vec = "| Command \n| Mean [ms] \n| Min [ms] \n| Max [ms] \n| Relative \n" - .bytes() - .collect(); - let cons: Vec = "| Command \n| Mean [s] \n| Min [s] \n| Max [s] \n| Relative \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() { - use std::collections::BTreeMap; - - let timing_result = BenchmarkResultWithRelativeSpeed { - result: &BenchmarkResult { - command: String::from("sleep 1"), - mean: 0.10491992406666667, - stddev: Some(0.00397851689425097), - median: 0.10491992406666667, - user: 0.005182013333333333, - system: 0.0, - min: 0.1003342584, - max: 0.10745223440000001, - times: Some(vec![0.1003342584, 0.10745223440000001, 0.10697327940000001]), - exit_codes: vec![Some(0), Some(0), Some(0)], - parameters: BTreeMap::new(), - }, - relative_speed: 1.000, - relative_speed_stddev: Option::from(1.03), - is_fastest: true, - }; - - let formatted = String::from_utf8(table_row(&timing_result, Unit::MilliSecond)).unwrap(); - - let formatted_expected = "| `sleep 1` \n\ - | 104.9 ± 4.0 \n\ - | 100.3 \n\ - | 107.5 \n\ - | 1.00 \n"; - - assert_eq!(formatted_expected, formatted); - - let formatted_seconds = String::from_utf8(table_row(&timing_result, Unit::Second)).unwrap(); - let formatted_expected_seconds = "| `sleep 1` \n\ - | 0.105 ± 0.004 \n\ - | 0.100 \n\ - | 0.107 \n\ - | 1.00 \n"; - - assert_eq!(formatted_expected_seconds, formatted_seconds); -} - -/// Ensure commands get properly escaped -#[test] -fn test_asciidoc_table_row_command_escape() { - use std::collections::BTreeMap; - let benchmark_result = BenchmarkResultWithRelativeSpeed { - result: &BenchmarkResult { - command: String::from("sleep 1|"), - mean: 0.10491992406666667, - stddev: Some(0.00397851689425097), - median: 0.10491992406666667, - user: 0.005182013333333333, - system: 0.0, - min: 0.1003342584, - max: 0.10745223440000001, - times: Some(vec![0.1003342584, 0.10745223440000001, 0.10697327940000001]), - exit_codes: vec![Some(0), Some(0), Some(0)], - parameters: BTreeMap::new(), - }, - relative_speed: 1.000, - relative_speed_stddev: Option::from(1.03), - is_fastest: true, - }; - let expected = String::from_utf8( - format!( - "| `sleep 1\\|` \n\ - | {} ± {} \n\ - | {} \n\ - | {} \n\ - | {:.2} \n", - Unit::Second.format(benchmark_result.result.mean), - Unit::Second.format(benchmark_result.result.stddev.unwrap()), - Unit::Second.format(benchmark_result.result.min), - Unit::Second.format(benchmark_result.result.max), - benchmark_result.relative_speed - ) - .into_bytes(), - ); - let generated_seconds = String::from_utf8(table_row(&benchmark_result, Unit::Second)); - - assert_eq!(expected, generated_seconds); -} +#[cfg(test)] +use crate::export::Exporter; /// Integration test #[test] -fn test_asciidoc() { +fn test_asciidoc_format_s() { use std::collections::BTreeMap; let exporter = AsciidocExporter::default(); - // NOTE: results are fabricated, unlike above + let results = vec![ BenchmarkResult { command: String::from("FOO=1 BAR=2 command | 1"), @@ -242,32 +129,99 @@ fn test_asciidoc() { }, }, ]; - // NOTE: only testing with s, s/ms is tested elsewhere - let expected: String = String::from( - "[cols=\"<,>,>,>,>\"]\n\ - |===\n\ - | Command \n\ - | Mean [s] \n\ - | Min [s] \n\ - | Max [s] \n\ - | Relative \n\ - \n\ - | `FOO=1 BAR=2 command \\| 1` \n\ - | 1.000 ± 2.000 \n\ - | 5.000 \n\ - | 6.000 \n\ - | 1.00 \n\ - \n\ - | `FOO=1 BAR=7 command \\| 2` \n\ - | 11.000 ± 12.000 \n\ - | 15.000 \n\ - | 16.000 \n\ - | 11.00 ± 25.06 \n\ - |===\n\ - ", - ); - let given = - String::from_utf8(exporter.serialize(&results, Some(Unit::Second)).unwrap()).unwrap(); - assert_eq!(expected, given); + let actual = + String::from_utf8(exporter.serialize(&results, Some(Unit::Second)).unwrap()).unwrap(); + let expect = format!( + "{} +| `FOO=1 BAR=2 command \\| 1` +| 1.000 ± 2.000 +| 5.000 +| 6.000 +| 1.00 + +| `FOO=1 BAR=7 command \\| 2` +| 11.000 ± 12.000 +| 15.000 +| 16.000 +| 11.00 ± 25.06 +|=== +", + cfg_test_table_header("s") + ); + + assert_eq!(expect, actual); +} + +/// This test demonstrates that the given unit (ms) is used to set +/// the units for all entries. +#[test] +fn test_asciidoc_format_ms() { + use std::collections::BTreeMap; + let exporter = AsciidocExporter::default(); + + let results = vec![ + BenchmarkResult { + command: String::from("FOO=1 BAR=7 command | 2"), + mean: 0.011, + stddev: Some(0.012), + median: 0.011, + user: 0.013, + system: 0.014, + min: 0.015, + max: 0.016, + times: Some(vec![0.017, 0.018, 0.019]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: { + let mut params = BTreeMap::new(); + params.insert("foo".into(), "1".into()); + params.insert("bar".into(), "7".into()); + params + }, + }, + BenchmarkResult { + command: String::from("FOO=1 BAR=2 command | 1"), + mean: 1.0, + stddev: Some(2.0), + median: 1.0, + user: 3.0, + system: 4.0, + min: 5.0, + max: 6.0, + times: Some(vec![7.0, 8.0, 9.0]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: { + let mut params = BTreeMap::new(); + params.insert("foo".into(), "1".into()); + params.insert("bar".into(), "2".into()); + params + }, + }, + ]; + + let actual = String::from_utf8( + exporter + .serialize(&results, Some(Unit::MilliSecond)) + .unwrap(), + ) + .unwrap(); + let expect = format!( + "{} +| `FOO=1 BAR=7 command \\| 2` +| 11.0 ± 12.0 +| 15.0 +| 16.0 +| 1.00 + +| `FOO=1 BAR=2 command \\| 1` +| 1000.0 ± 2000.0 +| 5000.0 +| 6000.0 +| 90.91 ± 207.11 +|=== +", + cfg_test_table_header("ms") + ); + + assert_eq!(expect, actual); } diff --git a/src/export/markup.rs b/src/export/markup.rs index 00cbf15..2b7c153 100644 --- a/src/export/markup.rs +++ b/src/export/markup.rs @@ -15,24 +15,30 @@ pub trait MarkupExporter { // prepare table header strings let notation = format!("[{}]", unit.short_name()); - // emit header - let mut table = self.table_row(&[ - "Command", - &format!("Mean {}", notation), - &format!("Min {}", notation), - &format!("Max {}", notation), - "Relative", - ]); - - // emit horizontal line - table.push_str(&self.table_divider(&[ + // prepare table cells alignment + let cells_alignment = [ Alignment::Left, Alignment::Right, Alignment::Right, Alignment::Right, Alignment::Right, + ]; + + // emit table header format + let mut table = self.table_header(&cells_alignment); + + // emit table header data + table.push_str(&self.table_row(&[ + "Command", + &format!("Mean {}", notation), + &format!("Min {}", notation), + &format!("Max {}", notation), + "Relative", ])); + // emit horizontal line + table.push_str(&self.table_divider(&cells_alignment)); + for entry in entries { let measurement = &entry.result; // prepare data row strings @@ -64,6 +70,9 @@ pub trait MarkupExporter { ])) } + // emit table footer format + table.push_str(&self.table_footer(&cells_alignment)); + table } @@ -71,6 +80,14 @@ pub trait MarkupExporter { fn table_divider(&self, cell_aligmnents: &[Alignment]) -> String; + fn table_header(&self, _cell_aligmnents: &[Alignment]) -> String { + "".to_string() + } + + fn table_footer(&self, _cell_aligmnents: &[Alignment]) -> String { + "".to_string() + } + fn command(&self, size: &str) -> String; }