mod.rs
  1  use std::fs::{File, OpenOptions};
  2  use std::io::Write;
  3  
  4  mod asciidoc;
  5  mod csv;
  6  mod json;
  7  mod markdown;
  8  mod markup;
  9  mod orgmode;
 10  #[cfg(test)]
 11  mod tests;
 12  
 13  use self::asciidoc::AsciidocExporter;
 14  use self::csv::CsvExporter;
 15  use self::json::JsonExporter;
 16  use self::markdown::MarkdownExporter;
 17  use self::orgmode::OrgmodeExporter;
 18  
 19  use crate::benchmark::benchmark_result::BenchmarkResult;
 20  use crate::options::SortOrder;
 21  use crate::util::units::Unit;
 22  
 23  use anyhow::{Context, Result};
 24  use clap::ArgMatches;
 25  
 26  /// The desired form of exporter to use for a given file.
 27  #[derive(Clone)]
 28  pub enum ExportType {
 29      /// Asciidoc Table
 30      Asciidoc,
 31  
 32      /// CSV (comma separated values) format
 33      Csv,
 34  
 35      /// JSON format
 36      Json,
 37  
 38      /// Markdown table
 39      Markdown,
 40  
 41      /// Emacs org-mode tables
 42      Orgmode,
 43  }
 44  
 45  /// Interface for different exporters.
 46  trait Exporter {
 47      /// Export the given entries in the serialized form.
 48      fn serialize(
 49          &self,
 50          results: &[BenchmarkResult],
 51          unit: Option<Unit>,
 52          sort_order: SortOrder,
 53      ) -> Result<Vec<u8>>;
 54  }
 55  
 56  pub enum ExportTarget {
 57      File(String),
 58      Stdout,
 59  }
 60  
 61  struct ExporterWithTarget {
 62      exporter: Box<dyn Exporter>,
 63      target: ExportTarget,
 64  }
 65  
 66  /// Handles the management of multiple file exporters.
 67  pub struct ExportManager {
 68      exporters: Vec<ExporterWithTarget>,
 69      time_unit: Option<Unit>,
 70      sort_order: SortOrder,
 71  }
 72  
 73  impl ExportManager {
 74      /// Build the ExportManager that will export the results specified
 75      /// in the given ArgMatches
 76      pub fn from_cli_arguments(
 77          matches: &ArgMatches,
 78          time_unit: Option<Unit>,
 79          sort_order: SortOrder,
 80      ) -> Result<Self> {
 81          let mut export_manager = Self {
 82              exporters: vec![],
 83              time_unit,
 84              sort_order,
 85          };
 86          {
 87              let mut add_exporter = |flag, exporttype| -> Result<()> {
 88                  if let Some(filename) = matches.get_one::<String>(flag) {
 89                      export_manager.add_exporter(exporttype, filename)?;
 90                  }
 91                  Ok(())
 92              };
 93              add_exporter("export-asciidoc", ExportType::Asciidoc)?;
 94              add_exporter("export-json", ExportType::Json)?;
 95              add_exporter("export-csv", ExportType::Csv)?;
 96              add_exporter("export-markdown", ExportType::Markdown)?;
 97              add_exporter("export-orgmode", ExportType::Orgmode)?;
 98          }
 99          Ok(export_manager)
100      }
101  
102      /// Add an additional exporter to the ExportManager
103      pub fn add_exporter(&mut self, export_type: ExportType, filename: &str) -> Result<()> {
104          let exporter: Box<dyn Exporter> = match export_type {
105              ExportType::Asciidoc => Box::<AsciidocExporter>::default(),
106              ExportType::Csv => Box::<CsvExporter>::default(),
107              ExportType::Json => Box::<JsonExporter>::default(),
108              ExportType::Markdown => Box::<MarkdownExporter>::default(),
109              ExportType::Orgmode => Box::<OrgmodeExporter>::default(),
110          };
111  
112          self.exporters.push(ExporterWithTarget {
113              exporter,
114              target: if filename == "-" {
115                  ExportTarget::Stdout
116              } else {
117                  let _ = File::create(filename)
118                      .with_context(|| format!("Could not create export file '{filename}'"))?;
119                  ExportTarget::File(filename.to_string())
120              },
121          });
122  
123          Ok(())
124      }
125  
126      /// Write the given results to all Exporters. The 'intermediate' flag specifies
127      /// whether this is being called while still performing benchmarks, or if this
128      /// is the final call after all benchmarks have been finished. In the former case,
129      /// results are written to all file targets (to always have them up to date, even
130      /// if a benchmark fails). In the latter case, we only print to stdout targets (in
131      /// order not to clutter the output of hyperfine with intermediate results).
132      pub fn write_results(&self, results: &[BenchmarkResult], intermediate: bool) -> Result<()> {
133          for e in &self.exporters {
134              let content = || {
135                  e.exporter
136                      .serialize(results, self.time_unit, self.sort_order)
137              };
138  
139              match e.target {
140                  ExportTarget::File(ref filename) => {
141                      if intermediate {
142                          write_to_file(filename, &content()?)?
143                      }
144                  }
145                  ExportTarget::Stdout => {
146                      if !intermediate {
147                          println!();
148                          println!("{}", String::from_utf8(content()?).unwrap());
149                      }
150                  }
151              }
152          }
153          Ok(())
154      }
155  }
156  
157  /// Write the given content to a file with the specified name
158  fn write_to_file(filename: &str, content: &[u8]) -> Result<()> {
159      let mut file = OpenOptions::new().write(true).open(filename)?;
160      file.write_all(content)
161          .with_context(|| format!("Failed to export results to '{filename}'"))
162  }