Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --input argument #563

Merged
merged 11 commits into from
Mar 15, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- Added new `--input` option, see #541 and #563 (@snease)

## Changes

Expand Down
12 changes: 9 additions & 3 deletions src/benchmark/executor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::process::{ExitStatus, Stdio};
use std::process::ExitStatus;

use crate::command::Command;
use crate::options::{CmdFailureAction, CommandOutputPolicy, Options, OutputStyleOption, Shell};
use crate::options::{
CmdFailureAction, CommandInputPolicy, CommandOutputPolicy, Options, OutputStyleOption, Shell,
};
use crate::output::progress_bar::get_progress_bar;
use crate::timer::{execute_and_measure, TimerResult};
use crate::util::randomized_environment_offset;
Expand Down Expand Up @@ -36,11 +38,13 @@ pub trait Executor {
fn run_command_and_measure_common(
mut command: std::process::Command,
command_failure_action: CmdFailureAction,
command_input_policy: &CommandInputPolicy,
command_output_policy: &CommandOutputPolicy,
command_name: &str,
) -> Result<TimerResult> {
let stdin = command_input_policy.get_stdin()?;
sharkdp marked this conversation as resolved.
Show resolved Hide resolved
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
command.stdin(Stdio::null()).stdout(stdout).stderr(stderr);
command.stdin(stdin).stdout(stdout).stderr(stderr);

command.env(
"HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET",
Expand Down Expand Up @@ -83,6 +87,7 @@ impl<'a> Executor for RawExecutor<'a> {
let result = run_command_and_measure_common(
command.get_command()?,
command_failure_action.unwrap_or(self.options.command_failure_action),
&self.options.command_input_policy,
&self.options.command_output_policy,
&command.get_command_line(),
)?;
Expand Down Expand Up @@ -142,6 +147,7 @@ impl<'a> Executor for ShellExecutor<'a> {
let mut result = run_command_and_measure_common(
command_builder,
command_failure_action.unwrap_or(self.options.command_failure_action),
&self.options.command_input_policy,
&self.options.command_output_policy,
&command.get_command_line(),
)?;
Expand Down
36 changes: 28 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,16 +274,36 @@ fn build_command() -> Command {
.action(ArgAction::Set)
.value_name("WHERE")
.help(
"Control where the output of the benchmark is redirected. <WHERE> can be:\n\n\
null: Redirect output to /dev/null (the default). \
Note that some programs like 'grep' detect when standard output is /dev/null \
and apply certain optimizations. To avoid that, consider using \
'--output=pipe'.\n\n\
pipe: Feed the output through a pipe before discarding it.\n\n\
inherit: Don't redirect the output at all (same as '--show-output').\n\n\
<FILE>: Write the output to the given file.",
"Control where the output of the benchmark is redirected. Note \
that some programs like 'grep' detect when standard output is \
/dev/null and apply certain optimizations. To avoid that, consider \
using '--output=pipe'.\n\
\n\
<WHERE> can be:\n\
\n \
null: Redirect output to /dev/null (the default).\n\
\n \
pipe: Feed the output through a pipe before discarding it.\n\
\n \
inherit: Don't redirect the output at all (same as '--show-output').\n\
\n \
<FILE>: Write the output to the given file.",
),
)
.arg(
Arg::new("input")
.long("input")
.action(ArgAction::Set)
.number_of_values(1)
.value_name("WHERE")
.help("Control where the input of the benchmark comes from.\n\
\n\
<WHERE> can be:\n\
\n \
null: Read from /dev/null (the default).\n\
\n \
<FILE>: Read the input from the given file."),
)
.arg(
Arg::new("command-name")
.long("command-name")
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ pub enum OptionsError<'a> {
ShellParseError(shell_words::ParseError),
#[error("Unknown output policy '{0}'. Use './{0}' to output to a file named '{0}'.")]
UnknownOutputPolicy(String),
#[error("The file '{0}' specified as '--input' does not exist")]
StdinDataFileDoesNotExist(String),
}
48 changes: 47 additions & 1 deletion src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,36 @@ impl Default for RunBounds {
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum CommandInputPolicy {
/// Read from the null device
Null,

/// Read input from a file
File(PathBuf),
}

impl Default for CommandInputPolicy {
fn default() -> Self {
CommandInputPolicy::Null
}
}

impl CommandInputPolicy {
pub fn get_stdin(&self) -> io::Result<Stdio> {
let stream: Stdio = match self {
CommandInputPolicy::Null => Stdio::null(),

CommandInputPolicy::File(path) => {
let file: File = File::open(&path)?;
Stdio::from(file)
}
};

Ok(stream)
}
}

/// How to handle the output of benchmarked commands
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandOutputPolicy {
Expand Down Expand Up @@ -193,10 +223,13 @@ pub struct Options {
/// Determines how we run commands
pub executor_kind: ExecutorKind,

/// Where input to the benchmarked command comes from
pub command_input_policy: CommandInputPolicy,

/// What to do with the output of the benchmarked command
pub command_output_policy: CommandOutputPolicy,

/// Which time unit to use when displaying resuls
/// Which time unit to use when displaying results
pub time_unit: Option<Unit>,
}

Expand All @@ -214,6 +247,7 @@ impl Default for Options {
executor_kind: ExecutorKind::default(),
command_output_policy: CommandOutputPolicy::Null,
time_unit: None,
command_input_policy: CommandInputPolicy::Null,
}
}
}
Expand Down Expand Up @@ -354,6 +388,18 @@ impl Options {
.map_err(|e| OptionsError::FloatParsingError("min-benchmarking-time", e))?;
}

options.command_input_policy = if let Some(path_str) = matches.get_one::<String>("input") {
let path = PathBuf::from(path_str);
if !path.exists() {
return Err(OptionsError::StdinDataFileDoesNotExist(
path_str.to_string(),
));
}
CommandInputPolicy::File(path)
} else {
CommandInputPolicy::Null
};

Ok(options)
}

Expand Down
1 change: 1 addition & 0 deletions tests/example_input_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This text is part of a file
26 changes: 26 additions & 0 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,32 @@ fn runs_commands_using_user_defined_shell() {
);
}

#[test]
fn can_pass_input_to_command_from_a_file() {
hyperfine()
.arg("--runs=1")
.arg("--input=example_input_file.txt")
.arg("--show-output")
.arg("cat")
.assert()
.success()
.stdout(predicate::str::contains("This text is part of a file"));
}

#[test]
fn fails_if_invalid_stdin_data_file_provided() {
hyperfine()
.arg("--runs=1")
.arg("--input=example_non_existent_file.txt")
.arg("--show-output")
.arg("cat")
.assert()
.failure()
.stderr(predicate::str::contains(
"The file 'example_non_existent_file.txt' specified as '--input' does not exist",
));
}

#[test]
fn returns_mean_time_in_correct_unit() {
hyperfine_debug()
Expand Down