diff --git a/src/hyperfine/benchmark.rs b/src/hyperfine/benchmark.rs index f7c5c27..af679c1 100644 --- a/src/hyperfine/benchmark.rs +++ b/src/hyperfine/benchmark.rs @@ -117,7 +117,7 @@ pub fn mean_shell_spawning_time( // Just run the shell without any command let res = time_shell_command( shell, - &Command::new(""), + &Command::new(None, ""), show_output, CmdFailureAction::RaiseError, None, @@ -229,20 +229,13 @@ pub fn run_benchmark( shell_spawning_time: TimingResult, options: &HyperfineOptions, ) -> io::Result { - let shell_cmd = cmd.get_shell_command(); - let command_name = if let Some(names) = &options.names { - names.get(num).unwrap_or(&shell_cmd) - } else { - &shell_cmd - }; - let command_name = command_name.to_string(); - + let command_name = cmd.get_name(); if options.output_style != OutputStyleOption::Disabled { println!( "{}{}: {}", "Benchmark ".bold(), (num + 1).to_string().bold(), - &command_name + command_name, ); } @@ -259,7 +252,7 @@ pub fn run_benchmark( } else { &values[num] }; - Command::new_parametrized(preparation_command, cmd.get_parameters().clone()) + Command::new_parametrized(None, preparation_command, cmd.get_parameters().clone()) }); // Warmup phase @@ -450,7 +443,7 @@ pub fn run_benchmark( // Run cleanup command let cleanup_cmd = options.cleanup_command.as_ref().map(|cleanup_command| { - Command::new_parametrized(cleanup_command, cmd.get_parameters().clone()) + Command::new_parametrized(None, cleanup_command, cmd.get_parameters().clone()) }); run_cleanup_command(&options.shell, &cleanup_cmd, options.show_output)?; diff --git a/src/hyperfine/parameter_range.rs b/src/hyperfine/parameter_range.rs index 92d15b4..c08e257 100644 --- a/src/hyperfine/parameter_range.rs +++ b/src/hyperfine/parameter_range.rs @@ -83,6 +83,7 @@ fn build_parameterized_commands<'a, T: Numeric>( param_min: T, param_max: T, step: T, + command_names: Vec<&'a str>, command_strings: Vec<&'a str>, param_name: &'a str, ) -> Result>, ParameterScanError> { @@ -90,9 +91,20 @@ fn build_parameterized_commands<'a, T: Numeric>( let param_range = RangeStep::new(param_min, param_max, step); let mut commands = vec![]; + let mut i = 0; + let name_count = command_names.len(); for value in param_range { for cmd in &command_strings { + // Sets the command name by index (remainder) if exists. + let name = if name_count > 0 { + Some(command_names[i % name_count]) + } else { + None + }; + i += 1; + commands.push(Command::new_parametrized( + name, cmd, vec![(param_name, ParameterValue::Numeric(value.into()))], )); @@ -102,10 +114,12 @@ fn build_parameterized_commands<'a, T: Numeric>( } pub fn get_parameterized_commands<'a>( + command_names: Option>, command_strings: Values<'a>, mut vals: clap::Values<'a>, step: Option<&str>, ) -> Result>, ParameterScanError> { + let command_names = command_names.map_or(vec![], |names| names.collect::>()); let command_strings = command_strings.collect::>(); let param_name = vals.next().unwrap(); let param_min = vals.next().unwrap(); @@ -121,6 +135,7 @@ pub fn get_parameterized_commands<'a>( param_min, param_max, step, + command_names, command_strings, param_name, ); @@ -135,7 +150,14 @@ pub fn get_parameterized_commands<'a>( } let step = Decimal::from_str(step.unwrap())?; - build_parameterized_commands(param_min, param_max, step, command_strings, param_name) + build_parameterized_commands( + param_min, + param_max, + step, + command_names, + command_strings, + param_name, + ) } #[test] @@ -163,8 +185,9 @@ fn test_decimal_range() { #[test] fn test_get_parameterized_commands_int() { let commands = - build_parameterized_commands(1i32, 7i32, 3i32, vec!["echo {val}"], "val").unwrap(); + build_parameterized_commands(1i32, 7i32, 3i32, vec![], vec!["echo {val}"], "val").unwrap(); assert_eq!(commands.len(), 3); + assert_eq!(commands[2].get_name(), "echo 7"); assert_eq!(commands[2].get_shell_command(), "echo 7"); } @@ -174,9 +197,54 @@ fn test_get_parameterized_commands_decimal() { let param_max = Decimal::from_str("1").unwrap(); let step = Decimal::from_str("0.33").unwrap(); - let commands = - build_parameterized_commands(param_min, param_max, step, vec!["echo {val}"], "val") - .unwrap(); + let commands = build_parameterized_commands( + param_min, + param_max, + step, + vec![], + vec!["echo {val}"], + "val", + ) + .unwrap(); assert_eq!(commands.len(), 4); + assert_eq!(commands[3].get_name(), "echo 0.99"); assert_eq!(commands[3].get_shell_command(), "echo 0.99"); } + +#[test] +fn test_get_parameterized_command_names() { + let commands = build_parameterized_commands( + 1i32, + 3i32, + 1i32, + vec!["name-{val}"], + vec!["echo {val}"], + "val", + ) + .unwrap(); + assert_eq!(commands.len(), 3); + let command_names = commands + .iter() + .map(|c| c.get_name()) + .collect::>(); + assert_eq!(command_names, vec!["name-1", "name-2", "name-3"]); +} + +#[test] +fn test_get_specified_command_names() { + let commands = build_parameterized_commands( + 1i32, + 3i32, + 1i32, + vec!["name-a", "name-b", "name-c"], + vec!["echo {val}"], + "val", + ) + .unwrap(); + assert_eq!(commands.len(), 3); + let command_names = commands + .iter() + .map(|c| c.get_name()) + .collect::>(); + assert_eq!(command_names, vec!["name-a", "name-b", "name-c"]); +} diff --git a/src/hyperfine/types.rs b/src/hyperfine/types.rs index 850a541..a5e3613 100644 --- a/src/hyperfine/types.rs +++ b/src/hyperfine/types.rs @@ -58,6 +58,9 @@ impl<'a> ToString for ParameterValue { /// A command that should be benchmarked. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Command<'a> { + /// The command name (without parameter substitution) + name: Option<&'a str>, + /// The command that should be executed (without parameter substitution) expression: &'a str, @@ -66,24 +69,42 @@ pub struct Command<'a> { } impl<'a> Command<'a> { - pub fn new(expression: &'a str) -> Command<'a> { + pub fn new(name: Option<&'a str>, expression: &'a str) -> Command<'a> { Command { + name, expression, parameters: Vec::new(), } } pub fn new_parametrized( + name: Option<&'a str>, expression: &'a str, parameters: Vec<(&'a str, ParameterValue)>, ) -> Command<'a> { Command { + name, expression, parameters, } } + pub fn get_name(&self) -> String { + self.name.map_or_else( + || self.get_shell_command(), + |name| self.replace_parameters_in(name), + ) + } + pub fn get_shell_command(&self) -> String { + self.replace_parameters_in(self.expression) + } + + pub fn get_parameters(&self) -> &Vec<(&'a str, ParameterValue)> { + &self.parameters + } + + fn replace_parameters_in(&self, original: &str) -> String { let mut result = String::new(); let mut replacements = BTreeMap::::new(); for (param_name, param_value) in &self.parameters { @@ -92,7 +113,7 @@ impl<'a> Command<'a> { param_value.to_string(), ); } - let mut remaining = self.expression; + let mut remaining = original; // Manually replace consecutive occurrences to avoid double-replacing: e.g., // // hyperfine -L foo 'a,{bar}' -L bar 'baz,quux' 'echo {foo} {bar}' @@ -111,15 +132,12 @@ impl<'a> Command<'a> { } result } - - pub fn get_parameters(&self) -> &Vec<(&'a str, ParameterValue)> { - &self.parameters - } } #[test] fn test_get_shell_command_nonoverlapping() { let cmd = Command::new_parametrized( + None, "echo {foo} {bar}", vec![ ("foo", ParameterValue::Text("{bar} baz".into())), @@ -129,6 +147,19 @@ fn test_get_shell_command_nonoverlapping() { assert_eq!(cmd.get_shell_command(), "echo {bar} baz quux"); } +#[test] +fn test_get_parameterized_command_name() { + let cmd = Command::new_parametrized( + Some("name-{bar}-{foo}"), + "echo {foo} {bar}", + vec![ + ("foo", ParameterValue::Text("baz".into())), + ("bar", ParameterValue::Text("quux".into())), + ], + ); + assert_eq!(cmd.get_name(), "name-quux-baz"); +} + impl<'a> fmt::Display for Command<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.get_shell_command()) diff --git a/src/main.rs b/src/main.rs index beb3ed6..d699f4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,7 +74,7 @@ fn main() { 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()) + Err(ref e) => error(&e.to_string()), }; let res = match options { @@ -139,16 +139,6 @@ fn build_hyperfine_options<'a>( (None, None) => {} }; - options.names = matches - .values_of("command-name") - .map(|values| values.map(String::from).collect::>()); - if let Some(ref names) = options.names { - let command_strings = matches.values_of("command").unwrap(); - if names.len() > command_strings.len() { - return Err(OptionsError::TooManyCommandNames(command_strings.len())); - } - } - options.preparation_command = matches .values_of("prepare") .map(|values| values.map(String::from).collect::>()); @@ -224,15 +214,18 @@ fn build_export_manager(matches: &ArgMatches<'_>) -> io::Result { /// Build the commands to benchmark fn build_commands<'a>(matches: &'a ArgMatches<'_>) -> Vec> { + let command_names = matches.values_of("command-name"); let command_strings = matches.values_of("command").unwrap(); if let Some(args) = matches.values_of("parameter-scan") { let step_size = matches.value_of("parameter-step-size"); - match get_parameterized_commands(command_strings, args, step_size) { + match get_parameterized_commands(command_names, command_strings, args, step_size) { Ok(commands) => commands, Err(e) => error(&e.to_string()), } } else if let Some(args) = matches.values_of("parameter-list") { + let command_names = command_names.map_or(vec![], |names| names.collect::>()); + let args: Vec<_> = args.collect(); let param_names_and_values: Vec<(&str, Vec)> = args .chunks_exact(2) @@ -262,9 +255,19 @@ fn build_commands<'a>(matches: &'a ArgMatches<'_>) -> Vec> { return Vec::new(); } + let mut i = 0; + let name_count = command_names.len(); let mut commands = Vec::with_capacity(param_space_size); let mut index = vec![0usize; dimensions.len()]; 'outer: loop { + // Sets the command name by index (remainder) if exists. + let name = if name_count > 0 { + Some(command_names[i % name_count]) + } else { + None + }; + i += 1; + let (command_index, params_indices) = index.split_first().unwrap(); let parameters = param_names_and_values .iter() @@ -272,6 +275,7 @@ fn build_commands<'a>(matches: &'a ArgMatches<'_>) -> Vec> { .map(|((name, values), i)| (*name, ParameterValue::Text(values[*i].clone()))) .collect(); commands.push(Command::new_parametrized( + name, command_list[*command_index], parameters, )); @@ -290,7 +294,18 @@ fn build_commands<'a>(matches: &'a ArgMatches<'_>) -> Vec> { commands } else { - command_strings.map(Command::new).collect() + 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()); + } + + let command_list = command_strings.collect::>(); + let mut commands = Vec::with_capacity(command_list.len()); + for (i, s) in command_list.iter().enumerate() { + commands.push(Command::new(command_names.get(i).copied(), &s)); + } + commands } } @@ -328,7 +343,7 @@ fn test_build_commands_cross_product() { let cmd = |cmd: usize, foo: &str, bar: &str| { let expression = ["echo {foo} {bar}", "printf '%s\n' {foo} {bar}"][cmd]; let params = vec![("foo", pv(foo)), ("bar", pv(bar))]; - Command::new_parametrized(expression, params) + Command::new_parametrized(None, expression, params) }; let expected = vec![ cmd(0, "a", "z"), @@ -342,3 +357,44 @@ fn test_build_commands_cross_product() { ]; assert_eq!(result, expected); } + +#[test] +fn test_build_parameter_list_commands() { + let matches = get_arg_matches(vec![ + "hyperfine", + "echo {foo}", + "--parameter-list", + "foo", + "1,2", + "--command-name", + "name-{foo}", + ]); + let commands = build_commands(&matches); + assert_eq!(commands.len(), 2); + assert_eq!(commands[0].get_name(), "name-1"); + assert_eq!(commands[1].get_name(), "name-2"); + assert_eq!(commands[0].get_shell_command(), "echo 1"); + assert_eq!(commands[1].get_shell_command(), "echo 2"); +} + +#[test] +fn test_build_parameter_range_commands() { + let matches = get_arg_matches(vec![ + "hyperfine", + "echo {val}", + "--parameter-scan", + "val", + "1", + "2", + "--parameter-step-size", + "1", + "--command-name", + "name-{val}", + ]); + let commands = build_commands(&matches); + assert_eq!(commands.len(), 2); + assert_eq!(commands[0].get_name(), "name-1"); + assert_eq!(commands[1].get_name(), "name-2"); + assert_eq!(commands[0].get_shell_command(), "echo 1"); + assert_eq!(commands[1].get_shell_command(), "echo 2"); +}