Merge pull request #979 from sharkdp/fix/parameterized-reference

Use parameterized benchmarks as references
This commit is contained in:
David Peter authored and GitHub committed 2026-10-05 15:55:12 +02:00
commit 46e1f9f2c1
12 files changed
+390 -92

No files matched your search

+6 -2
View File
@@ -116,13 +116,17 @@ not every time as would happen with the \fB\-\-prepare\fR option.
.HP .HP
\fB\-\-reference\fR \fICMD\fP \fB\-\-reference\fR \fICMD\fP
.IP .IP
Benchmark \fICMD\fP as the reference for the relative comparison of results. Use \fICMD\fP as the reference for the relative comparison of results. Without
If this option is not specified, the fastest command is used as the reference. parameters, benchmark \fICMD\fP as a separate reference command. If this option
is not specified, the fastest command is used as the reference.
With \fB\-\-parameter\-scan\fR or \fB\-\-parameter\-list\fR, select an existing
benchmark by its exact printed name. The name must match exactly one benchmark.
.HP .HP
\fB\-\-reference\-name\fR \fINAME\fP \fB\-\-reference\-name\fR \fINAME\fP
.IP .IP
Give a meaningful \fINAME\fP to the reference command. Give a meaningful \fINAME\fP to the reference command.
Requires \fB\-\-reference\fR. Requires \fB\-\-reference\fR.
Cannot be used with parameterized benchmarks; use \fB\-\-command\-name\fR instead.
.HP .HP
\fB\-p\fR, \fB\-\-prepare\fR \fICMD...\fP \fB\-p\fR, \fB\-\-prepare\fR \fICMD...\fP
.IP .IP
+53 -11
View File
@@ -16,6 +16,20 @@ pub struct BenchmarkResultWithRelativeSpeed<'a> {
pub relative_ordering: Ordering, pub relative_ordering: Ordering,
} }
impl BenchmarkResultWithRelativeSpeed<'_> {
pub fn reference_label(&self) -> &'static str {
if self.is_reference {
" (reference)"
} else {
match self.relative_ordering {
Ordering::Less => " (faster)",
Ordering::Equal => " (same speed)",
Ordering::Greater => " (slower)",
}
}
}
}
pub fn compare_mean_time(l: &BenchmarkResult, r: &BenchmarkResult) -> Ordering { pub fn compare_mean_time(l: &BenchmarkResult, r: &BenchmarkResult) -> Ordering {
l.mean_wall_clock_time() l.mean_wall_clock_time()
.partial_cmp(&r.mean_wall_clock_time()) .partial_cmp(&r.mean_wall_clock_time())
@@ -37,10 +51,13 @@ fn compute_relative_speeds<'a>(
let mut results: Vec<_> = results let mut results: Vec<_> = results
.iter() .iter()
.map(|result| { .map(|result| {
let is_reference = result == reference; // Separate benchmarks can have identical names and measurements.
let is_reference = std::ptr::eq(result, reference);
let relative_ordering = compare_mean_time(result, reference); let relative_ordering = compare_mean_time(result, reference);
if result.mean_wall_clock_time() == Time::zero() { if result.mean_wall_clock_time() == Time::zero()
|| reference.mean_wall_clock_time() == Time::zero()
{
return BenchmarkResultWithRelativeSpeed { return BenchmarkResultWithRelativeSpeed {
result, result,
relative_speed: if is_reference { 1.0 } else { f64::INFINITY }, relative_speed: if is_reference { 1.0 } else { f64::INFINITY },
@@ -64,11 +81,11 @@ fn compute_relative_speeds<'a>(
result.measurements.stddev(), result.measurements.stddev(),
reference.measurements.stddev(), reference.measurements.stddev(),
) { ) {
(Some(result_stddev), Some(fastest_stddev)) => Some( (Some(result_stddev), Some(reference_stddev)) => Some(
ratio ratio
* ((result_stddev / result.mean_wall_clock_time()) * ((result_stddev / result.mean_wall_clock_time())
.powi(uom::typenum::P2::new()) .powi(uom::typenum::P2::new())
+ (fastest_stddev / reference.mean_wall_clock_time()) + (reference_stddev / reference.mean_wall_clock_time())
.powi(uom::typenum::P2::new())) .powi(uom::typenum::P2::new()))
.sqrt(), .sqrt(),
), ),
@@ -122,14 +139,14 @@ pub fn compute_with_check(
Some(compute_relative_speeds(results, fastest, sort_order)) Some(compute_relative_speeds(results, fastest, sort_order))
} }
/// Same as compute_with_check, potentially resulting in relative speeds of infinity /// Compute relative speeds against the given reference, or the fastest result.
pub fn compute( pub fn compute<'a>(
results: &[BenchmarkResult], results: &'a [BenchmarkResult],
sort_order: SortOrder, sort_order: SortOrder,
) -> Vec<BenchmarkResultWithRelativeSpeed<'_>> { reference: Option<&'a BenchmarkResult>,
let fastest = fastest_of(results); ) -> Vec<BenchmarkResultWithRelativeSpeed<'a>> {
let reference = reference.unwrap_or_else(|| fastest_of(results));
compute_relative_speeds(results, fastest, sort_order) compute_relative_speeds(results, reference, sort_order)
} }
#[cfg(test)] #[cfg(test)]
@@ -192,3 +209,28 @@ fn test_compute_relative_speed_for_zero_times() {
assert!(annotated_results.is_none()); assert!(annotated_results.is_none());
} }
#[test]
fn reference_identity_distinguishes_equal_results() {
let results = vec![create_result("same", 1.0), create_result("same", 1.0)];
let entries = compute_relative_speeds(&results, &results[0], SortOrder::Command);
assert!(entries[0].is_reference);
assert!(!entries[1].is_reference);
}
#[test]
fn reference_ratios_handle_zero_times() {
let mut results = vec![create_result("reference", 2.0), create_result("zero", 0.0)];
for result in &mut results {
let measurement = result.measurements.measurements[0];
result.measurements.measurements.push(measurement);
}
let entries = compute_relative_speeds(&results, &results[0], SortOrder::Command);
assert_eq!(entries[1].relative_speed, f64::INFINITY);
let entries = compute_relative_speeds(&results, &results[1], SortOrder::Command);
assert_eq!(entries[0].relative_speed, f64::INFINITY);
assert_eq!(entries[0].relative_speed_stddev, None);
assert_eq!(entries[1].relative_speed, 1.0);
}
+13 -6
View File
@@ -54,7 +54,8 @@ impl<'a> Scheduler<'a> {
// We export results after each individual benchmark, because // We export results after each individual benchmark, because
// we would risk losing them if a later benchmark fails. // we would risk losing them if a later benchmark fails.
self.export_manager.write_results(&self.results, true)?; self.export_manager
.write_results(&self.results, true, self.options.reference_index)?;
} }
Ok(()) Ok(())
@@ -71,9 +72,8 @@ impl<'a> Scheduler<'a> {
let reference = self let reference = self
.options .options
.reference_command .reference_index
.as_ref() .map(|index| &self.results[index])
.map(|_| &self.results[0])
.unwrap_or_else(|| relative_speed::fastest_of(&self.results)); .unwrap_or_else(|| relative_speed::fastest_of(&self.results));
if let Some(annotated_results) = relative_speed::compute_with_check_from_reference( if let Some(annotated_results) = relative_speed::compute_with_check_from_reference(
@@ -130,9 +130,14 @@ impl<'a> Scheduler<'a> {
console_writeln!(stdout, "{}", "Relative speed comparison".bold())?; console_writeln!(stdout, "{}", "Relative speed comparison".bold())?;
for item in annotated_results { for item in annotated_results {
let relationship = if self.options.reference_index.is_some() {
item.reference_label()
} else {
""
};
console_writeln!( console_writeln!(
stdout, stdout,
" {}{} {}", " {}{} {}{}",
format!("{:10.2}", item.relative_speed).bold().green(), format!("{:10.2}", item.relative_speed).bold().green(),
if item.is_reference { if item.is_reference {
" ".into() " ".into()
@@ -142,6 +147,7 @@ impl<'a> Scheduler<'a> {
" ".into() " ".into()
}, },
item.result.command_with_unused_parameters, item.result.command_with_unused_parameters,
relationship,
)?; )?;
} }
} }
@@ -163,7 +169,8 @@ impl<'a> Scheduler<'a> {
} }
pub fn final_export(&self) -> Result<()> { pub fn final_export(&self) -> Result<()> {
self.export_manager.write_results(&self.results, false) self.export_manager
.write_results(&self.results, false, self.options.reference_index)
} }
} }
+7 -3
View File
@@ -105,8 +105,11 @@ fn build_command() -> Command {
.action(ArgAction::Set) .action(ArgAction::Set)
.value_name("CMD") .value_name("CMD")
.help( .help(
"The reference command for the relative comparison of results. \ "The reference for the relative comparison of results. Without parameters, \
If this is unset, results are compared with the fastest command as reference." CMD is run as a separate reference command. With --parameter-scan or \
--parameter-list, CMD must exactly match one unique benchmark name as \
shown in the output. If this is unset, results are compared with the \
fastest command as reference."
) )
) )
.arg( .arg(
@@ -114,7 +117,8 @@ fn build_command() -> Command {
.long("reference-name") .long("reference-name")
.action(ArgAction::Set) .action(ArgAction::Set)
.value_name("CMD") .value_name("CMD")
.help("Give a meaningful name to the reference command.") .help("Give a meaningful name to the reference command. This cannot be used \
with parameterized benchmarks; use --command-name instead.")
.requires("reference") .requires("reference")
) )
.arg( .arg(
+2 -1
View File
@@ -18,6 +18,7 @@ impl Exporter for CsvExporter {
results: &[BenchmarkResult], results: &[BenchmarkResult],
_unit: Option<TimeUnit>, _unit: Option<TimeUnit>,
_sort_order: SortOrder, _sort_order: SortOrder,
_reference_index: Option<usize>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
const CSV_UNIT: TimeUnit = TimeUnit::Second; const CSV_UNIT: TimeUnit = TimeUnit::Second;
const CSV_PRECISION: usize = 6; const CSV_PRECISION: usize = 6;
@@ -172,7 +173,7 @@ fn test_csv() {
let actual = String::from_utf8( let actual = String::from_utf8(
exporter exporter
.serialize(&results, Some(TimeUnit::Second), SortOrder::Command) .serialize(&results, Some(TimeUnit::Second), SortOrder::Command, None)
.unwrap(), .unwrap(),
) )
.unwrap(); .unwrap();
+1
View File
@@ -22,6 +22,7 @@ impl Exporter for JsonExporter {
results: &[BenchmarkResult], results: &[BenchmarkResult],
_unit: Option<TimeUnit>, _unit: Option<TimeUnit>,
_sort_order: SortOrder, _sort_order: SortOrder,
_reference_index: Option<usize>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let mut output = to_vec_pretty(&HyperfineSummary { results }); let mut output = to_vec_pretty(&HyperfineSummary { results });
if let Ok(ref mut content) = output { if let Ok(ref mut content) = output {
+34 -10
View File
@@ -1,10 +1,11 @@
use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed; use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed;
use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed}; use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed};
use crate::options::SortOrder; use crate::options::SortOrder;
use crate::quantity::{FormatQuantity, IsUnit, TimeUnit}; use crate::quantity::{FormatQuantity, IsUnit, Time, TimeUnit, Zero};
use super::Exporter; use super::Exporter;
use anyhow::Result; use anyhow::Result;
use std::cmp::Ordering;
pub enum Alignment { pub enum Alignment {
Left, Left,
@@ -16,6 +17,8 @@ pub trait MarkupExporter {
&self, &self,
entries: &[BenchmarkResultWithRelativeSpeed], entries: &[BenchmarkResultWithRelativeSpeed],
unit: TimeUnit, unit: TimeUnit,
reference_pending: bool,
explicit_reference: bool,
) -> String { ) -> String {
// prepare table header strings // prepare table header strings
let notation = format!("[{}]", unit.short_name()); let notation = format!("[{}]", unit.short_name());
@@ -58,13 +61,25 @@ pub trait MarkupExporter {
}; };
let min_str = measurement.measurements.min().format_value(unit); let min_str = measurement.measurements.min().format_value(unit);
let max_str = measurement.measurements.max().format_value(unit); let max_str = measurement.measurements.max().format_value(unit);
let rel_str = format!("{:.2}", entry.relative_speed); // The ratio of two zero times is undefined, even if they compare equal.
let rel_stddev_str = if entry.is_reference { let relative_unavailable = reference_pending
"".into() || (explicit_reference
} else if let Some(stddev) = entry.relative_speed_stddev { && !entry.is_reference
format!(" ± {stddev:.2}") && entry.relative_ordering == Ordering::Equal
&& measurement.mean_wall_clock_time() == Time::zero());
let relative = if relative_unavailable {
"N/A".to_string()
} else { } else {
"".into() let stddev = match (entry.is_reference, entry.relative_speed_stddev) {
(false, Some(stddev)) => format!(" ± {stddev:.2}"),
_ => String::new(),
};
let label = if explicit_reference {
entry.reference_label()
} else {
""
};
format!("{:.2}{stddev}{label}", entry.relative_speed)
}; };
// prepare table row entries // prepare table row entries
@@ -73,7 +88,7 @@ pub trait MarkupExporter {
&format!("{mean_str}{stddev_str}"), &format!("{mean_str}{stddev_str}"),
&min_str, &min_str,
&max_str, &max_str,
&format!("{rel_str}{rel_stddev_str}"), &relative,
])) ]))
} }
@@ -114,11 +129,20 @@ impl<T: MarkupExporter> Exporter for T {
results: &[BenchmarkResult], results: &[BenchmarkResult],
unit: Option<TimeUnit>, unit: Option<TimeUnit>,
sort_order: SortOrder, sort_order: SortOrder,
reference_index: Option<usize>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let unit = unit.unwrap_or_else(|| determine_unit_from_results(results)); let unit = unit.unwrap_or_else(|| determine_unit_from_results(results));
let entries = relative_speed::compute(results, sort_order); // Do not report ratios against another benchmark while the selected
// reference is still pending in an intermediate export.
let reference_pending = reference_index.is_some_and(|i| i >= results.len());
let entries = relative_speed::compute(
results,
sort_order,
reference_index.and_then(|i| results.get(i)),
);
let table = self.table_results(&entries, unit); let table =
self.table_results(&entries, unit, reference_pending, reference_index.is_some());
Ok(table.as_bytes().to_vec()) Ok(table.as_bytes().to_vec())
} }
} }
+10 -2
View File
@@ -51,6 +51,7 @@ trait Exporter {
results: &[BenchmarkResult], results: &[BenchmarkResult],
unit: Option<TimeUnit>, unit: Option<TimeUnit>,
sort_order: SortOrder, sort_order: SortOrder,
reference_index: Option<usize>,
) -> Result<Vec<u8>>; ) -> Result<Vec<u8>>;
} }
@@ -130,11 +131,18 @@ impl ExportManager {
/// results are written to all file targets (to always have them up to date, even /// results are written to all file targets (to always have them up to date, even
/// if a benchmark fails). In the latter case, we only print to stdout targets (in /// if a benchmark fails). In the latter case, we only print to stdout targets (in
/// order not to clutter the output of hyperfine with intermediate results). /// order not to clutter the output of hyperfine with intermediate results).
pub fn write_results(&self, results: &[BenchmarkResult], intermediate: bool) -> Result<()> { /// `reference_index` refers to the complete benchmark sequence, so it can
/// point past `results` while that benchmark is still pending.
pub fn write_results(
&self,
results: &[BenchmarkResult],
intermediate: bool,
reference_index: Option<usize>,
) -> Result<()> {
for e in &self.exporters { for e in &self.exporters {
let content = || { let content = || {
e.exporter e.exporter
.serialize(results, self.time_unit, self.sort_order) .serialize(results, self.time_unit, self.sort_order, reference_index)
}; };
match e.target { match e.target {
+1 -1
View File
@@ -14,7 +14,7 @@ fn get_output<E: Exporter + Default>(
sort_order: SortOrder, sort_order: SortOrder,
) -> String { ) -> String {
let exporter = E::default(); let exporter = E::default();
String::from_utf8(exporter.serialize(results, unit, sort_order).unwrap()).unwrap() String::from_utf8(exporter.serialize(results, unit, sort_order, None).unwrap()).unwrap()
} }
/// Ensure the makrup output includes the table header and the multiple /// Ensure the makrup output includes the table header and the multiple
+35 -2
View File
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{cmp, env, fmt, io}; use std::{cmp, env, fmt, io};
use anyhow::ensure; use anyhow::{bail, ensure};
use clap::ArgMatches; use clap::ArgMatches;
use crate::command::Commands; use crate::command::Commands;
@@ -208,12 +208,15 @@ pub struct Options {
/// Whether or not to ignore non-zero exit codes /// Whether or not to ignore non-zero exit codes
pub command_failure_action: CmdFailureAction, pub command_failure_action: CmdFailureAction,
// Command to use as a reference for relative speed comparison // Standalone reference command, cleared when an existing benchmark is selected.
pub reference_command: Option<String>, pub reference_command: Option<String>,
// Name of the reference command // Name of the reference command
pub reference_name: Option<String>, pub reference_name: Option<String>,
// Index of the reference in the benchmark sequence, if any.
pub reference_index: Option<usize>,
/// Command(s) to run before each timing run /// Command(s) to run before each timing run
pub preparation_command: Option<Vec<String>>, pub preparation_command: Option<Vec<String>>,
@@ -257,6 +260,7 @@ impl Default for Options {
command_failure_action: CmdFailureAction::RaiseError, command_failure_action: CmdFailureAction::RaiseError,
reference_command: None, reference_command: None,
reference_name: None, reference_name: None,
reference_index: None,
preparation_command: None, preparation_command: None,
conclusion_command: None, conclusion_command: None,
setup_command: None, setup_command: None,
@@ -464,6 +468,35 @@ impl Options {
} }
pub fn validate_against_command_list(&mut self, commands: &Commands) -> Result<()> { pub fn validate_against_command_list(&mut self, commands: &Commands) -> Result<()> {
if let Some(reference) = &self.reference_command {
if commands
.iter()
.next()
.is_some_and(|command| !command.get_parameters().is_empty())
{
ensure!(
self.reference_name.is_none(),
"--reference-name cannot be used with a parameterized reference; use --command-name to name the benchmark"
);
let mut matches = commands
.iter()
.enumerate()
.filter(|(_, command)| command.get_name_with_unused_parameters() == *reference);
let Some((index, _)) = matches.next() else {
bail!(
"Reference '{reference}' does not match any parameterized benchmark. Use its full displayed benchmark name."
);
};
ensure!(
matches.next().is_none(),
"Reference '{reference}' matches multiple parameterized benchmarks. Use --command-name to give them unique names."
);
self.reference_index = Some(index);
self.reference_command = None;
} else {
self.reference_index = Some(0);
}
}
let has_reference_command = self.reference_command.is_some(); let has_reference_command = self.reference_command.is_some();
let num_commands = commands.num_commands(has_reference_command); let num_commands = commands.num_commands(has_reference_command);
-20
View File
@@ -383,26 +383,6 @@ fn reference_is_executed_first() {
.run(); .run();
} }
#[test]
fn reference_is_executed_first_parameter_value() {
ExecutionOrderTest::new()
.arg("--runs=2")
.reference("reference")
.arg("--parameter-list")
.arg("number")
.arg("1,2,3")
.command("command {number}")
.expect_output("reference")
.expect_output("reference")
.expect_output("command 1")
.expect_output("command 1")
.expect_output("command 2")
.expect_output("command 2")
.expect_output("command 3")
.expect_output("command 3")
.run();
}
#[test] #[test]
fn reference_is_executed_separately_from_commands() { fn reference_is_executed_separately_from_commands() {
ExecutionOrderTest::new() ExecutionOrderTest::new()
+228 -34
View File
@@ -705,6 +705,68 @@ fn shows_benchmark_comparison_relative_to_reference() {
"); ");
} }
#[test]
fn command_sorted_comparison_and_markup_identify_reference() {
let _settings = snapshot_settings().bind_to_scope();
let directory = tempfile::tempdir().unwrap();
let export_path = directory.path().join("results.md");
let output = hyperfine_debug()
.args([
"--style=basic",
"--runs=1",
"--sort=command",
"--reference",
"sleep 2",
"--reference-name",
"baseline",
"--export-markdown",
])
.arg(&export_path)
.args(["sleep 1", "sleep 2", "sleep 3"])
.output()
.unwrap();
assert!(output.status.success(), "{:?}", output);
let stdout = String::from_utf8(output.stdout).unwrap();
let comparison = stdout.split_once("Relative speed comparison\n").unwrap().1;
insta::assert_snapshot!(comparison, @r"
1.00 baseline (reference)
2.00 sleep 1 (faster)
1.00 sleep 2 (same speed)
1.50 sleep 3 (slower)
");
let markdown = std::fs::read_to_string(export_path).unwrap();
insta::assert_snapshot!(markdown, @r"
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `baseline` | 2.000 | 2.000 | 2.000 | 1.00 (reference) |
| `sleep 1` | 1.000 | 1.000 | 1.000 | 2.00 (faster) |
| `sleep 2` | 2.000 | 2.000 | 2.000 | 1.00 (same speed) |
| `sleep 3` | 3.000 | 3.000 | 3.000 | 1.50 (slower) |
");
// Equal zero times have no meaningful relative factor.
let output = hyperfine_debug()
.args([
"--style=none",
"--runs=1",
"--export-markdown=-",
"--reference",
"sleep 0",
"--reference-name",
"baseline",
"sleep 0",
])
.output()
.unwrap();
assert!(output.status.success(), "{:?}", output);
let markdown = String::from_utf8(output.stdout).unwrap();
let row = markdown
.lines()
.find(|line| line.contains("sleep 0"))
.unwrap();
assert!(row.ends_with("| N/A |"), "{}", row);
}
#[test] #[test]
fn shows_reference_name() { fn shows_reference_name() {
let _settings = snapshot_settings().bind_to_scope(); let _settings = snapshot_settings().bind_to_scope();
@@ -795,48 +857,180 @@ fn rejects_negative_parameter_steps() {
} }
} }
#[cfg(unix)]
#[test] #[test]
fn performs_reference_and_all_benchmarks_in_parameter_scan() { fn selects_reference_with_parameterized_prepare() {
let directory = tempfile::tempdir().unwrap();
let csv_path = directory.path().join("results.csv");
let json_path = directory.path().join("results.json");
let output = hyperfine_raw_command()
.args([
"--style=basic",
"--shell=none",
"--runs=1",
"-L",
"delay",
"0.2,0.4,0.6",
"--prepare",
"sleep {delay}",
"--reference",
"echo a (delay = 0.4)",
"--export-csv",
])
.arg(&csv_path)
.arg("--export-json")
.arg(&json_path)
.arg("echo a")
.output()
.unwrap();
assert!(output.status.success(), "{:?}", output);
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.matches("Benchmark ").count(), 3);
for (number, delay) in ["0.2", "0.4", "0.6"].iter().enumerate() {
assert!(
stdout.contains(&format!(
"Benchmark {}: echo a (delay = {})",
number + 1,
delay
)),
"{}",
stdout
);
}
assert!(stdout.contains("echo a (delay = 0.4) ran"), "{}", stdout);
let mut csv = csv::Reader::from_path(csv_path).unwrap();
assert!(csv
.headers()
.unwrap()
.iter()
.any(|header| header == "parameter_delay"));
assert_eq!(
csv.records().collect::<Result<Vec<_>, _>>().unwrap().len(),
3
);
let json: serde_json::Value =
serde_json::from_slice(&std::fs::read(json_path).unwrap()).unwrap();
let results = json["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert_eq!(
results
.iter()
.map(|result| result["parameters"]["delay"].as_str().unwrap())
.collect::<Vec<_>>(),
["0.2", "0.4", "0.6"]
);
}
#[test]
fn rejects_unmatched_reference_before_export_or_setup() {
let directory = tempfile::tempdir().unwrap();
let export_path = directory.path().join("results.csv");
let marker_path = directory.path().join("setup-ran");
std::fs::write(&export_path, "previous contents").unwrap();
hyperfine()
.args(["--reference", "sleep 1", "-P", "secs", "2", "3", "--setup"])
.arg(format!("echo touched > \"{}\"", marker_path.display()))
.arg("--export-csv")
.arg(&export_path)
.arg("sleep {secs}")
.assert()
.failure()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains(
"does not match any parameterized benchmark",
));
assert_eq!(
std::fs::read_to_string(export_path).unwrap(),
"previous contents"
);
assert!(!marker_path.exists());
}
#[test]
fn rejects_ambiguous_parameterized_reference() {
hyperfine_debug()
.args([
"-L",
"x",
"1,2",
"--command-name",
"case",
"--reference",
"case",
"sleep {x}",
])
.assert()
.failure()
.stderr(predicate::str::contains(
"matches multiple parameterized benchmarks",
));
}
#[cfg(unix)]
#[test]
fn intermediate_markdown_waits_for_selected_reference() {
let directory = tempfile::tempdir().unwrap();
let export_path = directory.path().join("results.md");
hyperfine()
.args([
"--style=none",
"--shell=none",
"--runs=1",
"-L",
"delay",
"0.01,0.02,0.03",
"--reference",
"sleep 0.02",
"--prepare",
"true",
"--prepare",
"false",
"--prepare",
"true",
"--export-markdown",
])
.arg(&export_path)
.arg("sleep {delay}")
.assert()
.failure();
let markdown = std::fs::read_to_string(export_path).unwrap();
let row = markdown
.lines()
.find(|line| line.contains("sleep 0.01"))
.unwrap();
assert!(row.ends_with("| N/A |"), "{}", row);
}
#[test]
fn markdown_export_uses_selected_parameterized_reference() {
let _settings = snapshot_settings().bind_to_scope(); let _settings = snapshot_settings().bind_to_scope();
assert_cmd_snapshot!(hyperfine_debug() assert_cmd_snapshot!(hyperfine_debug()
.arg("--style=basic") .args([
.arg("--reference=sleep 25") "--style=none",
.arg("--parameter-scan") "--runs=1",
.arg("time") "--export-markdown=-",
.arg("30") "-P",
.arg("45") "x",
.arg("--parameter-step-size") "1",
.arg("5") "3",
.arg("sleep {time}"), @r" "--command-name",
"case-{x}",
"--reference",
"case-2",
"sleep {x}",
]), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
Benchmark 1: sleep 25
Time (mean ± σ): 25.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
Range (min … max): 25.000 s … 25.000 s 10 runs
Benchmark 2: sleep 30 | Command | Mean [s] | Min [s] | Max [s] | Relative |
Time (mean ± σ): 30.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s] |:---|---:|---:|---:|---:|
Range (min … max): 30.000 s … 30.000 s 10 runs | `case-1` | 1.000 | 1.000 | 1.000 | 2.00 (faster) |
| `case-2` | 2.000 | 2.000 | 2.000 | 1.00 (reference) |
| `case-3` | 3.000 | 3.000 | 3.000 | 1.50 (slower) |
Benchmark 3: sleep 35
Time (mean ± σ): 35.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
Range (min … max): 35.000 s … 35.000 s 10 runs
Benchmark 4: sleep 40
Time (mean ± σ): 40.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
Range (min … max): 40.000 s … 40.000 s 10 runs
Benchmark 5: sleep 45
Time (mean ± σ): 45.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
Range (min … max): 45.000 s … 45.000 s 10 runs
Summary
sleep 25 ran
1.20 ± 0.00 times faster than sleep 30
1.40 ± 0.00 times faster than sleep 35
1.60 ± 0.00 times faster than sleep 40
1.80 ± 0.00 times faster than sleep 45
----- stderr ----- ----- stderr -----
"); ");