2021-08-22 17:52:55 +02:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::fmt;
|
2022-02-06 21:15:36 +01:00
|
|
|
use std::str::FromStr;
|
2021-08-22 17:52:55 +02:00
|
|
|
|
2023-04-20 21:32:06 +02:00
|
|
|
use crate::parameter::tokenize::tokenize;
|
|
|
|
|
use crate::parameter::ParameterValue;
|
2022-02-06 21:15:36 +01:00
|
|
|
use crate::{
|
|
|
|
|
error::{OptionsError, ParameterScanError},
|
2022-02-20 14:04:37 +01:00
|
|
|
parameter::{
|
|
|
|
|
range_step::{Numeric, RangeStep},
|
|
|
|
|
ParameterNameAndValue,
|
|
|
|
|
},
|
2022-02-06 21:15:36 +01:00
|
|
|
};
|
2022-02-06 19:31:15 +01:00
|
|
|
|
2022-10-28 11:23:03 +02:00
|
|
|
use clap::{parser::ValuesRef, ArgMatches};
|
2022-02-06 19:31:15 +01:00
|
|
|
|
2022-02-22 13:31:03 +01:00
|
|
|
use anyhow::{bail, Context, Result};
|
2022-02-06 21:15:36 +01:00
|
|
|
use rust_decimal::Decimal;
|
2022-02-06 19:31:15 +01:00
|
|
|
|
2021-08-22 17:52:55 +02:00
|
|
|
/// 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,
|
|
|
|
|
|
|
|
|
|
/// Zero or more parameter values.
|
2022-02-20 14:04:37 +01:00
|
|
|
parameters: Vec<ParameterNameAndValue<'a>>,
|
2021-08-22 17:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> 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,
|
2022-02-20 14:04:37 +01:00
|
|
|
parameters: impl IntoIterator<Item = ParameterNameAndValue<'a>>,
|
2021-08-22 17:52:55 +02:00
|
|
|
) -> Command<'a> {
|
|
|
|
|
Command {
|
|
|
|
|
name,
|
|
|
|
|
expression,
|
2022-02-20 14:04:37 +01:00
|
|
|
parameters: parameters.into_iter().collect(),
|
2021-08-22 17:52:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_name(&self) -> String {
|
|
|
|
|
self.name.map_or_else(
|
2022-02-22 10:55:58 +01:00
|
|
|
|| self.get_command_line(),
|
2021-08-22 17:52:55 +02:00
|
|
|
|name| self.replace_parameters_in(name),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 21:32:06 +02:00
|
|
|
pub fn get_name_with_unused_parameters(&self) -> String {
|
2024-03-13 15:18:12 +08:00
|
|
|
let parameters = self
|
|
|
|
|
.get_unused_parameters()
|
|
|
|
|
.fold(String::new(), |output, (parameter, value)| {
|
2024-05-31 13:26:35 -07:00
|
|
|
output + &format!("{parameter} = {value}, ")
|
2024-03-13 15:18:12 +08:00
|
|
|
});
|
2023-04-20 21:32:06 +02:00
|
|
|
let parameters = parameters.trim_end_matches(", ");
|
|
|
|
|
let parameters = if parameters.is_empty() {
|
|
|
|
|
"".into()
|
|
|
|
|
} else {
|
2024-05-31 13:26:35 -07:00
|
|
|
format!(" ({parameters})")
|
2023-04-20 21:32:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
format!("{}{}", self.get_name(), parameters)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-22 10:55:58 +01:00
|
|
|
pub fn get_command_line(&self) -> String {
|
2021-08-22 17:52:55 +02:00
|
|
|
self.replace_parameters_in(self.expression)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-22 13:31:03 +01:00
|
|
|
pub fn get_command(&self) -> Result<std::process::Command> {
|
|
|
|
|
let command_line = self.get_command_line();
|
|
|
|
|
let mut tokens = shell_words::split(&command_line)
|
2024-05-31 13:26:35 -07:00
|
|
|
.with_context(|| format!("Failed to parse command '{command_line}'"))?
|
2022-02-22 13:31:03 +01:00
|
|
|
.into_iter();
|
|
|
|
|
|
|
|
|
|
if let Some(program_name) = tokens.next() {
|
|
|
|
|
let mut command_builder = std::process::Command::new(program_name);
|
|
|
|
|
command_builder.args(tokens);
|
|
|
|
|
Ok(command_builder)
|
|
|
|
|
} else {
|
|
|
|
|
bail!("Can not execute empty command")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-20 14:04:37 +01:00
|
|
|
pub fn get_parameters(&self) -> &[(&'a str, ParameterValue)] {
|
2021-08-22 17:52:55 +02:00
|
|
|
&self.parameters
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 21:32:06 +02:00
|
|
|
pub fn get_unused_parameters(&self) -> impl Iterator<Item = &(&'a str, ParameterValue)> {
|
|
|
|
|
self.parameters
|
|
|
|
|
.iter()
|
2024-05-31 13:26:35 -07:00
|
|
|
.filter(move |(parameter, _)| !self.expression.contains(&format!("{{{parameter}}}")))
|
2023-04-20 21:32:06 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-22 17:52:55 +02:00
|
|
|
fn replace_parameters_in(&self, original: &str) -> String {
|
|
|
|
|
let mut result = String::new();
|
|
|
|
|
let mut replacements = BTreeMap::<String, String>::new();
|
|
|
|
|
for (param_name, param_value) in &self.parameters {
|
2024-05-31 13:28:55 -07:00
|
|
|
replacements.insert(format!("{{{param_name}}}"), param_value.to_string());
|
2021-08-22 17:52:55 +02:00
|
|
|
}
|
|
|
|
|
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}'
|
|
|
|
|
//
|
2022-02-22 10:55:58 +01:00
|
|
|
// should not ever run 'echo baz baz'. See `test_get_command_line_nonoverlapping`.
|
2021-08-22 17:52:55 +02:00
|
|
|
'outer: while let Some(head) = remaining.chars().next() {
|
|
|
|
|
for (k, v) in &replacements {
|
|
|
|
|
if remaining.starts_with(k.as_str()) {
|
|
|
|
|
result.push_str(v);
|
|
|
|
|
remaining = &remaining[k.len()..];
|
|
|
|
|
continue 'outer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.push(head);
|
|
|
|
|
remaining = &remaining[head.len_utf8()..];
|
|
|
|
|
}
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-19 16:13:41 +01:00
|
|
|
/// A collection of commands that should be benchmarked
|
2022-02-06 21:02:15 +01:00
|
|
|
pub struct Commands<'a>(Vec<Command<'a>>);
|
|
|
|
|
|
|
|
|
|
impl<'a> Commands<'a> {
|
2024-10-26 12:06:52 +01:00
|
|
|
pub fn from_cli_arguments(matches: &'a ArgMatches) -> Result<Commands<'a>> {
|
2022-10-28 09:50:35 +02:00
|
|
|
let command_names = matches.get_many::<String>("command-name");
|
2022-10-28 11:23:03 +02:00
|
|
|
let command_strings = matches
|
|
|
|
|
.get_many::<String>("command")
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.map(|v| v.as_str())
|
|
|
|
|
.collect::<Vec<_>>();
|
2022-02-06 21:02:15 +01:00
|
|
|
|
2022-10-28 09:50:35 +02:00
|
|
|
if let Some(args) = matches.get_many::<String>("parameter-scan") {
|
2022-10-28 11:23:03 +02:00
|
|
|
let step_size = matches
|
|
|
|
|
.get_one::<String>("parameter-step-size")
|
|
|
|
|
.map(|s| s.as_str());
|
2022-02-19 16:13:41 +01:00
|
|
|
Ok(Self(Self::get_parameter_scan_commands(
|
2022-02-06 21:02:15 +01:00
|
|
|
command_names,
|
|
|
|
|
command_strings,
|
|
|
|
|
args,
|
|
|
|
|
step_size,
|
|
|
|
|
)?))
|
2022-10-28 09:50:35 +02:00
|
|
|
} else if let Some(args) = matches.get_many::<String>("parameter-list") {
|
2022-10-28 11:23:03 +02:00
|
|
|
let command_names = command_names.map_or(vec![], |names| {
|
|
|
|
|
names.map(|v| v.as_str()).collect::<Vec<_>>()
|
|
|
|
|
});
|
2022-10-28 09:50:35 +02:00
|
|
|
let args: Vec<_> = args.map(|v| v.as_str()).collect::<Vec<_>>();
|
2022-02-06 21:02:15 +01:00
|
|
|
let param_names_and_values: Vec<(&str, Vec<String>)> = args
|
2026-10-02 11:05:02 +02:00
|
|
|
.as_chunks::<2>()
|
|
|
|
|
.0
|
|
|
|
|
.iter()
|
2022-02-06 21:02:15 +01:00
|
|
|
.map(|pair| {
|
|
|
|
|
let name = pair[0];
|
|
|
|
|
let list_str = pair[1];
|
|
|
|
|
(name, tokenize(list_str))
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
{
|
|
|
|
|
let duplicates =
|
2022-02-19 16:13:41 +01:00
|
|
|
Self::find_duplicates(param_names_and_values.iter().map(|(name, _)| *name));
|
2022-02-06 21:02:15 +01:00
|
|
|
if !duplicates.is_empty() {
|
2026-10-02 11:05:02 +02:00
|
|
|
bail!("Duplicate parameter names: {}", duplicates.join(", "));
|
2022-02-06 21:02:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 20:14:40 +01:00
|
|
|
let dimensions: Vec<usize> = std::iter::once(command_strings.len())
|
2022-02-06 21:02:15 +01:00
|
|
|
.chain(
|
|
|
|
|
param_names_and_values
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(_, values)| values.len()),
|
|
|
|
|
)
|
|
|
|
|
.collect();
|
|
|
|
|
let param_space_size = dimensions.iter().product();
|
|
|
|
|
if param_space_size == 0 {
|
|
|
|
|
return Ok(Self(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 {
|
|
|
|
|
return Err(OptionsError::UnexpectedCommandNameCount(
|
|
|
|
|
command_name_count,
|
|
|
|
|
param_space_size,
|
|
|
|
|
)
|
|
|
|
|
.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
let mut commands = Vec::with_capacity(param_space_size);
|
|
|
|
|
let mut index = vec![0usize; dimensions.len()];
|
|
|
|
|
'outer: loop {
|
|
|
|
|
let name = command_names
|
|
|
|
|
.get(i)
|
2022-09-07 09:20:50 +02:00
|
|
|
.or_else(|| command_names.first())
|
2022-02-06 21:02:15 +01:00
|
|
|
.copied();
|
|
|
|
|
i += 1;
|
|
|
|
|
|
|
|
|
|
let (command_index, params_indices) = index.split_first().unwrap();
|
2022-02-20 14:04:37 +01:00
|
|
|
let parameters: Vec<_> = param_names_and_values
|
2022-02-06 21:02:15 +01:00
|
|
|
.iter()
|
|
|
|
|
.zip(params_indices)
|
|
|
|
|
.map(|((name, values), i)| (*name, ParameterValue::Text(values[*i].clone())))
|
|
|
|
|
.collect();
|
|
|
|
|
commands.push(Command::new_parametrized(
|
|
|
|
|
name,
|
2022-11-16 20:14:40 +01:00
|
|
|
command_strings[*command_index],
|
2022-02-06 21:02:15 +01:00
|
|
|
parameters,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// Increment index, exiting loop on overflow.
|
|
|
|
|
for (i, n) in index.iter_mut().zip(dimensions.iter()) {
|
|
|
|
|
*i += 1;
|
|
|
|
|
if *i < *n {
|
|
|
|
|
continue 'outer;
|
|
|
|
|
} else {
|
|
|
|
|
*i = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self(commands))
|
|
|
|
|
} else {
|
2022-10-28 11:23:03 +02:00
|
|
|
let command_names = command_names.map_or(vec![], |names| {
|
|
|
|
|
names.map(|v| v.as_str()).collect::<Vec<_>>()
|
|
|
|
|
});
|
2022-02-06 21:02:15 +01:00
|
|
|
if command_names.len() > command_strings.len() {
|
|
|
|
|
return Err(OptionsError::TooManyCommandNames(command_strings.len()).into());
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 20:24:56 +01:00
|
|
|
let mut commands = Vec::with_capacity(command_strings.len());
|
|
|
|
|
for (i, s) in command_strings.iter().enumerate() {
|
2022-02-06 21:02:15 +01:00
|
|
|
commands.push(Command::new(command_names.get(i).copied(), s));
|
|
|
|
|
}
|
|
|
|
|
Ok(Self(commands))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn iter(&self) -> impl Iterator<Item = &Command<'a>> {
|
|
|
|
|
self.0.iter()
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-11 22:18:19 +01:00
|
|
|
pub fn num_commands(&self, has_reference_command: bool) -> usize {
|
|
|
|
|
self.0.len() + if has_reference_command { 1 } else { 0 }
|
2022-02-06 21:02:15 +01:00
|
|
|
}
|
2022-02-19 16:13:41 +01:00
|
|
|
|
|
|
|
|
/// Finds all the strings that appear multiple times in the input iterator, returning them in
|
|
|
|
|
/// sorted order. If no string appears more than once, the result is an empty vector.
|
|
|
|
|
fn find_duplicates<'b, I: IntoIterator<Item = &'b str>>(i: I) -> Vec<&'b str> {
|
|
|
|
|
let mut counts = BTreeMap::<&'b str, usize>::new();
|
|
|
|
|
for s in i {
|
|
|
|
|
*counts.entry(s).or_default() += 1;
|
|
|
|
|
}
|
|
|
|
|
counts
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|(k, n)| if n > 1 { Some(k) } else { None })
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_parameter_scan_commands<'b, T: Numeric>(
|
|
|
|
|
param_name: &'b str,
|
|
|
|
|
param_min: T,
|
|
|
|
|
param_max: T,
|
|
|
|
|
step: T,
|
|
|
|
|
command_names: Vec<&'b str>,
|
|
|
|
|
command_strings: Vec<&'b str>,
|
|
|
|
|
) -> Result<Vec<Command<'b>>, ParameterScanError> {
|
|
|
|
|
let param_range = RangeStep::new(param_min, param_max, step)?;
|
|
|
|
|
let command_name_count = command_names.len();
|
|
|
|
|
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
let mut commands = vec![];
|
|
|
|
|
for value in param_range {
|
|
|
|
|
for cmd in &command_strings {
|
|
|
|
|
let name = command_names
|
|
|
|
|
.get(i)
|
2022-09-07 09:20:50 +02:00
|
|
|
.or_else(|| command_names.first())
|
2022-02-19 16:13:41 +01:00
|
|
|
.copied();
|
|
|
|
|
commands.push(Command::new_parametrized(
|
|
|
|
|
name,
|
|
|
|
|
cmd,
|
|
|
|
|
vec![(param_name, ParameterValue::Numeric(value.into()))],
|
|
|
|
|
));
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-05 23:54:30 +00:00
|
|
|
|
|
|
|
|
// `--command-name` should appear exactly once or exactly B times,
|
|
|
|
|
// where B is the total number of benchmarks.
|
|
|
|
|
let command_count = commands.len();
|
|
|
|
|
if command_name_count > 1 && command_name_count != command_count {
|
|
|
|
|
return Err(ParameterScanError::UnexpectedCommandNameCount(
|
|
|
|
|
command_name_count,
|
|
|
|
|
command_count,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-19 16:13:41 +01:00
|
|
|
Ok(commands)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_parameter_scan_commands<'b>(
|
2022-10-28 09:50:35 +02:00
|
|
|
command_names: Option<ValuesRef<'b, String>>,
|
|
|
|
|
command_strings: Vec<&'b str>,
|
|
|
|
|
mut vals: ValuesRef<'b, String>,
|
2022-02-19 16:13:41 +01:00
|
|
|
step: Option<&str>,
|
|
|
|
|
) -> Result<Vec<Command<'b>>, ParameterScanError> {
|
2022-10-28 11:23:03 +02:00
|
|
|
let command_names = command_names.map_or(vec![], |names| {
|
|
|
|
|
names.map(|v| v.as_str()).collect::<Vec<_>>()
|
|
|
|
|
});
|
2022-10-28 09:50:35 +02:00
|
|
|
let param_name = vals.next().unwrap().as_str();
|
|
|
|
|
let param_min = vals.next().unwrap().as_str();
|
|
|
|
|
let param_max = vals.next().unwrap().as_str();
|
2022-02-19 16:13:41 +01:00
|
|
|
|
|
|
|
|
// 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 Self::build_parameter_scan_commands(
|
|
|
|
|
param_name,
|
|
|
|
|
param_min,
|
|
|
|
|
param_max,
|
|
|
|
|
step,
|
|
|
|
|
command_names,
|
|
|
|
|
command_strings,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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())?;
|
|
|
|
|
Self::build_parameter_scan_commands(
|
|
|
|
|
param_name,
|
|
|
|
|
param_min,
|
|
|
|
|
param_max,
|
|
|
|
|
step,
|
|
|
|
|
command_names,
|
|
|
|
|
command_strings,
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-02-06 21:02:15 +01:00
|
|
|
}
|
|
|
|
|
|
2021-08-22 17:52:55 +02:00
|
|
|
#[test]
|
2022-02-22 10:55:58 +01:00
|
|
|
fn test_get_command_line_nonoverlapping() {
|
2021-08-22 17:52:55 +02:00
|
|
|
let cmd = Command::new_parametrized(
|
|
|
|
|
None,
|
|
|
|
|
"echo {foo} {bar}",
|
|
|
|
|
vec![
|
|
|
|
|
("foo", ParameterValue::Text("{bar} baz".into())),
|
|
|
|
|
("bar", ParameterValue::Text("quux".into())),
|
|
|
|
|
],
|
|
|
|
|
);
|
2022-02-22 10:55:58 +01:00
|
|
|
assert_eq!(cmd.get_command_line(), "echo {bar} baz quux");
|
2021-08-22 17:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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");
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-10 22:49:49 +00:00
|
|
|
impl fmt::Display for Command<'_> {
|
2021-08-22 17:52:55 +02:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2022-02-22 10:55:58 +01:00
|
|
|
write!(f, "{}", self.get_command_line())
|
2021-08-22 17:52:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-02-06 19:31:15 +01:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_build_commands_cross_product() {
|
2022-02-22 14:04:07 +01:00
|
|
|
use crate::cli::get_cli_arguments;
|
2022-02-06 19:31:15 +01:00
|
|
|
|
2022-02-06 21:02:15 +01:00
|
|
|
let matches = get_cli_arguments(vec![
|
2022-02-06 19:31:15 +01:00
|
|
|
"hyperfine",
|
|
|
|
|
"-L",
|
|
|
|
|
"par1",
|
|
|
|
|
"a,b",
|
|
|
|
|
"-L",
|
|
|
|
|
"par2",
|
|
|
|
|
"z,y",
|
|
|
|
|
"echo {par1} {par2}",
|
|
|
|
|
"printf '%s\n' {par1} {par2}",
|
|
|
|
|
]);
|
2022-02-06 21:02:15 +01:00
|
|
|
let result = Commands::from_cli_arguments(&matches).unwrap().0;
|
2022-02-06 19:31:15 +01:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
let pv = |s: &str| ParameterValue::Text(s.to_string());
|
|
|
|
|
let cmd = |cmd: usize, par1: &str, par2: &str| {
|
|
|
|
|
let expression = ["echo {par1} {par2}", "printf '%s\n' {par1} {par2}"][cmd];
|
|
|
|
|
let params = vec![("par1", pv(par1)), ("par2", pv(par2))];
|
|
|
|
|
Command::new_parametrized(None, expression, params)
|
|
|
|
|
};
|
|
|
|
|
let expected = vec![
|
|
|
|
|
cmd(0, "a", "z"),
|
|
|
|
|
cmd(1, "a", "z"),
|
|
|
|
|
cmd(0, "b", "z"),
|
|
|
|
|
cmd(1, "b", "z"),
|
|
|
|
|
cmd(0, "a", "y"),
|
|
|
|
|
cmd(1, "a", "y"),
|
|
|
|
|
cmd(0, "b", "y"),
|
|
|
|
|
cmd(1, "b", "y"),
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(result, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_build_parameter_list_commands() {
|
2022-02-22 14:04:07 +01:00
|
|
|
use crate::cli::get_cli_arguments;
|
2022-02-06 19:31:15 +01:00
|
|
|
|
2022-02-06 21:02:15 +01:00
|
|
|
let matches = get_cli_arguments(vec![
|
2022-02-06 19:31:15 +01:00
|
|
|
"hyperfine",
|
|
|
|
|
"echo {foo}",
|
|
|
|
|
"--parameter-list",
|
|
|
|
|
"foo",
|
|
|
|
|
"1,2",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"name-{foo}",
|
|
|
|
|
]);
|
2022-02-06 21:02:15 +01:00
|
|
|
let commands = Commands::from_cli_arguments(&matches).unwrap().0;
|
2022-02-06 19:31:15 +01:00
|
|
|
assert_eq!(commands.len(), 2);
|
|
|
|
|
assert_eq!(commands[0].get_name(), "name-1");
|
|
|
|
|
assert_eq!(commands[1].get_name(), "name-2");
|
2022-02-22 10:55:58 +01:00
|
|
|
assert_eq!(commands[0].get_command_line(), "echo 1");
|
|
|
|
|
assert_eq!(commands[1].get_command_line(), "echo 2");
|
2022-02-06 19:31:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2022-02-19 16:13:41 +01:00
|
|
|
fn test_build_parameter_scan_commands() {
|
2022-02-22 14:04:07 +01:00
|
|
|
use crate::cli::get_cli_arguments;
|
2022-02-06 21:02:15 +01:00
|
|
|
let matches = get_cli_arguments(vec![
|
2022-02-06 19:31:15 +01:00
|
|
|
"hyperfine",
|
|
|
|
|
"echo {val}",
|
|
|
|
|
"--parameter-scan",
|
|
|
|
|
"val",
|
|
|
|
|
"1",
|
|
|
|
|
"2",
|
|
|
|
|
"--parameter-step-size",
|
|
|
|
|
"1",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"name-{val}",
|
|
|
|
|
]);
|
2022-02-06 21:02:15 +01:00
|
|
|
let commands = Commands::from_cli_arguments(&matches).unwrap().0;
|
2022-02-06 19:31:15 +01:00
|
|
|
assert_eq!(commands.len(), 2);
|
|
|
|
|
assert_eq!(commands[0].get_name(), "name-1");
|
|
|
|
|
assert_eq!(commands[1].get_name(), "name-2");
|
2022-02-22 10:55:58 +01:00
|
|
|
assert_eq!(commands[0].get_command_line(), "echo 1");
|
|
|
|
|
assert_eq!(commands[1].get_command_line(), "echo 2");
|
2022-02-06 19:31:15 +01:00
|
|
|
}
|
2022-02-06 21:15:36 +01:00
|
|
|
|
2025-01-05 23:54:30 +00:00
|
|
|
#[test]
|
|
|
|
|
fn test_build_parameter_scan_commands_named() {
|
|
|
|
|
use crate::cli::get_cli_arguments;
|
|
|
|
|
let matches = get_cli_arguments(vec![
|
|
|
|
|
"hyperfine",
|
|
|
|
|
"echo {val}",
|
|
|
|
|
"sleep {val}",
|
|
|
|
|
"--parameter-scan",
|
|
|
|
|
"val",
|
|
|
|
|
"1",
|
|
|
|
|
"2",
|
|
|
|
|
"--parameter-step-size",
|
|
|
|
|
"1",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"echo-1",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"sleep-1",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"echo-2",
|
|
|
|
|
"--command-name",
|
|
|
|
|
"sleep-2",
|
|
|
|
|
]);
|
|
|
|
|
let commands = Commands::from_cli_arguments(&matches).unwrap().0;
|
|
|
|
|
assert_eq!(commands.len(), 4);
|
|
|
|
|
assert_eq!(commands[0].get_name(), "echo-1");
|
|
|
|
|
assert_eq!(commands[0].get_command_line(), "echo 1");
|
|
|
|
|
assert_eq!(commands[1].get_name(), "sleep-1");
|
|
|
|
|
assert_eq!(commands[1].get_command_line(), "sleep 1");
|
|
|
|
|
assert_eq!(commands[2].get_name(), "echo-2");
|
|
|
|
|
assert_eq!(commands[2].get_command_line(), "echo 2");
|
|
|
|
|
assert_eq!(commands[3].get_name(), "sleep-2");
|
|
|
|
|
assert_eq!(commands[3].get_command_line(), "sleep 2");
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-06 21:15:36 +01:00
|
|
|
#[test]
|
2022-02-19 16:13:41 +01:00
|
|
|
fn test_parameter_scan_commands_int() {
|
|
|
|
|
let commands = Commands::build_parameter_scan_commands(
|
|
|
|
|
"val",
|
|
|
|
|
1i32,
|
|
|
|
|
7i32,
|
|
|
|
|
3i32,
|
|
|
|
|
vec![],
|
|
|
|
|
vec!["echo {val}"],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2022-02-06 21:15:36 +01:00
|
|
|
assert_eq!(commands.len(), 3);
|
|
|
|
|
assert_eq!(commands[2].get_name(), "echo 7");
|
2022-02-22 10:55:58 +01:00
|
|
|
assert_eq!(commands[2].get_command_line(), "echo 7");
|
2022-02-06 21:15:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2022-02-19 16:13:41 +01:00
|
|
|
fn test_parameter_scan_commands_decimal() {
|
2022-02-06 21:15:36 +01:00
|
|
|
let param_min = Decimal::from_str("0").unwrap();
|
|
|
|
|
let param_max = Decimal::from_str("1").unwrap();
|
|
|
|
|
let step = Decimal::from_str("0.33").unwrap();
|
|
|
|
|
|
2022-02-19 16:13:41 +01:00
|
|
|
let commands = Commands::build_parameter_scan_commands(
|
|
|
|
|
"val",
|
2022-02-06 21:15:36 +01:00
|
|
|
param_min,
|
|
|
|
|
param_max,
|
|
|
|
|
step,
|
|
|
|
|
vec![],
|
|
|
|
|
vec!["echo {val}"],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(commands.len(), 4);
|
|
|
|
|
assert_eq!(commands[3].get_name(), "echo 0.99");
|
2022-02-22 10:55:58 +01:00
|
|
|
assert_eq!(commands[3].get_command_line(), "echo 0.99");
|
2022-02-06 21:15:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2022-10-05 13:15:16 -07:00
|
|
|
fn test_parameter_scan_commands_names() {
|
2022-02-19 16:13:41 +01:00
|
|
|
let commands = Commands::build_parameter_scan_commands(
|
|
|
|
|
"val",
|
2022-02-06 21:15:36 +01:00
|
|
|
1i32,
|
|
|
|
|
3i32,
|
|
|
|
|
1i32,
|
|
|
|
|
vec!["name-{val}"],
|
|
|
|
|
vec!["echo {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() {
|
2022-02-19 16:13:41 +01:00
|
|
|
let commands = Commands::build_parameter_scan_commands(
|
|
|
|
|
"val",
|
2022-02-06 21:15:36 +01:00
|
|
|
1i32,
|
|
|
|
|
3i32,
|
|
|
|
|
1i32,
|
|
|
|
|
vec!["name-a", "name-b", "name-c"],
|
|
|
|
|
vec!["echo {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() {
|
2022-02-19 16:13:41 +01:00
|
|
|
let result = Commands::build_parameter_scan_commands(
|
|
|
|
|
"val",
|
2022-02-06 21:15:36 +01:00
|
|
|
1i32,
|
|
|
|
|
3i32,
|
|
|
|
|
1i32,
|
|
|
|
|
vec!["name-1", "name-2"],
|
|
|
|
|
vec!["echo {val}"],
|
|
|
|
|
);
|
2022-02-19 16:13:41 +01:00
|
|
|
assert!(matches!(
|
|
|
|
|
result.unwrap_err(),
|
|
|
|
|
ParameterScanError::UnexpectedCommandNameCount(2, 3)
|
|
|
|
|
));
|
2022-02-06 21:15:36 +01:00
|
|
|
}
|