diff options
author | David Peter <mail@david-peter.de> | 2022-02-06 21:15:36 +0100 |
---|---|---|
committer | David Peter <sharkdp@users.noreply.github.com> | 2022-02-06 22:49:04 +0100 |
commit | 6db82cdb2a28caf763d3ba09b7ccefc49300ba44 (patch) | |
tree | 58947cc1725f907b68290cfab6b3fbc8ce97705f | |
parent | a6baf49b726fa0883048c79b3b2cecb7e1a87110 (diff) |
Split range into range_step and commands
-rw-r--r-- | src/command.rs | 180 | ||||
-rw-r--r-- | src/parameter/mod.rs | 2 | ||||
-rw-r--r-- | src/parameter/range.rs | 318 | ||||
-rw-r--r-- | src/parameter/range_step.rs | 150 |
4 files changed, 328 insertions, 322 deletions
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<Item = &'a str>>(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<Vec<Command<'a>>, 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<Values<'a>>, + command_strings: Values<'a>, + mut vals: clap::Values<'a>, + step: Option<&str>, +) -> Result<Vec<Command<'a>>, ParameterScanError> { + let command_names = command_names.map_or(vec![], |names| names.collect::<Vec<&str>>()); + let command_strings = command_strings.collect::<Vec<&str>>(); + 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::<i32>(), + param_max.parse::<i32>(), + step.unwrap_or("1").parse::<i32>(), + ) { + 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<Command<'a>>); impl<'a> Commands<'a> { - /// Build the commands to benchmark pub fn from_cli_arguments(matches: &'a ArgMatches) -> Result<Commands> { 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::<Vec<String>>(); + 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::<Vec<String>>(); + 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<Output = Self> - + Sub<Output = Self> - + Div<Output = Self> - + AddAssign - + PartialOrd - + Copy - + Clone - + From<i32> - + Into<Number> -{ -} -impl< - T: Add<Output = Self> - + Sub<Output = Self> - + Div<Output = Self> - + AddAssign - + PartialOrd - + Copy - + Clone - + From<i32> - + Into<Number>, - > Numeric for T -{ -} - -#[derive(Debug)] -struct RangeStep<T> { - state: T, - end: T, - step: T, -} - -impl<T: Numeric> RangeStep<T> { - fn new(start: T, end: T, step: T) -> Result<Self, ParameterScanError> { - 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<T: Numeric> Iterator for RangeStep<T> { - type Item = T; - - fn next(&mut self) -> Option<Self::Item> { - 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<usize>) { - range_step_size_hint(self.state, self.end, self.step) - } -} - -fn range_step_size_hint<T: Numeric>(start: T, end: T, step: T) -> (usize, Option<usize>) { - 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<Vec<Command<'a>>, 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<Values<'a>>, - command_strings: Values<'a>, - mut vals: clap::Values<'a>, - step: Option<&str>, -) -> Result<Vec<Command<'a>>, ParameterScanError> { - let command_names = command_names.map_or(vec![], |names| names.collect::<Vec<&str>>()); - let command_strings = command_strings.collect::<Vec<&str>>(); - 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::<i32>(), - param_max.parse::<i32>(), - step.unwrap_or("1").parse::<i32>(), - ) { - 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<i32> = 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<Decimal> = 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::<Vec<String>>(); - 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::<Vec<String>>(); - 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<Output = Self> + + Sub<Output = Self> + + Div<Output = Self> + + AddAssign + + PartialOrd + + Copy + + Clone + + From<i32> + + Into<Number> +{ +} +impl< + T: Add<Output = Self> + + Sub<Output = Self> + + Div<Output = Self> + + AddAssign + + PartialOrd + + Copy + + Clone + + From<i32> + + Into<Number>, + > Numeric for T +{ +} + +#[derive(Debug)] +pub struct RangeStep<T> { + state: T, + end: T, + step: T, +} + +impl<T: Numeric> RangeStep<T> { + pub fn new(start: T, end: T, step: T) -> Result<Self, ParameterScanError> { + 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<T: Numeric> Iterator for RangeStep<T> { + type Item = T; + + fn next(&mut self) -> Option<Self::Item> { + 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<usize>) { + range_step_size_hint(self.state, self.end, self.step) + } +} + +fn range_step_size_hint<T: Numeric>(start: T, end: T, step: T) -> (usize, Option<usize>) { + 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<i32> = 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<Decimal> = 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" + ); + } +} |