/ thirdparty / hyperfine / src / command.rs
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  }