diff --git a/src/hyperfine/export/markdown.rs b/src/hyperfine/export/markdown.rs index 5fc3f3c..8ef449d 100644 --- a/src/hyperfine/export/markdown.rs +++ b/src/hyperfine/export/markdown.rs @@ -1,40 +1,157 @@ use super::Exporter; + +use hyperfine::format::{Unit, format_duration_value}; use hyperfine::types::BenchmarkResult; use std::io::Result; -const MULTIPLIER: f64 = 1e3; +macro_rules! TABLE_HEADER { + ($unit:expr) => { + format!("| Command | Mean [{unit}] | Min…Max [{unit}] |\n|:---|---:|---:|\n", unit=$unit) }; +} #[derive(Default)] pub struct MarkdownExporter {} impl Exporter for MarkdownExporter { fn serialize(&self, results: &Vec) -> Result> { - let mut destination = start_table(); + // Default to `Second`. + let mut unit = Unit::Second; + + if !results.is_empty() { + // Use the first BenchmarkResult entry to determine the unit for all entries. + unit = format_duration_value(results[0].mean, None).1; + } + + let mut destination = start_table(unit); for result in results { - add_table_row(&mut destination, result); + add_table_row(&mut destination, result, unit); } Ok(destination) } } -fn start_table() -> Vec { - "| Command | Mean [ms] | Min…Max [ms] |\n|:---|---:|---:|\n" - .bytes() - .collect() +fn start_table(unit: Unit) -> Vec { + TABLE_HEADER!(unit.short_name()).bytes().collect() } -fn add_table_row(dest: &mut Vec, entry: &BenchmarkResult) { + +fn add_table_row(dest: &mut Vec, entry: &BenchmarkResult, unit: Unit) { + let mean_str = format_duration_value(entry.mean, Some(unit)).0; + let stddev_str = format_duration_value(entry.stddev, Some(unit)).0; + let min_str = format_duration_value(entry.min, Some(unit)).0; + let max_str = format_duration_value(entry.max, Some(unit)).0; + dest.extend( format!( - "| `{}` | {:.1} ± {:.1} | {:.1}…{:.1} |\n", - entry.command.replace("|", "\\|"), - entry.mean * MULTIPLIER, - entry.stddev * MULTIPLIER, - entry.min * MULTIPLIER, - entry.max * MULTIPLIER - ) - .as_bytes(), + "| `{command}` | {mean} ± {stddev} | {min}…{max} |\n", + command=entry.command.replace("|", "\\|"), + mean=mean_str, + stddev=stddev_str, + min=min_str, + max=max_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. +/// +/// This also demonstrates that the first entry's units (ms) are used to set +/// the units for all entries. +#[test] +fn test_markdown_format_ms() { + let exporter = MarkdownExporter::default(); + + let mut timing_results = vec![]; + + timing_results.push(BenchmarkResult::new( + String::from("sleep 0.1"), + 0.1057, // mean + 0.0016, // std dev + 0.0009, // user_mean + 0.0011, // system_mean + 0.1023, // min + 0.1080, // max + vec![0.1, 0.1, 0.1], // times + )); + + timing_results.push(BenchmarkResult::new( + String::from("sleep 2"), + 2.0050, // mean + 0.0020, // std dev + 0.0009, // user_mean + 0.0012, // system_mean + 2.0020, // min + 2.0080, // max + vec![2.0, 2.0, 2.0], // times + )); + + let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap(); + + let formatted_expected = format!( +"{}\ +| `sleep 0.1` | 105.7 ± 1.6 | 102.3…108.0 | +| `sleep 2` | 2005.0 ± 2.0 | 2002.0…2008.0 | +", TABLE_HEADER!("ms")); + + assert_eq!(formatted_expected, formatted); +} + +/// This (again) demonstrates that the first entry's units (s) are used to set +/// the units for all entries. +#[test] +fn test_markdown_format_s() { + let exporter = MarkdownExporter::default(); + + let mut timing_results = vec![]; + + timing_results.push(BenchmarkResult::new( + String::from("sleep 2"), + 2.0050, // mean + 0.0020, // std dev + 0.0009, // user_mean + 0.0012, // system_mean + 2.0020, // min + 2.0080, // max + vec![2.0, 2.0, 2.0], // times + )); + + timing_results.push(BenchmarkResult::new( + String::from("sleep 0.1"), + 0.1057, // mean + 0.0016, // std dev + 0.0009, // user_mean + 0.0011, // system_mean + 0.1023, // min + 0.1080, // max + vec![0.1, 0.1, 0.1], // times + )); + + let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap(); + + let formatted_expected = format!( +"{}\ +| `sleep 2` | 2.005 ± 0.002 | 2.002…2.008 | +| `sleep 0.1` | 0.106 ± 0.002 | 0.102…0.108 | +", TABLE_HEADER!("s")); + + assert_eq!(formatted_expected, formatted); +} + +/// An empty list of benchmark results will only include the table header +/// in the markdown output, using the default `Seconds` unit. +#[test] +fn test_markdown_format_empty_results() { + let exporter = MarkdownExporter::default(); + + let timing_results = vec![]; + + let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap(); + + let formatted_expected = TABLE_HEADER!("s"); + + assert_eq!(formatted_expected, formatted); +} diff --git a/src/hyperfine/format.rs b/src/hyperfine/format.rs index 311cf67..cccfe97 100644 --- a/src/hyperfine/format.rs +++ b/src/hyperfine/format.rs @@ -6,6 +6,34 @@ pub enum Unit { MilliSecond, } +impl Unit { + /// The abbreviation of the Unit. + pub fn short_name(&self) -> String { + match *self { + Unit::Second => String::from("s"), + Unit::MilliSecond => String::from("ms"), + } + } + + /// The multiplier value to convert from `from_unit` to the Unit. + pub fn multiplier_from(&self, from_unit: Unit) -> f64 { + match (*self, from_unit) { + (Unit::Second, Unit::Second) => 1.0, + (Unit::MilliSecond, Unit::MilliSecond) => 1.0, + (Unit::Second, Unit::MilliSecond) => 1e-3, + (Unit::MilliSecond, Unit::Second) => 1e3, + } + } + + /// Returns the Second value formatted for the Unit. + pub fn format(&self, value: Second) -> String { + match *self { + Unit::Second => format!("{:.3}", value * self.multiplier_from(Unit::Second)), + Unit::MilliSecond => format!("{:.1}", value * self.multiplier_from(Unit::Second)), + } + } +} + /// Format the given duration as a string. The output-unit can be enforced by setting `unit` to /// `Some(target_unit)`. If `unit` is `None`, it will be determined automatically. pub fn format_duration(duration: Second, unit: Option) -> String { @@ -16,11 +44,54 @@ pub fn format_duration(duration: Second, unit: Option) -> String { /// Like `format_duration`, but returns the target unit as well. pub fn format_duration_unit(duration: Second, unit: Option) -> (String, Unit) { - if (duration < 1.0 && unit.is_none()) || unit == Some(Unit::MilliSecond) { - (format!("{:.1} ms", duration * 1e3), Unit::MilliSecond) - } else { - (format!("{:.3} s", duration), Unit::Second) + let (out_str, out_unit) = format_duration_value(duration, unit); + + (format!("{} {}", out_str, out_unit.short_name()), out_unit) +} + +/// Like `format_duration`, but returns the target unit as well. +pub fn format_duration_value(duration: Second, unit: Option) -> (String, Unit) { + + // Default to `Second` until proven otherwise. + let mut duration_unit = Unit::Second; + + match unit { + Some(unit_option) => { + // Use user-supplied unit. + duration_unit = unit_option; + }, + None => { + if duration < 1.0 { + // It's a small value, use `Millisecond` instead. + duration_unit = Unit::MilliSecond; + } + }, } + + (duration_unit.format(duration), duration_unit) +} + +#[test] +fn test_unit_short_name() { + assert_eq!("s", Unit::Second.short_name()); + assert_eq!("ms", Unit::MilliSecond.short_name()); +} + +#[test] +fn test_unit_multiplier_from() { + assert_eq!(1.0, Unit::Second.multiplier_from(Unit::Second)); + assert_eq!(1.0, Unit::MilliSecond.multiplier_from(Unit::MilliSecond)); + + assert_eq!(0.001, Unit::Second.multiplier_from(Unit::MilliSecond)); + assert_eq!(1000.0, Unit::MilliSecond.multiplier_from(Unit::Second)); +} + +// Note - the values are rounded when formatted. +#[test] +fn test_unit_format() { + let value: Second = 123.456789; + assert_eq!("123.457", Unit::Second.format(value)); + assert_eq!("123456.8", Unit::MilliSecond.format(value)); } #[test]