diff --git a/src/cli.rs b/src/cli.rs index 14ff884..d5af45e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -241,6 +241,13 @@ fn build_command() -> Command<'static> { .value_name("FILE") .help("Export the timing summary statistics as a Markdown table to the given FILE."), ) + .arg( + Arg::new("export-orgmode") + .long("export-orgmode") + .takes_value(true) + .value_name("FILE") + .help("Export the timing summary statistics as a Emacs org-mode table to the given FILE."), + ) .arg( Arg::new("show-output") .long("show-output") diff --git a/src/export/mod.rs b/src/export/mod.rs index bdd5d3c..62947d2 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -6,11 +6,13 @@ mod csv; mod json; mod markdown; mod markup; +mod orgmode; use self::asciidoc::AsciidocExporter; use self::csv::CsvExporter; use self::json::JsonExporter; use self::markdown::MarkdownExporter; +use self::orgmode::OrgmodeExporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::util::units::Unit; @@ -32,6 +34,9 @@ pub enum ExportType { /// Markdown table Markdown, + + /// Emacs org-mode tables + Orgmode, } /// Interface for different exporters. @@ -67,6 +72,7 @@ impl ExportManager { add_exporter("export-json", ExportType::Json)?; add_exporter("export-csv", ExportType::Csv)?; add_exporter("export-markdown", ExportType::Markdown)?; + add_exporter("export-orgmode", ExportType::Orgmode)?; } Ok(export_manager) } @@ -81,6 +87,7 @@ impl ExportManager { ExportType::Csv => Box::new(CsvExporter::default()), ExportType::Json => Box::new(JsonExporter::default()), ExportType::Markdown => Box::new(MarkdownExporter::default()), + ExportType::Orgmode => Box::new(OrgmodeExporter::default()), }; self.exporters.push(ExporterWithFilename { exporter, diff --git a/src/export/orgmode.rs b/src/export/orgmode.rs new file mode 100644 index 0000000..ed37b7d --- /dev/null +++ b/src/export/orgmode.rs @@ -0,0 +1,182 @@ +use super::Exporter; +use crate::benchmark::benchmark_result::BenchmarkResult; +use crate::benchmark::relative_speed; +use crate::export::markup::MarkupFormatter; +use crate::util::units::Unit; + +use anyhow::{anyhow, Result}; + +#[derive(Default)] +pub struct OrgmodeFormatter; + +impl MarkupFormatter for OrgmodeFormatter { + fn table_data(&self, data: &[&str]) -> String { + format!( + "| {} | {} |\n", + data.first().unwrap(), + &data[1..].join(" | ") + ) + } + + fn table_line(&self, size: usize) -> String { + format!("|{}--|\n", "--+".repeat(size - 1)) + } + + fn command(&self, cmd: &str) -> String { + format!("={}=", cmd) + } +} + +#[derive(Default)] +pub struct OrgmodeExporter {} + +impl Exporter for OrgmodeExporter { + fn serialize(&self, results: &[BenchmarkResult], unit: Option) -> Result> { + let unit = self.unit(results, unit); + let entries = relative_speed::compute(results); + if entries.is_none() { + return Err(anyhow!( + "Relative speed comparison is not available for Emacs org-mode export." + )); + } + + let formatter = OrgmodeFormatter::default(); + let table = formatter.table_results(&entries.unwrap(), unit); + Ok(table.as_bytes().to_vec()) + } +} + +/// Check Emacs org-mode data row formatting +#[test] +fn test_orgmode_formatter_table_data() { + let formatter = OrgmodeFormatter::default(); + let data = vec!["a", "b", "c"]; + + let actual = formatter.table_data(&data); + let expect = "| a | b | c |\n"; + + assert_eq!(expect, actual); +} + +/// Check Emacs org-mode horizontal line formatting +#[test] +fn test_orgmode_formatter_table_line() { + let formatter = OrgmodeFormatter::default(); + let size = 5; + + let actual = formatter.table_line(size); + let expect = "|--+--+--+--+--|\n"; + + assert_eq!(expect, actual); +} + +/// Test helper function to create unit-based header and horizontal line +/// independently from the markup functionality for Emacs org-mode. +#[cfg(test)] +fn cfg_test_table_header(unit_short_name: String) -> String { + format!( + "| Command | Mean [{unit}] | Min [{unit}] | Max [{unit}] | Relative |\n|--+--+--+--+--|\n", + unit = unit_short_name + ) +} + +/// Ensure the Emacs org-mode 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 when the time unit is not given. +#[test] +fn test_orgmode_format_ms() { + use std::collections::BTreeMap; + let exporter = OrgmodeExporter::default(); + + 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 actual = String::from_utf8(exporter.serialize(&results, None).unwrap()).unwrap(); + let expect = format!( + "{}\ +| =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 | +", + cfg_test_table_header("ms".to_string()) + ); + + assert_eq!(expect, actual); +} + +/// This test demonstrates that the given unit (s) is used to set +/// the units for all entries. +#[test] +fn test_orgmode_format_s() { + use std::collections::BTreeMap; + let exporter = OrgmodeExporter::default(); + + 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 actual = + String::from_utf8(exporter.serialize(&results, Some(Unit::Second)).unwrap()).unwrap(); + let expect = format!( + "{}\ +| =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 | +", + cfg_test_table_header("s".to_string()) + ); + + assert_eq!(expect, actual); +}