mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-10-12 02:22:35 +00:00
Add --env for benchmark environment overrides
This commit is contained in:
@@ -155,6 +155,31 @@ Otherwise, inline the function into the benchmarked command:
|
||||
hyperfine -S 'my_function() { sleep 1; }; my_function'
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
Use `--env` to set environment variables for benchmark commands and their respective setup, prepare, conclude,
|
||||
and cleanup commands. Assignments are space-separated and support shell-style quoting,
|
||||
but no shell expansion:
|
||||
|
||||
```sh
|
||||
hyperfine --env 'OMP_NUM_THREADS=8 CFLAGS="-O3 -march=native"' 'my_command'
|
||||
```
|
||||
|
||||
Specify once for all commands or once per benchmark command:
|
||||
|
||||
```sh
|
||||
hyperfine \
|
||||
--env 'OMP_NUM_THREADS=8' './benchmark-cpp' \
|
||||
--env 'RAYON_NUM_THREADS=8' './benchmark-rust'
|
||||
```
|
||||
|
||||
Use an empty `--env=''` for no overrides. Values can contain parameter placeholders.
|
||||
For example, the following benchmarks `my_command` with `OMP_NUM_THREADS` ranging from 1 to 8:
|
||||
|
||||
```sh
|
||||
hyperfine -P threads 1 8 --env 'OMP_NUM_THREADS={threads}' 'my_command'
|
||||
```
|
||||
|
||||
### Choosing metrics and units
|
||||
|
||||
By default, hyperfine displays wall-clock time and memory usage
|
||||
|
||||
@@ -192,6 +192,17 @@ This performs benchmarks for 'g++ \-O2 main.cpp' and 'clang++ \-O2 main.cpp'.
|
||||
The option can be specified multiple times to run benchmarks for all
|
||||
possible parameter combinations.
|
||||
.HP
|
||||
\fB\-\-env\fR \fIASSIGNMENTS\fP
|
||||
.IP
|
||||
Set environment variables for benchmark commands and their respective setup, prepare, conclude,
|
||||
and cleanup commands. Assignments are space-separated and support shell-style quoting,
|
||||
but no shell expansion, e.g. \fB\-\-env\fR 'OMP_NUM_THREADS=8 CFLAGS="\-O3 \-march=native"'.
|
||||
Specify once for all commands or once per benchmark command, like \fB\-\-prepare\fR.
|
||||
Use an empty string for no overrides. Values can contain parameter placeholders.
|
||||
.IP
|
||||
For example, \fBhyperfine \-P threads 1 8 \-\-env\fR 'OMP_NUM_THREADS={threads}' 'my_command'
|
||||
benchmarks 'my_command' with OMP_NUM_THREADS ranging from 1 to 8.
|
||||
.HP
|
||||
\fB\-\-shell\fR \fISHELL\fP
|
||||
.IP
|
||||
Set the shell to use for executing benchmarked commands, including warmup runs
|
||||
|
||||
@@ -18,15 +18,13 @@ pub struct BenchmarkResult {
|
||||
/// regardless of any custom name.
|
||||
pub command: String,
|
||||
|
||||
/// The custom name after parameter substitution, if it differs from `command`.
|
||||
/// For example, `--command-name="wait {duration}s"` with `duration=1`
|
||||
/// produces `Some("wait 1s")`. Without a custom name, or when the expanded
|
||||
/// name equals `command`, this is `None`.
|
||||
/// The custom or automatically generated name after parameter substitution,
|
||||
/// if it differs from `command`. Automatic names include environment overrides.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The custom name (or `command`), followed by parameters not used in the
|
||||
/// command template, in command-line order. For example, `sleep {duration}`
|
||||
/// The name, followed by parameters not used in the command or environment
|
||||
/// templates, in command-line order. For example, `sleep {duration}`
|
||||
/// with `duration=1`, `branch=main`, and the custom name `wait {duration}s`
|
||||
/// produces `wait 1s (branch = main)`. Without the custom name, this is
|
||||
/// `sleep 1 (branch = main)`. Used in summaries and table exports.
|
||||
@@ -40,6 +38,10 @@ pub struct BenchmarkResult {
|
||||
/// Parameter values for this benchmark
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub parameters: BTreeMap<String, Parameter>,
|
||||
|
||||
/// Explicit environment overrides after parameter substitution.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub environment: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl BenchmarkResult {
|
||||
|
||||
@@ -215,6 +215,7 @@ impl Executor for ShellExecutor<'_> {
|
||||
) -> Result<Measurement> {
|
||||
let on_windows_cmd = cfg!(windows) && *self.shell == Shell::Default("cmd.exe");
|
||||
let mut command_builder = self.shell.command();
|
||||
command_builder.envs(command.get_environment());
|
||||
command_builder.arg(if on_windows_cmd { "/C" } else { "-c" });
|
||||
|
||||
// Windows needs special treatment for its behavior on parsing cmd arguments
|
||||
|
||||
@@ -61,8 +61,9 @@ impl<'a> Benchmark<'a> {
|
||||
output_policy: &CommandOutputPolicy,
|
||||
iteration: executor::BenchmarkIteration,
|
||||
) -> Result<Measurement> {
|
||||
let command = command.clone().with_environment_from(self.command);
|
||||
self.executor
|
||||
.run_intermediate_command_and_measure(command, iteration, output_policy)
|
||||
.run_intermediate_command_and_measure(&command, iteration, output_policy)
|
||||
.map_err(|error| {
|
||||
let hint = if cfg!(windows) {
|
||||
"Append ' || exit /b 0' to the command if this failure can be ignored."
|
||||
@@ -443,6 +444,11 @@ impl<'a> Benchmark<'a> {
|
||||
Ok(BenchmarkResult {
|
||||
command,
|
||||
name,
|
||||
environment: self
|
||||
.command
|
||||
.get_environment()
|
||||
.map(|(name, value)| (name.to_owned(), value))
|
||||
.collect(),
|
||||
display_name: self.command.get_name_with_unused_parameters(),
|
||||
measurements,
|
||||
parameters: self
|
||||
|
||||
+22
@@ -119,6 +119,28 @@ fn build_command() -> Command {
|
||||
.help("Perform exactly NUM runs for each command. If this option is not specified, \
|
||||
hyperfine automatically determines the number of runs."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("env")
|
||||
.long("env")
|
||||
.action(ArgAction::Append)
|
||||
.num_args(1)
|
||||
.value_name("ASSIGNMENTS")
|
||||
.help("Set environment variables for benchmark commands and their respective setup, \
|
||||
prepare, conclude, and cleanup commands. Assignments are space-separated and \
|
||||
support shell-style quoting, but no shell expansion:\n\n \
|
||||
hyperfine \\\n \
|
||||
--env 'OMP_NUM_THREADS=8 CFLAGS=\"-O3 -march=native\"' \\\n \
|
||||
'my_command'\n\n\
|
||||
Specify once for all commands or once per benchmark command:\n\n \
|
||||
hyperfine \\\n \
|
||||
--env 'OMP_NUM_THREADS=8' './benchmark-cpp' \\\n \
|
||||
--env 'RAYON_NUM_THREADS=8' './benchmark-rust'\n\n\
|
||||
Use an empty --env='' for no overrides. Values can contain parameter placeholders. \
|
||||
For example, the following benchmarks 'my_command' with OMP_NUM_THREADS ranging \
|
||||
from 1 to 8:\n\n \
|
||||
hyperfine -P threads 1 8 \\\n \
|
||||
--env 'OMP_NUM_THREADS={threads}' 'my_command'"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("setup")
|
||||
.long("setup")
|
||||
|
||||
+91
-6
@@ -14,9 +14,11 @@ use crate::{
|
||||
|
||||
use clap::{parser::ValuesRef, ArgMatches};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
type Environment = Vec<(String, String)>;
|
||||
|
||||
/// A command that should be benchmarked.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Command<'a> {
|
||||
@@ -28,6 +30,9 @@ pub struct Command<'a> {
|
||||
|
||||
/// Zero or more parameter values.
|
||||
parameters: Vec<ParameterNameAndValue<'a>>,
|
||||
|
||||
/// Environment overrides, before parameter substitution.
|
||||
environment: Environment,
|
||||
}
|
||||
|
||||
impl<'a> Command<'a> {
|
||||
@@ -36,6 +41,7 @@ impl<'a> Command<'a> {
|
||||
name,
|
||||
expression,
|
||||
parameters: Vec::new(),
|
||||
environment: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +54,26 @@ impl<'a> Command<'a> {
|
||||
name,
|
||||
expression,
|
||||
parameters: parameters.into_iter().collect(),
|
||||
environment: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> String {
|
||||
self.name.map_or_else(
|
||||
|| self.get_command_line(),
|
||||
|name| self.replace_parameters_in(name),
|
||||
)
|
||||
match self.name {
|
||||
Some(name) => self.replace_parameters_in(name),
|
||||
None => {
|
||||
let environment = self
|
||||
.get_environment()
|
||||
.map(|(name, value)| format!("{name}={}", shell_words::quote(&value)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if environment.is_empty() {
|
||||
self.get_command_line()
|
||||
} else {
|
||||
format!("{environment} {}", self.get_command_line())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name_with_unused_parameters(&self) -> String {
|
||||
@@ -87,6 +105,7 @@ impl<'a> Command<'a> {
|
||||
if let Some(program_name) = tokens.next() {
|
||||
let mut command_builder = std::process::Command::new(program_name);
|
||||
command_builder.args(tokens);
|
||||
command_builder.envs(self.get_environment());
|
||||
Ok(command_builder)
|
||||
} else {
|
||||
bail!("Can not execute empty command")
|
||||
@@ -97,8 +116,24 @@ impl<'a> Command<'a> {
|
||||
&self.parameters
|
||||
}
|
||||
|
||||
pub fn get_environment(&self) -> impl Iterator<Item = (&str, String)> {
|
||||
self.environment
|
||||
.iter()
|
||||
.map(move |(name, value)| (name.as_str(), self.replace_parameters_in(value)))
|
||||
}
|
||||
|
||||
pub fn with_environment_from(mut self, command: &Command<'_>) -> Self {
|
||||
self.environment = command.environment.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_parameter_unused(&self, parameter: &str) -> bool {
|
||||
!self.expression.contains(&format!("{{{parameter}}}"))
|
||||
let placeholder = format!("{{{parameter}}}");
|
||||
!self.expression.contains(&placeholder)
|
||||
&& !self
|
||||
.environment
|
||||
.iter()
|
||||
.any(|(_, value)| value.contains(&placeholder))
|
||||
}
|
||||
|
||||
pub fn get_unused_parameters(&self) -> impl Iterator<Item = &(&'a str, ParameterValue)> {
|
||||
@@ -137,8 +172,58 @@ impl<'a> Command<'a> {
|
||||
/// A collection of commands that should be benchmarked
|
||||
pub struct Commands<'a>(Vec<Command<'a>>);
|
||||
|
||||
fn parse_environment(value: &str) -> Result<Environment> {
|
||||
let mut environment = Environment::new();
|
||||
for assignment in shell_words::split(value).context("Failed to parse '--env' assignments")? {
|
||||
let (name, value) = assignment.split_once('=').with_context(|| {
|
||||
format!(
|
||||
"Invalid value '{assignment}' for '--env': expected an assignment like NAME=value or NAME=\"quoted value\"."
|
||||
)
|
||||
})?;
|
||||
ensure!(
|
||||
!name.is_empty()
|
||||
&& name.bytes().enumerate().all(|(index, byte)| {
|
||||
byte == b'_'
|
||||
|| byte.is_ascii_alphabetic()
|
||||
|| (index > 0 && byte.is_ascii_digit())
|
||||
}),
|
||||
"Invalid environment variable name '{name}' in '--env'"
|
||||
);
|
||||
// Keep only the last assignment, including case variants on Windows.
|
||||
environment.retain(|(key, _)| {
|
||||
if cfg!(windows) {
|
||||
!key.eq_ignore_ascii_case(name)
|
||||
} else {
|
||||
key != name
|
||||
}
|
||||
});
|
||||
environment.push((name.to_owned(), value.to_owned()));
|
||||
}
|
||||
Ok(environment)
|
||||
}
|
||||
|
||||
impl<'a> Commands<'a> {
|
||||
pub fn from_cli_arguments(matches: &'a ArgMatches) -> Result<Commands<'a>> {
|
||||
let mut commands = Self::build_from_cli_arguments(matches)?;
|
||||
if let Some(values) = matches.get_many::<String>("env") {
|
||||
let environments = values
|
||||
.map(|value| parse_environment(value))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let num_commands = commands.num_commands();
|
||||
ensure!(
|
||||
environments.len() == 1 || environments.len() == num_commands,
|
||||
"The '--env' option has to be provided just once or N times, where N={num_commands} is the \
|
||||
number of benchmark commands."
|
||||
);
|
||||
for (index, command) in commands.0.iter_mut().enumerate() {
|
||||
command.environment =
|
||||
environments[if environments.len() == 1 { 0 } else { index }].clone();
|
||||
}
|
||||
}
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
fn build_from_cli_arguments(matches: &'a ArgMatches) -> Result<Commands<'a>> {
|
||||
let command_names = matches.get_many::<String>("command-name");
|
||||
let command_strings = matches
|
||||
.get_many::<String>("command")
|
||||
|
||||
@@ -91,6 +91,7 @@ fn test_csv() {
|
||||
|
||||
let results = vec![
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("echo command_a"),
|
||||
name: Some(String::from("command_a")),
|
||||
display_name: String::from("command_a"),
|
||||
@@ -141,6 +142,7 @@ fn test_csv() {
|
||||
},
|
||||
},
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("command_b"),
|
||||
name: None,
|
||||
display_name: String::from("command_b"),
|
||||
|
||||
@@ -35,6 +35,7 @@ fn get_output<E: Exporter + Default>(results: &[BenchmarkResult], unit: Option<U
|
||||
fn test_markup_export_auto_ms() {
|
||||
let results = [
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 0.1"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 0.1"),
|
||||
@@ -70,6 +71,7 @@ fn test_markup_export_auto_ms() {
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 2"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 2"),
|
||||
@@ -147,6 +149,7 @@ fn test_markup_export_auto_ms() {
|
||||
fn test_markup_export_auto_s() {
|
||||
let results = [
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 2"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 2"),
|
||||
@@ -182,6 +185,7 @@ fn test_markup_export_auto_s() {
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 0.1"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 0.1"),
|
||||
@@ -259,6 +263,7 @@ fn test_markup_export_auto_s() {
|
||||
fn test_markup_export_manual_ms() {
|
||||
let timing_results = [
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 2"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 2"),
|
||||
@@ -294,6 +299,7 @@ fn test_markup_export_manual_ms() {
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 0.1"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 0.1"),
|
||||
@@ -370,6 +376,7 @@ fn test_markup_export_manual_ms() {
|
||||
fn test_markup_export_manual_s() {
|
||||
let results = [
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 2"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 2"),
|
||||
@@ -405,6 +412,7 @@ fn test_markup_export_manual_s() {
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
BenchmarkResult {
|
||||
environment: BTreeMap::new(),
|
||||
command: String::from("sleep 0.1"),
|
||||
name: None,
|
||||
display_name: String::from("sleep 0.1"),
|
||||
|
||||
@@ -47,6 +47,14 @@ fn run() -> Result<()> {
|
||||
{
|
||||
for command in commands.iter() {
|
||||
let command_line = command.get_command_line();
|
||||
if shell_syntax::starts_with_assignment(&command_line) {
|
||||
bail!(
|
||||
"Cannot run '{command_line}' directly because it starts with a variable assignment.\n\
|
||||
Use the --env option to set environment variables, for example:\n\n \
|
||||
hyperfine --env 'OMP_NUM_THREADS=8 CFLAGS=\"-O3 -march=native\"' 'my_command'\n\n\
|
||||
Alternatively, use -S / --shell=default to run the command in a shell."
|
||||
);
|
||||
}
|
||||
if let Some(operator) = shell_syntax::first_unquoted_operator(&command_line) {
|
||||
bail!(
|
||||
concat!(
|
||||
|
||||
+49
-2
@@ -1,8 +1,32 @@
|
||||
//! Conservative detection of shell operators in commands run without a shell.
|
||||
//! Conservative detection of shell syntax in commands run without a shell.
|
||||
|
||||
use std::iter::Peekable;
|
||||
use std::str::CharIndices;
|
||||
|
||||
/// Detects the variable assignment at the start of a command like
|
||||
/// `OMP_NUM_THREADS=8 my_command`.
|
||||
pub fn starts_with_assignment(command: &str) -> bool {
|
||||
let Some(token) = tokenize(command).next() else {
|
||||
return false;
|
||||
};
|
||||
let mut bytes = token.bytes();
|
||||
if !bytes
|
||||
.next()
|
||||
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for byte in bytes {
|
||||
if byte == b'=' {
|
||||
return true;
|
||||
}
|
||||
if !byte.is_ascii_alphanumeric() && byte != b'_' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find the first standalone operator that is neither quoted nor escaped.
|
||||
///
|
||||
/// Operators attached to another word (such as `hello>out`), expansions, and
|
||||
@@ -90,7 +114,30 @@ impl<'a> Iterator for Tokens<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{first_unquoted_operator, tokenize};
|
||||
use super::{first_unquoted_operator, starts_with_assignment, tokenize};
|
||||
|
||||
#[test]
|
||||
fn detects_leading_assignments() {
|
||||
assert!(starts_with_assignment("A=1 command"));
|
||||
assert!(starts_with_assignment("A=$HOME command"));
|
||||
assert!(starts_with_assignment("A= command"));
|
||||
assert!(starts_with_assignment("_A2='hello world' B=2 command"));
|
||||
assert!(starts_with_assignment("A=1"));
|
||||
assert!(starts_with_assignment(" # comment\n A=1 command"));
|
||||
|
||||
assert!(!starts_with_assignment(""));
|
||||
assert!(!starts_with_assignment("# A=1 command"));
|
||||
assert!(!starts_with_assignment("command A=1"));
|
||||
assert!(!starts_with_assignment("env A=1 command"));
|
||||
assert!(!starts_with_assignment("'A=1' command"));
|
||||
assert!(!starts_with_assignment("\"A\"=1 command"));
|
||||
assert!(!starts_with_assignment(r"A\=1 command"));
|
||||
assert!(!starts_with_assignment("./A=1 command"));
|
||||
assert!(!starts_with_assignment("1A=1 command"));
|
||||
assert!(!starts_with_assignment("A-B=1 command"));
|
||||
assert!(!starts_with_assignment("=1 command"));
|
||||
assert!(!starts_with_assignment("A"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokenizer_preserves_words_quotes_and_escapes() {
|
||||
|
||||
@@ -53,6 +53,66 @@ fn one_run_is_supported() {
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_basic_usage() {
|
||||
let command = if cfg!(windows) {
|
||||
"echo %HYPERFINE_TEST%"
|
||||
} else {
|
||||
"echo \"$HYPERFINE_TEST\""
|
||||
};
|
||||
hyperfine()
|
||||
.args(["--shell=default", "--runs=1", "--show-output"])
|
||||
.arg("--command-name=env-test")
|
||||
.args(["--env", r#"HYPERFINE_TEST="hello, world""#])
|
||||
.arg(command)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("hello, world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_parameter_expansion() {
|
||||
let command = if cfg!(windows) {
|
||||
"echo %HYPERFINE_TEST%"
|
||||
} else {
|
||||
"echo \"$HYPERFINE_TEST\""
|
||||
};
|
||||
hyperfine()
|
||||
.args(["--shell=default", "--runs=1", "--show-output"])
|
||||
.arg("--command-name=env-test")
|
||||
.args(["-P", "value", "1", "2"])
|
||||
.args(["--env", "HYPERFINE_TEST=hello-{value}"])
|
||||
.arg(command)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("hello-1"))
|
||||
.stdout(predicate::str::contains("hello-2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_environment() {
|
||||
hyperfine()
|
||||
.args(["--env", "OMP_NUM_THREADS"])
|
||||
.arg("my_command")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"Invalid value 'OMP_NUM_THREADS' for '--env': expected an assignment like NAME=value or NAME=\"quoted value\".",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_explicit_shell_for_environment_assignments() {
|
||||
// Without an explicit shell choice, report the leading variable assignment.
|
||||
hyperfine()
|
||||
.args(["--runs=1", "SOME_ENV_VAR=$HOME my_command"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"starts with a variable assignment",
|
||||
));
|
||||
}
|
||||
|
||||
/// Regression test: hyperfine must not panic when writing to a closed
|
||||
/// stdout pipe (e.g. `hyperfine ... | head -n 1` or a downstream process
|
||||
/// that terminates early). A `BrokenPipe` error should result in a quiet
|
||||
|
||||
Reference in New Issue
Block a user