command.rs
1 use std::collections::BTreeMap; 2 use std::fmt; 3 use std::str::FromStr; 4 5 use crate::parameter::tokenize::tokenize; 6 use crate::parameter::ParameterValue; 7 use crate::{ 8 error::{OptionsError, ParameterScanError}, 9 parameter::{ 10 range_step::{Numeric, RangeStep}, 11 ParameterNameAndValue, 12 }, 13 }; 14 15 use clap::{parser::ValuesRef, ArgMatches}; 16 17 use anyhow::{bail, Context, Result}; 18 use rust_decimal::Decimal; 19 20 /// A command that should be benchmarked. 21 #[derive(Debug, Clone, PartialEq, Eq)] 22 pub struct Command<'a> { 23 /// The command name (without parameter substitution) 24 name: Option<&'a str>, 25 26 /// The command that should be executed (without parameter substitution) 27 expression: &'a str, 28 29 /// Zero or more parameter values. 30 parameters: Vec<ParameterNameAndValue<'a>>, 31 } 32 33 impl<'a> Command<'a> { 34 pub fn new(name: Option<&'a str>, expression: &'a str) -> Command<'a> { 35 Command { 36 name, 37 expression, 38 parameters: Vec::new(), 39 } 40 } 41 42 pub fn new_parametrized( 43 name: Option<&'a str>, 44 expression: &'a str, 45 parameters: impl IntoIterator<Item = ParameterNameAndValue<'a>>, 46 ) -> Command<'a> { 47 Command { 48 name, 49 expression, 50 parameters: parameters.into_iter().collect(), 51 } 52 } 53 54 pub fn get_name(&self) -> String { 55 self.name.map_or_else( 56 || self.get_command_line(), 57 |name| self.replace_parameters_in(name), 58 ) 59 } 60 61 pub fn get_name_with_unused_parameters(&self) -> String { 62 let parameters = self 63 .get_unused_parameters() 64 .fold(String::new(), |output, (parameter, value)| { 65 output + &format!("{parameter} = {value}, ") 66 }); 67 let parameters = parameters.trim_end_matches(", "); 68 let parameters = if parameters.is_empty() { 69 "".into() 70 } else { 71 format!(" ({parameters})") 72 }; 73 74 format!("{}{}", self.get_name(), parameters) 75 } 76 77 pub fn get_command_line(&self) -> String { 78 self.replace_parameters_in(self.expression) 79 } 80 81 pub fn get_command(&self) -> Result<std::process::Command> { 82 let command_line = self.get_command_line(); 83 let mut tokens = shell_words::split(&command_line) 84 .with_context(|| format!("Failed to parse command '{command_line}'"))? 85 .into_iter(); 86 87 if let Some(program_name) = tokens.next() { 88 let mut command_builder = std::process::Command::new(program_name); 89 command_builder.args(tokens); 90 Ok(command_builder) 91 } else { 92 bail!("Can not execute empty command") 93 } 94 } 95 96 pub fn get_parameters(&self) -> &[(&'a str, ParameterValue)] { 97 &self.parameters 98 } 99 100 pub fn get_unused_parameters(&self) -> impl Iterator<Item = &(&'a str, ParameterValue)> { 101 self.parameters 102 .iter() 103 .filter(move |(parameter, _)| !self.expression.contains(&format!("{{{parameter}}}"))) 104 } 105 106 fn replace_parameters_in(&self, original: &str) -> String { 107 let mut result = String::new(); 108 let mut replacements = BTreeMap::<String, String>::new(); 109 for (param_name, param_value) in &self.parameters { 110 replacements.insert(format!("{{{param_name}}}"), param_value.to_string()); 111 } 112 let mut remaining = original; 113 // Manually replace consecutive occurrences to avoid double-replacing: e.g., 114 // 115 // hyperfine -L foo 'a,{bar}' -L bar 'baz,quux' 'echo {foo} {bar}' 116 // 117 // should not ever run 'echo baz baz'. See `test_get_command_line_nonoverlapping`. 118 'outer: while let Some(head) = remaining.chars().next() { 119 for (k, v) in &replacements { 120 if remaining.starts_with(k.as_str()) { 121 result.push_str(v); 122 remaining = &remaining[k.len()..]; 123 continue 'outer; 124 } 125 } 126 result.push(head); 127 remaining = &remaining[head.len_utf8()..]; 128 } 129 result 130 } 131 } 132 133 /// A collection of commands that should be benchmarked 134 pub struct Commands<'a>(Vec<Command<'a>>); 135 136 impl<'a> Commands<'a> { 137 pub fn from_cli_arguments(matches: &'a ArgMatches) -> Result<Commands<'a>> { 138 let command_names = matches.get_many::<String>("command-name"); 139 let command_strings = matches 140 .get_many::<String>("command") 141 .unwrap_or_default() 142 .map(|v| v.as_str()) 143 .collect::<Vec<_>>(); 144 145 if let Some(args) = matches.get_many::<String>("parameter-scan") { 146 let step_size = matches 147 .get_one::<String>("parameter-step-size") 148 .map(|s| s.as_str()); 149 Ok(Self(Self::get_parameter_scan_commands( 150 command_names, 151 command_strings, 152 args, 153 step_size, 154 )?)) 155 } else if let Some(args) = matches.get_many::<String>("parameter-list") { 156 let command_names = command_names.map_or(vec![], |names| { 157 names.map(|v| v.as_str()).collect::<Vec<_>>() 158 }); 159 let args: Vec<_> = args.map(|v| v.as_str()).collect::<Vec<_>>(); 160 let param_names_and_values: Vec<(&str, Vec<String>)> = args 161 .chunks_exact(2) 162 .map(|pair| { 163 let name = pair[0]; 164 let list_str = pair[1]; 165 (name, tokenize(list_str)) 166 }) 167 .collect(); 168 { 169 let duplicates = 170 Self::find_duplicates(param_names_and_values.iter().map(|(name, _)| *name)); 171 if !duplicates.is_empty() { 172 bail!("Duplicate parameter names: {}", &duplicates.join(", ")); 173 } 174 } 175 176 let dimensions: Vec<usize> = std::iter::once(command_strings.len()) 177 .chain( 178 param_names_and_values 179 .iter() 180 .map(|(_, values)| values.len()), 181 ) 182 .collect(); 183 let param_space_size = dimensions.iter().product(); 184 if param_space_size == 0 { 185 return Ok(Self(Vec::new())); 186 } 187 188 // `--command-name` should appear exactly once or exactly B times, 189 // where B is the total number of benchmarks. 190 let command_name_count = command_names.len(); 191 if command_name_count > 1 && command_name_count != param_space_size { 192 return Err(OptionsError::UnexpectedCommandNameCount( 193 command_name_count, 194 param_space_size, 195 ) 196 .into()); 197 } 198 199 let mut i = 0; 200 let mut commands = Vec::with_capacity(param_space_size); 201 let mut index = vec![0usize; dimensions.len()]; 202 'outer: loop { 203 let name = command_names 204 .get(i) 205 .or_else(|| command_names.first()) 206 .copied(); 207 i += 1; 208 209 let (command_index, params_indices) = index.split_first().unwrap(); 210 let parameters: Vec<_> = param_names_and_values 211 .iter() 212 .zip(params_indices) 213 .map(|((name, values), i)| (*name, ParameterValue::Text(values[*i].clone()))) 214 .collect(); 215 commands.push(Command::new_parametrized( 216 name, 217 command_strings[*command_index], 218 parameters, 219 )); 220 221 // Increment index, exiting loop on overflow. 222 for (i, n) in index.iter_mut().zip(dimensions.iter()) { 223 *i += 1; 224 if *i < *n { 225 continue 'outer; 226 } else { 227 *i = 0; 228 } 229 } 230 break 'outer; 231 } 232 233 Ok(Self(commands)) 234 } else { 235 let command_names = command_names.map_or(vec![], |names| { 236 names.map(|v| v.as_str()).collect::<Vec<_>>() 237 }); 238 if command_names.len() > command_strings.len() { 239 return Err(OptionsError::TooManyCommandNames(command_strings.len()).into()); 240 } 241 242 let mut commands = Vec::with_capacity(command_strings.len()); 243 for (i, s) in command_strings.iter().enumerate() { 244 commands.push(Command::new(command_names.get(i).copied(), s)); 245 } 246 Ok(Self(commands)) 247 } 248 } 249 250 pub fn iter(&self) -> impl Iterator<Item = &Command<'a>> { 251 self.0.iter() 252 } 253 254 pub fn num_commands(&self, has_reference_command: bool) -> usize { 255 self.0.len() + if has_reference_command { 1 } else { 0 } 256 } 257 258 /// Finds all the strings that appear multiple times in the input iterator, returning them in 259 /// sorted order. If no string appears more than once, the result is an empty vector. 260 fn find_duplicates<'b, I: IntoIterator<Item = &'b str>>(i: I) -> Vec<&'b str> { 261 let mut counts = BTreeMap::<&'b str, usize>::new(); 262 for s in i { 263 *counts.entry(s).or_default() += 1; 264 } 265 counts 266 .into_iter() 267 .filter_map(|(k, n)| if n > 1 { Some(k) } else { None }) 268 .collect() 269 } 270 271 fn build_parameter_scan_commands<'b, T: Numeric>( 272 param_name: &'b str, 273 param_min: T, 274 param_max: T, 275 step: T, 276 command_names: Vec<&'b str>, 277 command_strings: Vec<&'b str>, 278 ) -> Result<Vec<Command<'b>>, ParameterScanError> { 279 let param_range = RangeStep::new(param_min, param_max, step)?; 280 let command_name_count = command_names.len(); 281 282 let mut i = 0; 283 let mut commands = vec![]; 284 for value in param_range { 285 for cmd in &command_strings { 286 let name = command_names 287 .get(i) 288 .or_else(|| command_names.first()) 289 .copied(); 290 commands.push(Command::new_parametrized( 291 name, 292 cmd, 293 vec![(param_name, ParameterValue::Numeric(value.into()))], 294 )); 295 i += 1; 296 } 297 } 298 299 // `--command-name` should appear exactly once or exactly B times, 300 // where B is the total number of benchmarks. 301 let command_count = commands.len(); 302 if command_name_count > 1 && command_name_count != command_count { 303 return Err(ParameterScanError::UnexpectedCommandNameCount( 304 command_name_count, 305 command_count, 306 )); 307 } 308 309 Ok(commands) 310 } 311 312 fn get_parameter_scan_commands<'b>( 313 command_names: Option<ValuesRef<'b, String>>, 314 command_strings: Vec<&'b str>, 315 mut vals: ValuesRef<'b, String>, 316 step: Option<&str>, 317 ) -> Result<Vec<Command<'b>>, ParameterScanError> { 318 let command_names = command_names.map_or(vec![], |names| { 319 names.map(|v| v.as_str()).collect::<Vec<_>>() 320 }); 321 let param_name = vals.next().unwrap().as_str(); 322 let param_min = vals.next().unwrap().as_str(); 323 let param_max = vals.next().unwrap().as_str(); 324 325 // attempt to parse as integers 326 if let (Ok(param_min), Ok(param_max), Ok(step)) = ( 327 param_min.parse::<i32>(), 328 param_max.parse::<i32>(), 329 step.unwrap_or("1").parse::<i32>(), 330 ) { 331 return Self::build_parameter_scan_commands( 332 param_name, 333 param_min, 334 param_max, 335 step, 336 command_names, 337 command_strings, 338 ); 339 } 340 341 // try parsing them as decimals 342 let param_min = Decimal::from_str(param_min)?; 343 let param_max = Decimal::from_str(param_max)?; 344 345 if step.is_none() { 346 return Err(ParameterScanError::StepRequired); 347 } 348 349 let step = Decimal::from_str(step.unwrap())?; 350 Self::build_parameter_scan_commands( 351 param_name, 352 param_min, 353 param_max, 354 step, 355 command_names, 356 command_strings, 357 ) 358 } 359 } 360 361 #[test] 362 fn test_get_command_line_nonoverlapping() { 363 let cmd = Command::new_parametrized( 364 None, 365 "echo {foo} {bar}", 366 vec![ 367 ("foo", ParameterValue::Text("{bar} baz".into())), 368 ("bar", ParameterValue::Text("quux".into())), 369 ], 370 ); 371 assert_eq!(cmd.get_command_line(), "echo {bar} baz quux"); 372 } 373 374 #[test] 375 fn test_get_parameterized_command_name() { 376 let cmd = Command::new_parametrized( 377 Some("name-{bar}-{foo}"), 378 "echo {foo} {bar}", 379 vec![ 380 ("foo", ParameterValue::Text("baz".into())), 381 ("bar", ParameterValue::Text("quux".into())), 382 ], 383 ); 384 assert_eq!(cmd.get_name(), "name-quux-baz"); 385 } 386 387 impl fmt::Display for Command<'_> { 388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 389 write!(f, "{}", self.get_command_line()) 390 } 391 } 392 393 #[test] 394 fn test_build_commands_cross_product() { 395 use crate::cli::get_cli_arguments; 396 397 let matches = get_cli_arguments(vec![ 398 "hyperfine", 399 "-L", 400 "par1", 401 "a,b", 402 "-L", 403 "par2", 404 "z,y", 405 "echo {par1} {par2}", 406 "printf '%s\n' {par1} {par2}", 407 ]); 408 let result = Commands::from_cli_arguments(&matches).unwrap().0; 409 410 // Iteration order: command list first, then parameters in listed order (here, "par1" before 411 // "par2", which is distinct from their sorted order), with parameter values in listed order. 412 let pv = |s: &str| ParameterValue::Text(s.to_string()); 413 let cmd = |cmd: usize, par1: &str, par2: &str| { 414 let expression = ["echo {par1} {par2}", "printf '%s\n' {par1} {par2}"][cmd]; 415 let params = vec![("par1", pv(par1)), ("par2", pv(par2))]; 416 Command::new_parametrized(None, expression, params) 417 }; 418 let expected = vec![ 419 cmd(0, "a", "z"), 420 cmd(1, "a", "z"), 421 cmd(0, "b", "z"), 422 cmd(1, "b", "z"), 423 cmd(0, "a", "y"), 424 cmd(1, "a", "y"), 425 cmd(0, "b", "y"), 426 cmd(1, "b", "y"), 427 ]; 428 assert_eq!(result, expected); 429 } 430 431 #[test] 432 fn test_build_parameter_list_commands() { 433 use crate::cli::get_cli_arguments; 434 435 let matches = get_cli_arguments(vec![ 436 "hyperfine", 437 "echo {foo}", 438 "--parameter-list", 439 "foo", 440 "1,2", 441 "--command-name", 442 "name-{foo}", 443 ]); 444 let commands = Commands::from_cli_arguments(&matches).unwrap().0; 445 assert_eq!(commands.len(), 2); 446 assert_eq!(commands[0].get_name(), "name-1"); 447 assert_eq!(commands[1].get_name(), "name-2"); 448 assert_eq!(commands[0].get_command_line(), "echo 1"); 449 assert_eq!(commands[1].get_command_line(), "echo 2"); 450 } 451 452 #[test] 453 fn test_build_parameter_scan_commands() { 454 use crate::cli::get_cli_arguments; 455 let matches = get_cli_arguments(vec![ 456 "hyperfine", 457 "echo {val}", 458 "--parameter-scan", 459 "val", 460 "1", 461 "2", 462 "--parameter-step-size", 463 "1", 464 "--command-name", 465 "name-{val}", 466 ]); 467 let commands = Commands::from_cli_arguments(&matches).unwrap().0; 468 assert_eq!(commands.len(), 2); 469 assert_eq!(commands[0].get_name(), "name-1"); 470 assert_eq!(commands[1].get_name(), "name-2"); 471 assert_eq!(commands[0].get_command_line(), "echo 1"); 472 assert_eq!(commands[1].get_command_line(), "echo 2"); 473 } 474 475 #[test] 476 fn test_build_parameter_scan_commands_named() { 477 use crate::cli::get_cli_arguments; 478 let matches = get_cli_arguments(vec![ 479 "hyperfine", 480 "echo {val}", 481 "sleep {val}", 482 "--parameter-scan", 483 "val", 484 "1", 485 "2", 486 "--parameter-step-size", 487 "1", 488 "--command-name", 489 "echo-1", 490 "--command-name", 491 "sleep-1", 492 "--command-name", 493 "echo-2", 494 "--command-name", 495 "sleep-2", 496 ]); 497 let commands = Commands::from_cli_arguments(&matches).unwrap().0; 498 assert_eq!(commands.len(), 4); 499 assert_eq!(commands[0].get_name(), "echo-1"); 500 assert_eq!(commands[0].get_command_line(), "echo 1"); 501 assert_eq!(commands[1].get_name(), "sleep-1"); 502 assert_eq!(commands[1].get_command_line(), "sleep 1"); 503 assert_eq!(commands[2].get_name(), "echo-2"); 504 assert_eq!(commands[2].get_command_line(), "echo 2"); 505 assert_eq!(commands[3].get_name(), "sleep-2"); 506 assert_eq!(commands[3].get_command_line(), "sleep 2"); 507 } 508 509 #[test] 510 fn test_parameter_scan_commands_int() { 511 let commands = Commands::build_parameter_scan_commands( 512 "val", 513 1i32, 514 7i32, 515 3i32, 516 vec![], 517 vec!["echo {val}"], 518 ) 519 .unwrap(); 520 assert_eq!(commands.len(), 3); 521 assert_eq!(commands[2].get_name(), "echo 7"); 522 assert_eq!(commands[2].get_command_line(), "echo 7"); 523 } 524 525 #[test] 526 fn test_parameter_scan_commands_decimal() { 527 let param_min = Decimal::from_str("0").unwrap(); 528 let param_max = Decimal::from_str("1").unwrap(); 529 let step = Decimal::from_str("0.33").unwrap(); 530 531 let commands = Commands::build_parameter_scan_commands( 532 "val", 533 param_min, 534 param_max, 535 step, 536 vec![], 537 vec!["echo {val}"], 538 ) 539 .unwrap(); 540 assert_eq!(commands.len(), 4); 541 assert_eq!(commands[3].get_name(), "echo 0.99"); 542 assert_eq!(commands[3].get_command_line(), "echo 0.99"); 543 } 544 545 #[test] 546 fn test_parameter_scan_commands_names() { 547 let commands = Commands::build_parameter_scan_commands( 548 "val", 549 1i32, 550 3i32, 551 1i32, 552 vec!["name-{val}"], 553 vec!["echo {val}"], 554 ) 555 .unwrap(); 556 assert_eq!(commands.len(), 3); 557 let command_names = commands 558 .iter() 559 .map(|c| c.get_name()) 560 .collect::<Vec<String>>(); 561 assert_eq!(command_names, vec!["name-1", "name-2", "name-3"]); 562 } 563 564 #[test] 565 fn test_get_specified_command_names() { 566 let commands = Commands::build_parameter_scan_commands( 567 "val", 568 1i32, 569 3i32, 570 1i32, 571 vec!["name-a", "name-b", "name-c"], 572 vec!["echo {val}"], 573 ) 574 .unwrap(); 575 assert_eq!(commands.len(), 3); 576 let command_names = commands 577 .iter() 578 .map(|c| c.get_name()) 579 .collect::<Vec<String>>(); 580 assert_eq!(command_names, vec!["name-a", "name-b", "name-c"]); 581 } 582 583 #[test] 584 fn test_different_command_name_count_with_parameters() { 585 let result = Commands::build_parameter_scan_commands( 586 "val", 587 1i32, 588 3i32, 589 1i32, 590 vec!["name-1", "name-2"], 591 vec!["echo {val}"], 592 ); 593 assert!(matches!( 594 result.unwrap_err(), 595 ParameterScanError::UnexpectedCommandNameCount(2, 3) 596 )); 597 }