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