diff --git a/Cargo.lock b/Cargo.lock index 23deba1..d854693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + [[package]] name = "approx" version = "0.5.1" @@ -246,6 +252,7 @@ dependencies = [ name = "hyperfine" version = "1.12.0" dependencies = [ + "anyhow", "approx", "assert_cmd", "atty", diff --git a/Cargo.toml b/Cargo.toml index 528894a..89f9922 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ rust_decimal = "1.19" rand = "0.8" shell-words = "1.0" thiserror = "1.0" +anyhow = "1.0" [target.'cfg(not(windows))'.dependencies] libc = "0.2" diff --git a/src/benchmark.rs b/src/benchmark.rs index f26572f..344c596 100644 --- a/src/benchmark.rs +++ b/src/benchmark.rs @@ -1,5 +1,4 @@ use std::cmp; -use std::io; use std::process::{ExitStatus, Stdio}; use colored::*; @@ -18,6 +17,8 @@ use crate::timer::{TimerStart, TimerStop}; use crate::units::Second; use crate::warnings::Warnings; +use anyhow::{bail, Result}; + /// Threshold for warning about fast execution time pub const MIN_EXECUTION_TIME: Second = 5e-3; @@ -50,7 +51,7 @@ pub fn time_shell_command( show_output: bool, failure_action: CmdFailureAction, shell_spawning_time: Option, -) -> io::Result<(TimingResult, ExitStatus)> { +) -> Result<(TimingResult, ExitStatus)> { let (stdout, stderr) = if show_output { (Stdio::inherit(), Stdio::inherit()) } else { @@ -65,18 +66,14 @@ pub fn time_shell_command( let mut time_system = result.system_time; if failure_action == CmdFailureAction::RaiseError && !result.status.success() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "{}. \ - Use the '-i'/'--ignore-failure' option if you want to ignore this. \ + bail!( + "{}. Use the '-i'/'--ignore-failure' option if you want to ignore this. \ Alternatively, use the '--show-output' option to debug what went wrong.", - result.status.code().map_or( - "The process has been terminated by a signal".into(), - |c| format!("Command terminated with non-zero exit code: {}", c) - ) - ), - )); + result.status.code().map_or( + "The process has been terminated by a signal".into(), + |c| format!("Command terminated with non-zero exit code: {}", c) + ) + ); } // Correct for shell spawning time @@ -101,7 +98,7 @@ pub fn mean_shell_spawning_time( shell: &Shell, style: OutputStyleOption, show_output: bool, -) -> io::Result { +) -> Result { const COUNT: u64 = 50; let progress_bar = if style != OutputStyleOption::Disabled { Some(get_progress_bar( @@ -135,14 +132,10 @@ pub fn mean_shell_spawning_time( format!("{} -c \"\"", shell) }; - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "Could not measure shell execution time. \ - Make sure you can run '{}'.", - shell_cmd - ), - )); + bail!( + "Could not measure shell execution time. Make sure you can run '{}'.", + shell_cmd + ); } Ok((r, _)) => { times_real.push(r.time_real); @@ -172,11 +165,11 @@ fn run_intermediate_command( command: &Option>, show_output: bool, error_output: &'static str, -) -> io::Result { +) -> Result { if let Some(ref cmd) = command { let res = time_shell_command(shell, cmd, show_output, CmdFailureAction::RaiseError, None); if res.is_err() { - return Err(io::Error::new(io::ErrorKind::Other, error_output)); + bail!(error_output); } return res.map(|r| r.0); } @@ -190,7 +183,7 @@ fn run_setup_command( shell: &Shell, command: &Option>, show_output: bool, -) -> io::Result { +) -> Result { let error_output = "The setup command terminated with a non-zero exit code. \ Append ' || true' to the command if you are sure that this can be ignored."; @@ -202,7 +195,7 @@ fn run_preparation_command( shell: &Shell, command: &Option>, show_output: bool, -) -> io::Result { +) -> Result { let error_output = "The preparation command terminated with a non-zero exit code. \ Append ' || true' to the command if you are sure that this can be ignored."; @@ -214,7 +207,7 @@ fn run_cleanup_command( shell: &Shell, command: &Option>, show_output: bool, -) -> io::Result { +) -> Result { let error_output = "The cleanup command terminated with a non-zero exit code. \ Append ' || true' to the command if you are sure that this can be ignored."; @@ -248,7 +241,7 @@ pub fn run_benchmark( cmd: &Command<'_>, shell_spawning_time: TimingResult, options: &HyperfineOptions, -) -> io::Result { +) -> Result { let command_name = cmd.get_name(); if options.output_style != OutputStyleOption::Disabled { println!( diff --git a/src/export/asciidoc.rs b/src/export/asciidoc.rs index 088a615..b568b63 100644 --- a/src/export/asciidoc.rs +++ b/src/export/asciidoc.rs @@ -1,5 +1,3 @@ -use std::io::{Error, ErrorKind, Result}; - use crate::benchmark_result::BenchmarkResult; use crate::format::format_duration_value; use crate::relative_speed::{self, BenchmarkResultWithRelativeSpeed}; @@ -7,6 +5,8 @@ use crate::units::Unit; use super::Exporter; +use anyhow::{anyhow, Result}; + #[derive(Default)] pub struct AsciidocExporter {} @@ -36,9 +36,8 @@ impl Exporter for AsciidocExporter { Ok(res) } else { - Err(Error::new( - ErrorKind::Other, - "Relative speed comparison is not available for Asciidoctor export.", + Err(anyhow!( + "Relative speed comparison is not available for Asciidoctor export." )) } } diff --git a/src/export/csv.rs b/src/export/csv.rs index e1f2581..cb3e4ae 100644 --- a/src/export/csv.rs +++ b/src/export/csv.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::io::{Error, ErrorKind, Result}; use csv::WriterBuilder; @@ -7,6 +6,8 @@ use super::Exporter; use crate::benchmark_result::BenchmarkResult; use crate::units::Unit; +use anyhow::Result; + #[derive(Default)] pub struct CsvExporter {} @@ -49,9 +50,7 @@ impl Exporter for CsvExporter { writer.write_record(fields)?; } - writer - .into_inner() - .map_err(|e| Error::new(ErrorKind::Other, e)) + Ok(writer.into_inner()?) } } diff --git a/src/export/json.rs b/src/export/json.rs index aea96ea..ba932e8 100644 --- a/src/export/json.rs +++ b/src/export/json.rs @@ -1,5 +1,3 @@ -use std::io::{Error, ErrorKind, Result}; - use serde::*; use serde_json::to_vec_pretty; @@ -7,6 +5,8 @@ use super::Exporter; use crate::benchmark_result::BenchmarkResult; use crate::units::Unit; +use anyhow::Result; + #[derive(Serialize, Debug)] struct HyperfineSummary<'a> { results: &'a [BenchmarkResult], @@ -22,6 +22,6 @@ impl Exporter for JsonExporter { content.push(b'\n'); } - output.map_err(|e| Error::new(ErrorKind::Other, e)) + Ok(output?) } } diff --git a/src/export/markdown.rs b/src/export/markdown.rs index 7bf11a5..1b14f70 100644 --- a/src/export/markdown.rs +++ b/src/export/markdown.rs @@ -1,11 +1,11 @@ -use std::io::{Error, ErrorKind, Result}; - use super::Exporter; use crate::benchmark_result::BenchmarkResult; use crate::format::format_duration_value; use crate::relative_speed::{self, BenchmarkResultWithRelativeSpeed}; use crate::units::Unit; +use anyhow::{anyhow, Result}; + #[derive(Default)] pub struct MarkdownExporter {} @@ -31,9 +31,8 @@ impl Exporter for MarkdownExporter { Ok(destination) } else { - Err(Error::new( - ErrorKind::Other, - "Relative speed comparison is not available for Markdown export.", + Err(anyhow!( + "Relative speed comparison is not available for Markdown export." )) } } diff --git a/src/export/mod.rs b/src/export/mod.rs index 1356e9c..28c9af2 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -1,5 +1,5 @@ use std::fs::{File, OpenOptions}; -use std::io::{Result, Write}; +use std::io::Write; mod asciidoc; mod csv; @@ -14,6 +14,8 @@ use self::markdown::MarkdownExporter; use crate::benchmark_result::BenchmarkResult; use crate::units::Unit; +use anyhow::{Context, Result}; + /// The desired form of exporter to use for a given file. #[derive(Clone)] pub enum ExportType { @@ -50,7 +52,8 @@ pub struct ExportManager { impl ExportManager { /// Add an additional exporter to the ExportManager pub fn add_exporter(&mut self, export_type: ExportType, filename: &str) -> Result<()> { - let _ = File::create(filename)?; + let _ = File::create(filename) + .with_context(|| format!("Could not create export file '{filename}'"))?; let exporter: Box = match export_type { ExportType::Asciidoc => Box::new(AsciidocExporter::default()), @@ -80,4 +83,5 @@ impl ExportManager { fn write_to_file(filename: &str, content: &[u8]) -> Result<()> { let mut file = OpenOptions::new().write(true).open(filename)?; file.write_all(content) + .with_context(|| format!("Failed to export results to '{}'", filename)) } diff --git a/src/main.rs b/src/main.rs index d91f60c..5ed7ee7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use std::cmp; use std::collections::BTreeMap; use std::env; -use std::io; use atty::Stream; use clap::ArgMatches; @@ -39,11 +38,7 @@ use tokenize::tokenize; use types::ParameterValue; use units::Unit; -/// Print error message to stderr and terminate -pub fn error(message: &str) -> ! { - eprintln!("{} {}", "Error:".red(), message); - std::process::exit(1); -} +use anyhow::{bail, Result}; pub fn write_benchmark_comparison(results: &[BenchmarkResult]) { if results.len() < 2 { @@ -83,12 +78,11 @@ pub fn write_benchmark_comparison(results: &[BenchmarkResult]) { } } -/// Runs the benchmark for the given commands -fn run( +fn run_benchmarks_and_print_comparison( commands: &[Command<'_>], options: &HyperfineOptions, export_manager: &ExportManager, -) -> io::Result<()> { +) -> Result<()> { let shell_spawning_time = mean_shell_spawning_time(&options.shell, options.output_style, options.show_output)?; @@ -96,9 +90,9 @@ fn run( if let Some(preparation_command) = &options.preparation_command { if preparation_command.len() > 1 && commands.len() != preparation_command.len() { - error( + bail!( "The '--prepare' option has to be provided just once or N times, where N is the \ - number of benchmark commands.", + number of benchmark commands." ); } } @@ -108,13 +102,7 @@ fn run( timing_results.push(run_benchmark(num, cmd, shell_spawning_time, options)?); // Export (intermediate) results - let ans = export_manager.write_results(&timing_results, options.time_unit); - if let Err(e) = ans { - error(&format!( - "The following error occurred while exporting: {}", - e - )); - } + export_manager.write_results(&timing_results, options.time_unit)?; } // Print relative speed comparison @@ -125,23 +113,22 @@ fn run( Ok(()) } -fn main() { +fn run() -> Result<()> { let matches = get_arg_matches(env::args_os()); - let options = build_hyperfine_options(&matches); - let commands = build_commands(&matches); - let export_manager = match build_export_manager(&matches) { - Ok(export_manager) => export_manager, - Err(ref e) => error(&e.to_string()), - }; + let options = build_hyperfine_options(&matches)?; + let commands = build_commands(&matches)?; + let export_manager = build_export_manager(&matches)?; - let res = match options { - Ok(ref opts) => run(&commands, opts, &export_manager), - Err(ref e) => error(&e.to_string()), - }; + run_benchmarks_and_print_comparison(&commands, &options, &export_manager) +} - match res { +fn main() { + match run() { Ok(_) => {} - Err(e) => error(&e.to_string()), + Err(e) => { + eprintln!("{} {:#}", "Error:".red(), e); + std::process::exit(1); + } } } @@ -243,10 +230,10 @@ fn build_hyperfine_options<'a>(matches: &ArgMatches) -> Result io::Result { +fn build_export_manager(matches: &ArgMatches) -> Result { let mut export_manager = ExportManager::default(); { - let mut add_exporter = |flag, exporttype| -> io::Result<()> { + let mut add_exporter = |flag, exporttype| -> Result<()> { if let Some(filename) = matches.value_of(flag) { export_manager.add_exporter(exporttype, filename)?; } @@ -267,10 +254,12 @@ fn build_commands(matches: &ArgMatches) -> Result> { if let Some(args) = matches.values_of("parameter-scan") { let step_size = matches.value_of("parameter-step-size"); - match get_parameterized_commands(command_names, command_strings, args, step_size) { - Ok(commands) => commands, - Err(e) => error(&e.to_string()), - } + Ok(get_parameterized_commands( + command_names, + command_strings, + args, + step_size, + )?) } else if let Some(args) = matches.values_of("parameter-list") { let command_names = command_names.map_or(vec![], |names| names.collect::>()); @@ -286,7 +275,7 @@ fn build_commands(matches: &ArgMatches) -> Result> { { let dupes = find_dupes(param_names_and_values.iter().map(|(name, _)| *name)); if !dupes.is_empty() { - error(&format!("duplicate parameter names: {}", &dupes.join(", "))) + bail!("Duplicate parameter names: {}", &dupes.join(", ")); } } let command_list = command_strings.collect::>(); @@ -300,16 +289,18 @@ fn build_commands(matches: &ArgMatches) -> Result> { .collect(); let param_space_size = dimensions.iter().product(); if param_space_size == 0 { - return Vec::new(); + return Ok(Vec::new()); } // `--command-name` should appear exactly once or exactly B times, // where B is the total number of benchmarks. let command_name_count = command_names.len(); if command_name_count > 1 && command_name_count != param_space_size { - let err = - OptionsError::UnexpectedCommandNameCount(command_name_count, param_space_size); - error(&err.to_string()); + return Err(OptionsError::UnexpectedCommandNameCount( + command_name_count, + param_space_size, + ) + .into()); } let mut i = 0; @@ -346,12 +337,11 @@ fn build_commands(matches: &ArgMatches) -> Result> { break 'outer; } - commands + Ok(commands) } else { let command_names = command_names.map_or(vec![], |names| names.collect::>()); if command_names.len() > command_strings.len() { - let err = OptionsError::TooManyCommandNames(command_strings.len()); - error(&err.to_string()); + return Err(OptionsError::TooManyCommandNames(command_strings.len()).into()); } let command_list = command_strings.collect::>(); @@ -359,7 +349,7 @@ fn build_commands(matches: &ArgMatches) -> Result> { for (i, s) in command_list.iter().enumerate() { commands.push(Command::new(command_names.get(i).copied(), s)); } - commands + Ok(commands) } } @@ -389,7 +379,7 @@ fn test_build_commands_cross_product() { "echo {par1} {par2}", "printf '%s\n' {par1} {par2}", ]); - let result = build_commands(&matches); + let result = build_commands(&matches).unwrap(); // Iteration order: command list first, then parameters in listed order (here, "par1" before // "par2", which is distinct from their sorted order), with parameter values in listed order. @@ -423,7 +413,7 @@ fn test_build_parameter_list_commands() { "--command-name", "name-{foo}", ]); - let commands = build_commands(&matches); + let commands = build_commands(&matches).unwrap(); assert_eq!(commands.len(), 2); assert_eq!(commands[0].get_name(), "name-1"); assert_eq!(commands[1].get_name(), "name-2"); @@ -445,7 +435,7 @@ fn test_build_parameter_range_commands() { "--command-name", "name-{val}", ]); - let commands = build_commands(&matches); + let commands = build_commands(&matches).unwrap(); assert_eq!(commands.len(), 2); assert_eq!(commands[0].get_name(), "name-1"); assert_eq!(commands[1].get_name(), "name-2"); diff --git a/src/shell.rs b/src/shell.rs index 22f34f8..b80718c 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,9 +1,10 @@ -use std::io; use std::process::{ExitStatus, Stdio}; use crate::options::Shell; use crate::timer::get_cpu_timer; +use anyhow::{Context, Result}; + /// Used to indicate the result of running a command #[derive(Debug, Copy, Clone)] pub struct ExecuteResult { @@ -24,7 +25,7 @@ pub fn execute_and_time( stderr: Stdio, command: &str, shell: &Shell, -) -> io::Result { +) -> Result { let mut child = run_shell_command(stdout, stderr, command, shell)?; let cpu_timer = get_cpu_timer(&child); let status = child.wait()?; @@ -44,7 +45,7 @@ pub fn execute_and_time( stderr: Stdio, command: &str, shell: &Shell, -) -> io::Result { +) -> Result { let cpu_timer = get_cpu_timer(); let status = run_shell_command(stdout, stderr, command, shell)?; @@ -65,7 +66,7 @@ fn run_shell_command( stderr: Stdio, command: &str, shell: &Shell, -) -> io::Result { +) -> Result { shell .command() .arg("-c") @@ -78,6 +79,7 @@ fn run_shell_command( .stdout(stdout) .stderr(stderr) .status() + .with_context(|| format!("Failed to run command '{}'", command)) } /// Run a Windows shell command using `cmd.exe /C` @@ -87,7 +89,7 @@ fn run_shell_command( stderr: Stdio, command: &str, shell: &Shell, -) -> io::Result { +) -> Result { shell .command() .arg("/C")