diff --git a/src/export/markdown.rs b/src/export/markdown.rs index d89aca3..d1c5438 100644 --- a/src/export/markdown.rs +++ b/src/export/markdown.rs @@ -1,6 +1,10 @@ use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; -use crate::benchmark::relative_speed::{self, BenchmarkResultWithRelativeSpeed}; +use crate::benchmark::relative_speed; +use crate::export::markup::markup_results_unit; +use crate::export::markup::markup_table_data; +use crate::export::markup::markup_table_line; +use crate::export::markup::MarkupType; use crate::output::format::format_duration_value; use crate::util::units::Unit; @@ -11,78 +15,84 @@ pub struct MarkdownExporter {} impl Exporter for MarkdownExporter { 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 - }; + let unit = markup_results_unit(results, unit); - if let Some(annotated_results) = relative_speed::compute(results) { - let mut destination = start_table(unit); - - for result in annotated_results { - add_table_row(&mut destination, &result, unit); - } - - Ok(destination) - } else { - Err(anyhow!( + let entries = relative_speed::compute(results); + if entries.is_none() { + return Err(anyhow!( "Relative speed comparison is not available for Markdown export." - )) + )); } + + // prepare table header strings + let notation = format!("[{}]", unit.short_name()); + let mut data: Vec> = vec![vec![ + format!("Command"), + format!("Mean {}", notation), + format!("Min {}", notation), + format!("Max {}", notation), + format!("Relative"), + ]]; + + for entry in entries.unwrap() { + let measurement = &entry.result; + // prepare data row strings + let cmd_str = measurement.command.replace("|", "\\|"); + let mean_str = format_duration_value(measurement.mean, Some(unit)).0; + let stddev_str = if let Some(stddev) = measurement.stddev { + format!(" ± {}", format_duration_value(stddev, Some(unit)).0) + } else { + "".into() + }; + let min_str = format_duration_value(measurement.min, Some(unit)).0; + let max_str = format_duration_value(measurement.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() + }; + // prepare table row entries + data.push(vec![ + format!("`{}`", cmd_str), + format!("{}{}", mean_str, stddev_str), + format!("{}", min_str), + format!("{}", max_str), + format!("{}{}", rel_str, rel_stddev_str), + ]) + } + + let head: &Vec = data.first().unwrap(); + let tail: &[Vec] = &data[1..]; + let kind = MarkupType::Markdown; + + // emit header + let mut table = markup_table_data(&kind, head); + + // emit horizontal line + table.push_str(&markup_table_line(&kind, head.len())); + + // emit data rows + for row in tail { + table.push_str(&markup_table_data(&kind, row)) + } + + Ok(table.as_bytes().to_vec()) } } -fn table_header(unit_short_name: String) -> String { +/// Test helper function to create unit-based header and horizontal line +/// independently from the markup functionality. +#[cfg(test)] +fn test_table_header(unit_short_name: String) -> String { format!( "| Command | Mean [{unit}] | Min [{unit}] | Max [{unit}] | Relative |\n|:---|---:|---:|---:|---:|\n", unit = unit_short_name ) } -fn start_table(unit: Unit) -> Vec { - table_header(unit.short_name()).bytes().collect() -} - -fn add_table_row(dest: &mut Vec, entry: &BenchmarkResultWithRelativeSpeed, unit: Unit) { - 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() - }; - - dest.extend( - format!( - "| `{command}` | {mean}{stddev} | {min} | {max} | {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, - ) - .as_bytes(), - ); -} - /// Ensure the markdown output includes the table header and the multiple /// benchmark results as a table. The list of actual times is not included /// in the output. @@ -130,7 +140,7 @@ fn test_markdown_format_ms() { | `sleep 0.1` | 105.7 ± 1.6 | 102.3 | 108.0 | 1.00 | | `sleep 2` | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 | ", - table_header("ms".to_string()) + test_table_header("ms".to_string()) ); assert_eq!(formatted_expected, formatted); @@ -179,7 +189,7 @@ fn test_markdown_format_s() { | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 | | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 | ", - table_header("s".to_string()) + test_table_header("s".to_string()) ); assert_eq!(formatted_expected, formatted); @@ -232,7 +242,7 @@ fn test_markdown_format_time_unit_s() { | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 | | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 | ", - table_header("s".to_string()) + test_table_header("s".to_string()) ); assert_eq!(formatted_expected, formatted); @@ -286,7 +296,7 @@ fn test_markdown_format_time_unit_ms() { | `sleep 2` | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 | | `sleep 0.1` | 105.7 ± 1.6 | 102.3 | 108.0 | 1.00 | ", - table_header("ms".to_string()) + test_table_header("ms".to_string()) ); assert_eq!(formatted_expected, formatted); diff --git a/src/export/markup.rs b/src/export/markup.rs new file mode 100644 index 0000000..d8df297 --- /dev/null +++ b/src/export/markup.rs @@ -0,0 +1,230 @@ +use crate::benchmark::benchmark_result::BenchmarkResult; +use crate::output::format::format_duration_value; +use crate::util::units::Unit; + +#[derive(Clone)] +pub enum MarkupType { + /// Markdown table + Markdown, +} + +pub fn markup_table_data(kind: &MarkupType, data: &Vec) -> String { + return match kind { + MarkupType::Markdown => format!("| {} |\n", data.join(" | ")), + }; +} + +pub fn markup_table_line(kind: &MarkupType, size: usize) -> String { + return match kind { + MarkupType::Markdown => format!("|:---|{}\n", "---:|".repeat(size - 1)), + }; +} + +pub fn markup_results_unit(results: &[BenchmarkResult], unit: Option) -> Unit { + return 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 + }; +} + +/// Check Markdown-based data row formatting +#[test] +fn test_markup_table_data_markdown() { + let kind = MarkupType::Markdown; + let data: Vec<_> = vec!["a", "b", "c"].into_iter().map(String::from).collect(); + + let markup_actual = markup_table_data(&kind, &data); + let markup_expected = format!("| a | b | c |\n"); + + assert_eq!(markup_expected, markup_actual); +} + +/// Check Markdown-based horizontal line formatting +#[test] +fn test_markup_table_line_markdown() { + let kind = MarkupType::Markdown; + let size = 5; + + let markup_actual = markup_table_line(&kind, size); + let markup_expected = format!("|:---|---:|---:|---:|---:|\n"); + + assert_eq!(markup_expected, markup_actual); +} + +/// Check unit resolving for timing results and given unit 's' +#[test] +fn test_markup_table_results_unit_given_s() { + use std::collections::BTreeMap; + let results = vec![ + BenchmarkResult { + command: String::from("sleep 2"), + mean: 2.0050, + stddev: Some(0.0020), + median: 2.0050, + user: 0.0009, + system: 0.0012, + min: 2.0020, + max: 2.0080, + times: Some(vec![2.0, 2.0, 2.0]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + BenchmarkResult { + command: String::from("sleep 0.1"), + mean: 0.1057, + stddev: Some(0.0016), + median: 0.1057, + user: 0.0009, + system: 0.0011, + min: 0.1023, + max: 0.1080, + times: Some(vec![0.1, 0.1, 0.1]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + ]; + let unit = Some(Unit::Second); + + let markup_actual = markup_results_unit(&results, unit); + let markup_expected = Unit::Second; + + assert_eq!(markup_expected, markup_actual); +} + +/// Check unit resolving for timing results and given unit 'ms' +#[test] +fn test_markup_table_results_unit_given_ms() { + use std::collections::BTreeMap; + let results = vec![ + BenchmarkResult { + command: String::from("sleep 2"), + mean: 2.0050, + stddev: Some(0.0020), + median: 2.0050, + user: 0.0009, + system: 0.0012, + min: 2.0020, + max: 2.0080, + times: Some(vec![2.0, 2.0, 2.0]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + BenchmarkResult { + command: String::from("sleep 0.1"), + mean: 0.1057, + stddev: Some(0.0016), + median: 0.1057, + user: 0.0009, + system: 0.0011, + min: 0.1023, + max: 0.1080, + times: Some(vec![0.1, 0.1, 0.1]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + ]; + let unit = Some(Unit::MilliSecond); + + let markup_actual = markup_results_unit(&results, unit); + let markup_expected = Unit::MilliSecond; + + assert_eq!(markup_expected, markup_actual); +} + +/// Check unit resolving for timing results using the first result entry as 's' +#[test] +fn test_markup_table_results_unit_first_s() { + use std::collections::BTreeMap; + let results = vec![ + BenchmarkResult { + command: String::from("sleep 2"), + mean: 2.0050, + stddev: Some(0.0020), + median: 2.0050, + user: 0.0009, + system: 0.0012, + min: 2.0020, + max: 2.0080, + times: Some(vec![2.0, 2.0, 2.0]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + BenchmarkResult { + command: String::from("sleep 0.1"), + mean: 0.1057, + stddev: Some(0.0016), + median: 0.1057, + user: 0.0009, + system: 0.0011, + min: 0.1023, + max: 0.1080, + times: Some(vec![0.1, 0.1, 0.1]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + ]; + let unit = None; + + let markup_actual = markup_results_unit(&results, unit); + let markup_expected = Unit::Second; + + assert_eq!(markup_expected, markup_actual); +} + +/// Check unit resolving for timing results using the first result entry as 'ms' +#[test] +fn test_markup_table_results_unit_first_ms() { + use std::collections::BTreeMap; + let results = vec![ + BenchmarkResult { + command: String::from("sleep 0.1"), + mean: 0.1057, + stddev: Some(0.0016), + median: 0.1057, + user: 0.0009, + system: 0.0011, + min: 0.1023, + max: 0.1080, + times: Some(vec![0.1, 0.1, 0.1]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + BenchmarkResult { + command: String::from("sleep 2"), + mean: 2.0050, + stddev: Some(0.0020), + median: 2.0050, + user: 0.0009, + system: 0.0012, + min: 2.0020, + max: 2.0080, + times: Some(vec![2.0, 2.0, 2.0]), + exit_codes: vec![Some(0), Some(0), Some(0)], + parameters: BTreeMap::new(), + }, + ]; + let unit = None; + + let markup_actual = markup_results_unit(&results, unit); + let markup_expected = Unit::MilliSecond; + + assert_eq!(markup_expected, markup_actual); +} + +/// Check unit resolving for not timing results and no given unit defaulting to 's' +#[test] +fn test_markup_table_results_unit_default_s() { + let results: Vec = vec![]; + let unit = None; + + let markup_actual = markup_results_unit(&results, unit); + let markup_expected = Unit::Second; + + assert_eq!(markup_expected, markup_actual); +} diff --git a/src/export/mod.rs b/src/export/mod.rs index aa5f0a2..bdd5d3c 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -5,6 +5,7 @@ mod asciidoc; mod csv; mod json; mod markdown; +mod markup; use self::asciidoc::AsciidocExporter; use self::csv::CsvExporter;