From 6db82cdb2a28caf763d3ba09b7ccefc49300ba44 Mon Sep 17 00:00:00 2001 From: David Peter Date: Sun, 6 Feb 2022 21:15:36 +0100 Subject: [PATCH] Split range into range_step and commands --- src/command.rs | 180 +++++++++++++++++++- src/parameter/mod.rs | 2 +- src/parameter/range.rs | 318 ------------------------------------ src/parameter/range_step.rs | 150 +++++++++++++++++ 4 files changed, 328 insertions(+), 322 deletions(-) delete mode 100644 src/parameter/range.rs create mode 100644 src/parameter/range_step.rs diff --git a/src/command.rs b/src/command.rs index 8b079b5..b64e19c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,8 +1,11 @@ use std::collections::BTreeMap; use std::fmt; +use std::str::FromStr; -use crate::error::OptionsError; -use crate::parameter::range::get_parameterized_commands; +use crate::{ + error::{OptionsError, ParameterScanError}, + parameter::range_step::{Numeric, RangeStep}, +}; use clap::ArgMatches; @@ -10,6 +13,8 @@ use crate::parameter::tokenize::tokenize; use crate::parameter::ParameterValue; use anyhow::{bail, Result}; +use clap::Values; +use rust_decimal::Decimal; /// A command that should be benchmarked. #[derive(Debug, Clone, PartialEq, Eq)] @@ -103,10 +108,96 @@ fn find_duplicates<'a, I: IntoIterator>(i: I) -> Vec<&'a str> { .collect() } +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> { + let param_range = RangeStep::new(param_min, param_max, step)?; + let param_count = param_range.size_hint().1.unwrap(); + let command_name_count = command_names.len(); + + // `--command-name` should appear exactly once or exactly B times, + // where B is the total number of benchmarks. + if command_name_count > 1 && command_name_count != param_count { + return Err(ParameterScanError::UnexpectedCommandNameCount( + command_name_count, + param_count, + )); + } + + let mut i = 0; + let mut commands = vec![]; + for value in param_range { + for cmd in &command_strings { + let name = command_names + .get(i) + .or_else(|| command_names.get(0)) + .copied(); + commands.push(Command::new_parametrized( + name, + cmd, + vec![(param_name, ParameterValue::Numeric(value.into()))], + )); + i += 1; + } + } + Ok(commands) +} + +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(); + let param_max = vals.next().unwrap(); + + // attempt to parse as integers + if let (Ok(param_min), Ok(param_max), Ok(step)) = ( + param_min.parse::(), + param_max.parse::(), + step.unwrap_or("1").parse::(), + ) { + return build_parameterized_commands( + param_min, + param_max, + step, + command_names, + command_strings, + param_name, + ); + } + + // try parsing them as decimals + let param_min = Decimal::from_str(param_min)?; + let param_max = Decimal::from_str(param_max)?; + + if step.is_none() { + return Err(ParameterScanError::StepRequired); + } + + let step = Decimal::from_str(step.unwrap())?; + build_parameterized_commands( + param_min, + param_max, + step, + command_names, + command_strings, + param_name, + ) +} + pub struct Commands<'a>(Vec>); impl<'a> Commands<'a> { - /// Build the commands to benchmark pub fn from_cli_arguments(matches: &'a ArgMatches) -> Result { let command_names = matches.values_of("command-name"); let command_strings = matches.values_of("command").unwrap(); @@ -335,3 +426,86 @@ fn test_build_parameter_range_commands() { assert_eq!(commands[0].get_shell_command(), "echo 1"); assert_eq!(commands[1].get_shell_command(), "echo 2"); } + +#[test] +fn test_get_parameterized_commands_int() { + let commands = + 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"); +} + +#[test] +fn test_get_parameterized_commands_decimal() { + let param_min = Decimal::from_str("0").unwrap(); + 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![], + 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"]); +} + +#[test] +fn test_different_command_name_count_with_parameters() { + let result = build_parameterized_commands( + 1i32, + 3i32, + 1i32, + vec!["name-1", "name-2"], + vec!["echo {val}"], + "val", + ); + assert_eq!( + format!("{}", result.unwrap_err()), + "'--command-name' has been specified 2 times. It has to appear exactly once, or exactly 3 times (number of benchmarks)" + ); +} diff --git a/src/parameter/mod.rs b/src/parameter/mod.rs index f6fdb8f..7210c90 100644 --- a/src/parameter/mod.rs +++ b/src/parameter/mod.rs @@ -1,6 +1,6 @@ use crate::util::number::Number; -pub mod range; +pub mod range_step; pub mod tokenize; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/parameter/range.rs b/src/parameter/range.rs deleted file mode 100644 index 480ee0c..0000000 --- a/src/parameter/range.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::convert::TryInto; -use std::ops::{Add, AddAssign, Div, Sub}; -use std::str::FromStr; - -use clap::Values; -use rust_decimal::Decimal; - -use super::ParameterValue; -use crate::command::Command; -use crate::error::ParameterScanError; -use crate::util::number::Number; - -trait Numeric: - Add - + Sub - + Div - + AddAssign - + PartialOrd - + Copy - + Clone - + From - + Into -{ -} -impl< - T: Add - + Sub - + Div - + AddAssign - + PartialOrd - + Copy - + Clone - + From - + Into, - > Numeric for T -{ -} - -#[derive(Debug)] -struct RangeStep { - state: T, - end: T, - step: T, -} - -impl RangeStep { - fn new(start: T, end: T, step: T) -> Result { - if end < start { - return Err(ParameterScanError::EmptyRange); - } - - if step == T::from(0) { - return Err(ParameterScanError::ZeroStep); - } - - const MAX_PARAMETERS: usize = 100_000; - match range_step_size_hint(start, end, step) { - (_, Some(size)) if size <= MAX_PARAMETERS => Ok(Self { - state: start, - end, - step, - }), - _ => Err(ParameterScanError::TooLarge), - } - } -} - -impl Iterator for RangeStep { - type Item = T; - - fn next(&mut self) -> Option { - if self.state > self.end { - return None; - } - let return_val = self.state; - self.state += self.step; - - Some(return_val) - } - - fn size_hint(&self) -> (usize, Option) { - range_step_size_hint(self.state, self.end, self.step) - } -} - -fn range_step_size_hint(start: T, end: T, step: T) -> (usize, Option) { - if step == T::from(0) { - return (usize::MAX, None); - } - - let steps = (end - start + T::from(1)) / step; - steps - .into() - .try_into() - .map_or((usize::MAX, None), |u| (u, Some(u))) -} - -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> { - let param_range = RangeStep::new(param_min, param_max, step)?; - let param_count = param_range.size_hint().1.unwrap(); - let command_name_count = command_names.len(); - - // `--command-name` should appear exactly once or exactly B times, - // where B is the total number of benchmarks. - if command_name_count > 1 && command_name_count != param_count { - return Err(ParameterScanError::UnexpectedCommandNameCount( - command_name_count, - param_count, - )); - } - - let mut i = 0; - let mut commands = vec![]; - for value in param_range { - for cmd in &command_strings { - let name = command_names - .get(i) - .or_else(|| command_names.get(0)) - .copied(); - commands.push(Command::new_parametrized( - name, - cmd, - vec![(param_name, ParameterValue::Numeric(value.into()))], - )); - i += 1; - } - } - Ok(commands) -} - -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(); - let param_max = vals.next().unwrap(); - - // attempt to parse as integers - if let (Ok(param_min), Ok(param_max), Ok(step)) = ( - param_min.parse::(), - param_max.parse::(), - step.unwrap_or("1").parse::(), - ) { - return build_parameterized_commands( - param_min, - param_max, - step, - command_names, - command_strings, - param_name, - ); - } - - // try parsing them as decimals - let param_min = Decimal::from_str(param_min)?; - let param_max = Decimal::from_str(param_max)?; - - if step.is_none() { - return Err(ParameterScanError::StepRequired); - } - - let step = Decimal::from_str(step.unwrap())?; - build_parameterized_commands( - param_min, - param_max, - step, - command_names, - command_strings, - param_name, - ) -} - -#[test] -fn test_integer_range() { - let param_range: Vec = RangeStep::new(0, 10, 3).unwrap().collect(); - - assert_eq!(param_range.len(), 4); - assert_eq!(param_range[0], 0); - assert_eq!(param_range[3], 9); -} - -#[test] -fn test_decimal_range() { - let param_min = Decimal::from(0); - let param_max = Decimal::from(1); - let step = Decimal::from_str("0.1").unwrap(); - - let param_range: Vec = RangeStep::new(param_min, param_max, step) - .unwrap() - .collect(); - - assert_eq!(param_range.len(), 11); - assert_eq!(param_range[0], Decimal::from(0)); - assert_eq!(param_range[10], Decimal::from(1)); -} - -#[test] -fn test_range_step_validate() { - let result = RangeStep::new(0, 10, 3); - assert!(result.is_ok()); - - let result = RangeStep::new( - Decimal::from(0), - Decimal::from(1), - Decimal::from_str("0.1").unwrap(), - ); - assert!(result.is_ok()); - - let result = RangeStep::new(11, 10, 1); - assert_eq!(format!("{}", result.unwrap_err()), "Empty parameter range"); - - let result = RangeStep::new(0, 10, 0); - assert_eq!( - format!("{}", result.unwrap_err()), - "Zero is not a valid parameter step" - ); - - let result = RangeStep::new(0, 100_001, 1); - assert_eq!( - format!("{}", result.unwrap_err()), - "Parameter range is too large" - ); -} - -#[test] -fn test_get_parameterized_commands_int() { - let commands = - 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"); -} - -#[test] -fn test_get_parameterized_commands_decimal() { - let param_min = Decimal::from_str("0").unwrap(); - 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![], - 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"]); -} - -#[test] -fn test_different_command_name_count_with_parameters() { - let result = build_parameterized_commands( - 1i32, - 3i32, - 1i32, - vec!["name-1", "name-2"], - vec!["echo {val}"], - "val", - ); - assert_eq!( - format!("{}", result.unwrap_err()), - "'--command-name' has been specified 2 times. It has to appear exactly once, or exactly 3 times (number of benchmarks)" - ); -} diff --git a/src/parameter/range_step.rs b/src/parameter/range_step.rs new file mode 100644 index 0000000..bffbeb1 --- /dev/null +++ b/src/parameter/range_step.rs @@ -0,0 +1,150 @@ +use std::convert::TryInto; +use std::ops::{Add, AddAssign, Div, Sub}; + +use crate::error::ParameterScanError; +use crate::util::number::Number; + +pub trait Numeric: + Add + + Sub + + Div + + AddAssign + + PartialOrd + + Copy + + Clone + + From + + Into +{ +} +impl< + T: Add + + Sub + + Div + + AddAssign + + PartialOrd + + Copy + + Clone + + From + + Into, + > Numeric for T +{ +} + +#[derive(Debug)] +pub struct RangeStep { + state: T, + end: T, + step: T, +} + +impl RangeStep { + pub fn new(start: T, end: T, step: T) -> Result { + if end < start { + return Err(ParameterScanError::EmptyRange); + } + + if step == T::from(0) { + return Err(ParameterScanError::ZeroStep); + } + + const MAX_PARAMETERS: usize = 100_000; + match range_step_size_hint(start, end, step) { + (_, Some(size)) if size <= MAX_PARAMETERS => Ok(Self { + state: start, + end, + step, + }), + _ => Err(ParameterScanError::TooLarge), + } + } +} + +impl Iterator for RangeStep { + type Item = T; + + fn next(&mut self) -> Option { + if self.state > self.end { + return None; + } + let return_val = self.state; + self.state += self.step; + + Some(return_val) + } + + fn size_hint(&self) -> (usize, Option) { + range_step_size_hint(self.state, self.end, self.step) + } +} + +fn range_step_size_hint(start: T, end: T, step: T) -> (usize, Option) { + if step == T::from(0) { + return (usize::MAX, None); + } + + let steps = (end - start + T::from(1)) / step; + steps + .into() + .try_into() + .map_or((usize::MAX, None), |u| (u, Some(u))) +} + +#[cfg(test)] +mod tests { + use super::*; + + use rust_decimal::Decimal; + use std::str::FromStr; + + #[test] + fn test_integer_range() { + let param_range: Vec = RangeStep::new(0, 10, 3).unwrap().collect(); + + assert_eq!(param_range.len(), 4); + assert_eq!(param_range[0], 0); + assert_eq!(param_range[3], 9); + } + + #[test] + fn test_decimal_range() { + let param_min = Decimal::from(0); + let param_max = Decimal::from(1); + let step = Decimal::from_str("0.1").unwrap(); + + let param_range: Vec = RangeStep::new(param_min, param_max, step) + .unwrap() + .collect(); + + assert_eq!(param_range.len(), 11); + assert_eq!(param_range[0], Decimal::from(0)); + assert_eq!(param_range[10], Decimal::from(1)); + } + + #[test] + fn test_range_step_validate() { + let result = RangeStep::new(0, 10, 3); + assert!(result.is_ok()); + + let result = RangeStep::new( + Decimal::from(0), + Decimal::from(1), + Decimal::from_str("0.1").unwrap(), + ); + assert!(result.is_ok()); + + let result = RangeStep::new(11, 10, 1); + assert_eq!(format!("{}", result.unwrap_err()), "Empty parameter range"); + + let result = RangeStep::new(0, 10, 0); + assert_eq!( + format!("{}", result.unwrap_err()), + "Zero is not a valid parameter step" + ); + + let result = RangeStep::new(0, 100_001, 1); + assert_eq!( + format!("{}", result.unwrap_err()), + "Parameter range is too large" + ); + } +}