#![allow(clippy::all, clippy::pedantic, clippy::nursery)]
mod common;
use assert_cmd::Command;
use chrono::Utc;
use common::init_test_logging;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use tempfile::TempDir;
use tracing::info;
macro_rules! skip_if_no_bd {
() => {
if let Some(reason) = common::bd_skip_reason() {
eprintln!("Skipping benchmark comparison: {reason}");
return;
}
};
}
#[derive(Debug, Clone)]
pub struct CmdOutput {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub duration: Duration,
}
pub struct BenchmarkWorkspace {
pub temp_dir: TempDir,
pub br_root: PathBuf,
pub bd_root: PathBuf,
pub log_dir: PathBuf,
}
impl BenchmarkWorkspace {
pub fn new() -> Self {
let temp_dir = TempDir::new().expect("create temp dir");
let root = temp_dir.path().to_path_buf();
let br_root = root.join("br_workspace");
let bd_root = root.join("bd_workspace");
let log_dir = root.join("logs");
fs::create_dir_all(&br_root).expect("create br workspace");
fs::create_dir_all(&bd_root).expect("create bd workspace");
fs::create_dir_all(&log_dir).expect("create log dir");
Self {
temp_dir,
br_root,
bd_root,
log_dir,
}
}
pub fn init_both(&self) -> (CmdOutput, CmdOutput) {
let br_out = self.run_br(["init"], "init");
let bd_out = self.run_bd(["init"], "init");
(br_out, bd_out)
}
pub fn run_br<I, S>(&self, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_br_cmd(&self.br_root, &self.log_dir, args, &format!("br_{label}"))
}
pub fn run_bd<I, S>(&self, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_bd_cmd(&self.bd_root, &self.log_dir, args, &format!("bd_{label}"))
}
pub fn time_br<I, S>(&self, args: I) -> Duration
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let start = Instant::now();
let _ = self.run_br(args, "timing");
start.elapsed()
}
pub fn time_bd<I, S>(&self, args: I) -> Duration
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let start = Instant::now();
let _ = self.run_bd(args, "timing");
start.elapsed()
}
}
#[derive(Debug, Clone)]
struct LogConfig {
json_logs: bool,
junit: bool,
summary: bool,
failure_context: bool,
}
impl LogConfig {
fn from_env() -> Self {
Self {
json_logs: env_flag("CONFORMANCE_JSON_LOGS"),
junit: env_flag("CONFORMANCE_JUNIT_XML"),
summary: env_flag("CONFORMANCE_SUMMARY"),
failure_context: env_flag("CONFORMANCE_FAILURE_CONTEXT"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct RunLogEntry {
timestamp: String,
label: String,
binary: String,
args: Vec<String>,
cwd: String,
status_code: i32,
success: bool,
duration_ms: u128,
stdout_len: usize,
stderr_len: usize,
log_path: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct SummaryStats {
runs: u64,
failures: u64,
total_ms: u128,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct SummaryReport {
generated_at: String,
total_runs: u64,
total_failures: u64,
by_binary: std::collections::HashMap<String, SummaryStats>,
by_label: std::collections::HashMap<String, SummaryStats>,
comparisons: std::collections::HashMap<String, ComparisonStats>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct ComparisonStats {
br_runs: u64,
bd_runs: u64,
br_total_ms: u128,
bd_total_ms: u128,
speedup_bd_over_br: Option<f64>,
}
static LOG_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn log_mutex() -> &'static Mutex<()> {
LOG_MUTEX.get_or_init(|| Mutex::new(()))
}
fn env_flag(name: &str) -> bool {
match std::env::var(name) {
Ok(value) => matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
),
Err(_) => false,
}
}
fn collect_dir_listing(path: &PathBuf) -> Vec<String> {
let mut entries = Vec::new();
if let Ok(read_dir) = fs::read_dir(path) {
for entry in read_dir.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(meta) = entry.metadata() {
if meta.is_dir() {
entries.push(format!("{name}/"));
} else {
entries.push(format!("{name} ({:?} bytes)", meta.len()));
}
} else {
entries.push(name);
}
}
}
entries.sort();
entries
}
fn append_run_entry(log_dir: &PathBuf, entry: &RunLogEntry) {
let log_path = log_dir.join("conformance_runs.jsonl");
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.expect("open conformance_runs.jsonl");
let json = serde_json::to_string(entry).expect("serialize run entry");
writeln!(file, "{json}").expect("append run entry");
}
fn read_run_entries(log_dir: &PathBuf) -> Vec<RunLogEntry> {
let log_path = log_dir.join("conformance_runs.jsonl");
let Ok(contents) = fs::read_to_string(&log_path) else {
return Vec::new();
};
contents
.lines()
.filter_map(|line| serde_json::from_str::<RunLogEntry>(line).ok())
.collect()
}
fn update_summary(log_dir: &PathBuf, entries: &[RunLogEntry]) {
let mut report = SummaryReport::default();
report.generated_at = chrono::Utc::now().to_rfc3339();
for entry in entries {
report.total_runs += 1;
if !entry.success {
report.total_failures += 1;
}
let by_binary = report
.by_binary
.entry(entry.binary.clone())
.or_insert_with(SummaryStats::default);
by_binary.runs += 1;
if !entry.success {
by_binary.failures += 1;
}
by_binary.total_ms = by_binary.total_ms.saturating_add(entry.duration_ms);
let by_label = report
.by_label
.entry(entry.label.clone())
.or_insert_with(SummaryStats::default);
by_label.runs += 1;
if !entry.success {
by_label.failures += 1;
}
by_label.total_ms = by_label.total_ms.saturating_add(entry.duration_ms);
let comparison = report
.comparisons
.entry(entry.label.clone())
.or_insert_with(ComparisonStats::default);
if entry.binary == "br" {
comparison.br_runs += 1;
comparison.br_total_ms = comparison.br_total_ms.saturating_add(entry.duration_ms);
} else if entry.binary == "bd" {
comparison.bd_runs += 1;
comparison.bd_total_ms = comparison.bd_total_ms.saturating_add(entry.duration_ms);
}
}
for comparison in report.comparisons.values_mut() {
if comparison.br_total_ms > 0 && comparison.bd_total_ms > 0 {
comparison.speedup_bd_over_br =
Some(comparison.bd_total_ms as f64 / comparison.br_total_ms as f64);
}
}
let summary_path = log_dir.join("conformance_summary.json");
let json = serde_json::to_string_pretty(&report).expect("serialize summary");
fs::write(summary_path, json).expect("write summary");
}
fn xml_escape(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn write_junit(log_dir: &PathBuf, entries: &[RunLogEntry]) {
let total = entries.len();
let failures = entries.iter().filter(|e| !e.success).count();
let mut xml = String::new();
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
xml.push('\n');
xml.push_str(&format!(
r#"<testsuite name="conformance_runs" tests="{total}" failures="{failures}">"#
));
xml.push('\n');
for entry in entries {
let name = xml_escape(&format!("{}:{}", entry.binary, entry.label));
let classname = xml_escape(&entry.binary);
let time_secs = entry.duration_ms as f64 / 1000.0;
xml.push_str(&format!(
r#" <testcase classname="{classname}" name="{name}" time="{time_secs:.3}">"#
));
if !entry.success {
let msg = xml_escape(&format!(
"exit={}; log={}",
entry.status_code, entry.log_path
));
xml.push_str(&format!(r#"<failure message="{msg}"/>"#));
}
xml.push_str("</testcase>\n");
}
xml.push_str("</testsuite>\n");
let junit_path = log_dir.join("conformance_junit.xml");
fs::write(junit_path, xml).expect("write junit xml");
}
fn write_failure_context(
log_dir: &PathBuf,
entry: &RunLogEntry,
stdout: &str,
stderr: &str,
cwd: &PathBuf,
) {
let beads_dir = cwd.join(".beads");
let context = serde_json::json!({
"timestamp": entry.timestamp,
"label": entry.label,
"binary": entry.binary,
"args": entry.args,
"cwd": entry.cwd,
"status_code": entry.status_code,
"success": entry.success,
"duration_ms": entry.duration_ms,
"stdout_len": entry.stdout_len,
"stderr_len": entry.stderr_len,
"stdout_preview": stdout.chars().take(2000).collect::<String>(),
"stderr_preview": stderr.chars().take(2000).collect::<String>(),
"beads_dir": beads_dir.display().to_string(),
"beads_entries": collect_dir_listing(&beads_dir),
"recent_runs": read_run_entries(log_dir).into_iter().rev().take(5).collect::<Vec<_>>(),
});
let path = log_dir.join(format!("{}.failure.json", entry.label));
let json = serde_json::to_string_pretty(&context).expect("serialize failure context");
fs::write(path, json).expect("write failure context");
}
fn record_run(log_dir: &PathBuf, entry: RunLogEntry, stdout: &str, stderr: &str, cwd: &PathBuf) {
let config = LogConfig::from_env();
if !(config.json_logs || config.junit || config.summary || config.failure_context) {
return;
}
let _guard = log_mutex().lock().expect("lock test log mutex");
append_run_entry(log_dir, &entry);
let entries = read_run_entries(log_dir);
if config.summary {
update_summary(log_dir, &entries);
}
if config.junit {
write_junit(log_dir, &entries);
}
if config.failure_context && !entry.success {
write_failure_context(log_dir, &entry, stdout, stderr, cwd);
}
}
fn run_br_cmd<I, S>(cwd: &PathBuf, log_dir: &PathBuf, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("br"));
cmd.current_dir(cwd);
cmd.args(args);
cmd.env("NO_COLOR", "1");
cmd.env("HOME", cwd);
let start = Instant::now();
let output = cmd.output().expect("run br");
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let log_path = log_dir.join(format!("{label}.log"));
let log_body = format!(
"label: {label}\nduration: {:?}\nstatus: {}\n\nstdout:\n{}\n\nstderr:\n{}\n",
duration, output.status, stdout, stderr
);
let _ = fs::write(&log_path, log_body);
let entry = RunLogEntry {
timestamp: Utc::now().to_rfc3339(),
label: label.to_string(),
binary: "br".to_string(),
args: cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect(),
cwd: cwd.display().to_string(),
status_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
duration_ms: duration.as_millis(),
stdout_len: stdout.len(),
stderr_len: stderr.len(),
log_path: log_path.display().to_string(),
};
record_run(log_dir, entry, &stdout, &stderr, cwd);
CmdOutput {
stdout,
stderr,
success: output.status.success(),
duration,
}
}
fn run_bd_cmd<I, S>(cwd: &PathBuf, log_dir: &PathBuf, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = std::process::Command::new(common::bd_binary_name());
cmd.current_dir(cwd);
cmd.args(args);
cmd.env("NO_COLOR", "1");
cmd.env("HOME", cwd);
let start = Instant::now();
let output = cmd.output().expect("run bd");
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let log_path = log_dir.join(format!("{label}.log"));
let log_body = format!(
"label: {label}\nduration: {:?}\nstatus: {}\n\nstdout:\n{}\n\nstderr:\n{}\n",
duration, output.status, stdout, stderr
);
let _ = fs::write(&log_path, log_body);
let entry = RunLogEntry {
timestamp: Utc::now().to_rfc3339(),
label: label.to_string(),
binary: "bd".to_string(),
args: cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect(),
cwd: cwd.display().to_string(),
status_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
duration_ms: duration.as_millis(),
stdout_len: stdout.len(),
stderr_len: stderr.len(),
log_path: log_path.display().to_string(),
};
record_run(log_dir, entry, &stdout, &stderr, cwd);
CmdOutput {
stdout,
stderr,
success: output.status.success(),
duration,
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkConfig {
pub warmup_runs: usize,
pub timed_runs: usize,
pub outlier_threshold: f64,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
warmup_runs: 2,
timed_runs: 5,
outlier_threshold: 2.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingStats {
pub mean_ms: f64,
pub median_ms: f64,
pub p95_ms: f64,
pub std_dev_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub run_count: usize,
}
impl TimingStats {
pub fn from_durations(durations: &[Duration]) -> Self {
if durations.is_empty() {
return Self {
mean_ms: 0.0,
median_ms: 0.0,
p95_ms: 0.0,
std_dev_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
run_count: 0,
};
}
let mut ms_values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
ms_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = ms_values.len();
let mean = ms_values.iter().sum::<f64>() / n as f64;
let median = if n % 2 == 0 {
(ms_values[n / 2 - 1] + ms_values[n / 2]) / 2.0
} else {
ms_values[n / 2]
};
let p95_idx = (n as f64 * 0.95).ceil() as usize - 1;
let p95 = ms_values[p95_idx.min(n - 1)];
let variance = ms_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
let std_dev = variance.sqrt();
Self {
mean_ms: mean,
median_ms: median,
p95_ms: p95,
std_dev_ms: std_dev,
min_ms: ms_values[0],
max_ms: ms_values[n - 1],
run_count: n,
}
}
pub fn filter_outliers(durations: &[Duration], threshold: f64) -> Vec<Duration> {
if durations.len() < 3 {
return durations.to_vec();
}
let ms_values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
let mean = ms_values.iter().sum::<f64>() / ms_values.len() as f64;
let variance =
ms_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / ms_values.len() as f64;
let std_dev = variance.sqrt();
durations
.iter()
.zip(ms_values.iter())
.filter(|&(_, &ms)| (ms - mean).abs() <= threshold * std_dev)
.map(|(d, _)| *d)
.collect()
}
}
pub fn run_benchmark<F>(config: &BenchmarkConfig, mut f: F) -> TimingStats
where
F: FnMut() -> Duration,
{
for _ in 0..config.warmup_runs {
let _ = f();
}
let mut durations: Vec<Duration> = Vec::with_capacity(config.timed_runs);
for _ in 0..config.timed_runs {
durations.push(f());
}
let filtered = TimingStats::filter_outliers(&durations, config.outlier_threshold);
TimingStats::from_durations(&filtered)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkComparison {
pub name: String,
pub description: String,
pub br_stats: TimingStats,
pub bd_stats: TimingStats,
pub speedup_ratio: f64,
pub speedup_percent: f64,
}
impl BenchmarkComparison {
pub fn new(
name: &str,
description: &str,
br_stats: TimingStats,
bd_stats: TimingStats,
) -> Self {
let speedup_ratio = if bd_stats.mean_ms > 0.0 {
br_stats.mean_ms / bd_stats.mean_ms
} else {
1.0
};
let speedup_percent = if bd_stats.mean_ms > 0.0 {
((bd_stats.mean_ms - br_stats.mean_ms) / bd_stats.mean_ms) * 100.0
} else {
0.0
};
Self {
name: name.to_string(),
description: description.to_string(),
br_stats,
bd_stats,
speedup_ratio,
speedup_percent,
}
}
pub fn print(&self) {
println!("\n=== {} ===", self.name);
println!("Description: {}", self.description);
println!(
"br: mean={:.2}ms median={:.2}ms p95={:.2}ms",
self.br_stats.mean_ms, self.br_stats.median_ms, self.br_stats.p95_ms
);
println!(
"bd: mean={:.2}ms median={:.2}ms p95={:.2}ms",
self.bd_stats.mean_ms, self.bd_stats.median_ms, self.bd_stats.p95_ms
);
if self.speedup_percent > 0.0 {
println!(
"Result: br is {:.1}% FASTER (ratio: {:.2}x)",
self.speedup_percent, self.speedup_ratio
);
} else if self.speedup_percent < 0.0 {
println!(
"Result: br is {:.1}% SLOWER (ratio: {:.2}x)",
-self.speedup_percent, self.speedup_ratio
);
} else {
println!(
"Result: Similar performance (ratio: {:.2}x)",
self.speedup_ratio
);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkReport {
pub timestamp: String,
pub config: BenchmarkConfigJson,
pub comparisons: Vec<BenchmarkComparison>,
pub memory: Vec<MemoryComparison>,
pub summary: BenchmarkSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkConfigJson {
pub warmup_runs: usize,
pub timed_runs: usize,
pub outlier_threshold: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkSummary {
pub total_benchmarks: usize,
pub br_faster_count: usize,
pub bd_faster_count: usize,
pub avg_speedup_percent: f64,
pub avg_speedup_ratio: f64,
}
impl BenchmarkReport {
pub fn new(
config: &BenchmarkConfig,
comparisons: Vec<BenchmarkComparison>,
memory: Vec<MemoryComparison>,
) -> Self {
let total = comparisons.len();
let br_faster = comparisons
.iter()
.filter(|c| c.speedup_percent > 0.0)
.count();
let bd_faster = comparisons
.iter()
.filter(|c| c.speedup_percent < 0.0)
.count();
let avg_speedup = if total > 0 {
comparisons.iter().map(|c| c.speedup_percent).sum::<f64>() / total as f64
} else {
0.0
};
let avg_ratio = if total > 0 {
comparisons.iter().map(|c| c.speedup_ratio).sum::<f64>() / total as f64
} else {
1.0
};
Self {
timestamp: chrono::Utc::now().to_rfc3339(),
config: BenchmarkConfigJson {
warmup_runs: config.warmup_runs,
timed_runs: config.timed_runs,
outlier_threshold: config.outlier_threshold,
},
comparisons,
memory,
summary: BenchmarkSummary {
total_benchmarks: total,
br_faster_count: br_faster,
bd_faster_count: bd_faster,
avg_speedup_percent: avg_speedup,
avg_speedup_ratio: avg_ratio,
},
}
}
pub fn print_summary(&self) {
println!("\n========================================");
println!("BENCHMARK COMPARISON REPORT");
println!("========================================");
println!("Timestamp: {}", self.timestamp);
println!(
"Config: {} warmup, {} timed runs, {:.1}x outlier threshold",
self.config.warmup_runs, self.config.timed_runs, self.config.outlier_threshold
);
println!("");
for comparison in &self.comparisons {
comparison.print();
}
if !self.memory.is_empty() {
println!("\n========================================");
println!("MEMORY USAGE (MAX RSS)");
println!("========================================");
for entry in &self.memory {
println!("\n=== {} ===", entry.name);
println!("Description: {}", entry.description);
let br_rss = entry
.br
.max_rss_kb
.map_or("n/a".to_string(), |rss| format!("{rss} KB"));
let bd_rss = entry
.bd
.max_rss_kb
.map_or("n/a".to_string(), |rss| format!("{rss} KB"));
println!("br max RSS: {br_rss}");
println!("bd max RSS: {bd_rss}");
}
}
println!("\n========================================");
println!("SUMMARY");
println!("========================================");
println!("Total benchmarks: {}", self.summary.total_benchmarks);
println!(
"br faster: {} ({:.0}%)",
self.summary.br_faster_count,
100.0 * self.summary.br_faster_count as f64 / self.summary.total_benchmarks as f64
);
println!(
"bd faster: {} ({:.0}%)",
self.summary.bd_faster_count,
100.0 * self.summary.bd_faster_count as f64 / self.summary.total_benchmarks as f64
);
println!(
"Average speedup: {:.1}% ({:.2}x ratio)",
self.summary.avg_speedup_percent, self.summary.avg_speedup_ratio
);
if self.summary.avg_speedup_percent > 0.0 {
println!("\nOverall: br (Rust) is faster on average");
} else if self.summary.avg_speedup_percent < 0.0 {
println!("\nOverall: bd (Go) is faster on average");
} else {
println!("\nOverall: Similar performance");
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStats {
pub max_rss_kb: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryComparison {
pub name: String,
pub description: String,
pub br: MemoryStats,
pub bd: MemoryStats,
}
fn parse_max_rss_kb(stderr: &str) -> Option<u64> {
for line in stderr.lines() {
if let Some(rest) = line.strip_prefix("Maximum resident set size (kbytes):") {
return rest.trim().parse::<u64>().ok();
}
}
None
}
fn time_binary_with_rss<P: AsRef<Path>>(
program: P,
cwd: &PathBuf,
args: &[&str],
) -> Option<MemoryStats> {
let time_path = Path::new("/usr/bin/time");
if !time_path.exists() {
info!("memory_benchmark: /usr/bin/time not found; skipping");
return None;
}
let output = std::process::Command::new(time_path)
.arg("-v")
.arg(program.as_ref())
.args(args)
.current_dir(cwd)
.env("NO_COLOR", "1")
.env("HOME", cwd)
.output()
.expect("run /usr/bin/time");
let stderr = String::from_utf8_lossy(&output.stderr);
let max_rss_kb = parse_max_rss_kb(&stderr);
Some(MemoryStats { max_rss_kb })
}
fn benchmark_memory_usage_1000() -> Option<MemoryComparison> {
info!("benchmark_memory_usage_1000: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 1000);
let br_bin = assert_cmd::cargo::cargo_bin!("br");
let br_stats = time_binary_with_rss(&br_bin, &workspace.br_root, &["list", "--json"])
.unwrap_or(MemoryStats { max_rss_kb: None });
let bd_stats = time_binary_with_rss("bd", &workspace.bd_root, &["list", "--json"])
.unwrap_or(MemoryStats { max_rss_kb: None });
if br_stats.max_rss_kb.is_none() && bd_stats.max_rss_kb.is_none() {
info!("benchmark_memory_usage_1000: no RSS data available");
return None;
}
Some(MemoryComparison {
name: "memory_list_1000".to_string(),
description: "Max RSS for list --json with 1000 issues".to_string(),
br: br_stats,
bd: bd_stats,
})
}
fn benchmark_memory_sync_flush_1000() -> Option<MemoryComparison> {
info!("benchmark_memory_sync_flush_1000: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 1000);
let br_bin = assert_cmd::cargo::cargo_bin!("br");
let br_stats = time_binary_with_rss(&br_bin, &workspace.br_root, &["sync", "--flush-only"])
.unwrap_or(MemoryStats { max_rss_kb: None });
let bd_stats = time_binary_with_rss("bd", &workspace.bd_root, &["sync", "--flush-only"])
.unwrap_or(MemoryStats { max_rss_kb: None });
if br_stats.max_rss_kb.is_none() && bd_stats.max_rss_kb.is_none() {
info!("benchmark_memory_sync_flush_1000: no RSS data available");
return None;
}
Some(MemoryComparison {
name: "memory_sync_flush_1000".to_string(),
description: "Max RSS for sync --flush-only with 1000 issues".to_string(),
br: br_stats,
bd: bd_stats,
})
}
fn benchmark_memory_sync_import_1000() -> Option<MemoryComparison> {
info!("benchmark_memory_sync_import_1000: starting");
let jsonl_data = generate_import_jsonl(1000);
let br_workspace = BenchmarkWorkspace::new();
let br_init = br_workspace.run_br(["init"], "init");
assert!(br_init.success, "br init failed: {}", br_init.stderr);
let br_jsonl_path = br_workspace.br_root.join(".beads").join("issues.jsonl");
fs::write(&br_jsonl_path, &jsonl_data).expect("write br issues.jsonl");
let bd_workspace = BenchmarkWorkspace::new();
let bd_init = bd_workspace.run_bd(["init"], "init");
assert!(bd_init.success, "bd init failed: {}", bd_init.stderr);
let bd_jsonl_path = bd_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::write(&bd_jsonl_path, &jsonl_data).expect("write bd issues.jsonl");
let br_bin = assert_cmd::cargo::cargo_bin!("br");
let br_stats = time_binary_with_rss(&br_bin, &br_workspace.br_root, &["sync", "--import-only"])
.unwrap_or(MemoryStats { max_rss_kb: None });
let bd_stats = time_binary_with_rss("bd", &bd_workspace.bd_root, &["sync", "--import-only"])
.unwrap_or(MemoryStats { max_rss_kb: None });
if br_stats.max_rss_kb.is_none() && bd_stats.max_rss_kb.is_none() {
info!("benchmark_memory_sync_import_1000: no RSS data available");
return None;
}
Some(MemoryComparison {
name: "memory_sync_import_1000".to_string(),
description: "Max RSS for sync --import-only with 1000 issues".to_string(),
br: br_stats,
bd: bd_stats,
})
}
fn benchmark_init(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_init: starting");
let br_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
workspace.time_br(["init"])
});
let bd_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
workspace.time_bd(["init"])
});
info!(
"benchmark_init: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new(
"init",
"Initialize workspace (cold start)",
br_stats,
bd_stats,
)
}
fn benchmark_create_single(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_create_single: starting");
let workspace = BenchmarkWorkspace::new();
workspace.init_both();
let mut br_counter = 0;
let br_stats = run_benchmark(config, || {
let title = format!("Benchmark issue {}", br_counter);
br_counter += 1;
workspace.time_br(["create", &title, "--json"])
});
let mut bd_counter = 0;
let bd_stats = run_benchmark(config, || {
let title = format!("Benchmark issue {}", bd_counter);
bd_counter += 1;
workspace.time_bd(["create", &title, "--json"])
});
info!(
"benchmark_create_single: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("create_single", "Create single issue", br_stats, bd_stats)
}
fn benchmark_create_batch_100(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_create_batch_100: starting");
let br_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
let _ = workspace.run_br(["init"], "init");
let start = Instant::now();
for i in 0..100 {
let title = format!("Batch issue {}", i);
let _ = workspace.run_br(["create", &title, "--json"], "create");
}
start.elapsed()
});
let bd_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
let _ = workspace.run_bd(["init"], "init");
let start = Instant::now();
for i in 0..100 {
let title = format!("Batch issue {}", i);
let _ = workspace.run_bd(["create", &title, "--json"], "create");
}
start.elapsed()
});
info!(
"benchmark_create_batch_100: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new(
"create_batch_100",
"Create 100 issues (throughput)",
br_stats,
bd_stats,
)
}
fn populate_workspace(workspace: &BenchmarkWorkspace, count: usize) {
workspace.init_both();
for i in 0..count {
let title = format!("Issue {}", i);
let priority = format!("{}", i % 5);
workspace.run_br(
["create", &title, "--priority", &priority, "--json"],
"setup",
);
workspace.run_bd(
["create", &title, "--priority", &priority, "--json"],
"setup",
);
}
}
fn generate_import_jsonl(count: usize) -> Vec<u8> {
let workspace = BenchmarkWorkspace::new();
let init = workspace.run_br(["init"], "init");
assert!(init.success, "br init failed: {}", init.stderr);
for i in 0..count {
let title = format!("Import seed issue {}", i);
let create = workspace.run_br(["create", &title, "--json"], "create");
assert!(create.success, "br create failed: {}", create.stderr);
}
let flush = workspace.run_br(["sync", "--flush-only"], "sync_flush");
assert!(flush.success, "br sync flush failed: {}", flush.stderr);
let jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
fs::read(&jsonl_path).expect("read issues.jsonl for import seed")
}
fn benchmark_list_10(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_list_10: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 10);
let br_stats = run_benchmark(config, || workspace.time_br(["list", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["list", "--json"]));
info!(
"benchmark_list_10: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("list_10", "List 10 issues", br_stats, bd_stats)
}
fn benchmark_list_100(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_list_100: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 100);
let br_stats = run_benchmark(config, || workspace.time_br(["list", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["list", "--json"]));
info!(
"benchmark_list_100: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("list_100", "List 100 issues", br_stats, bd_stats)
}
fn benchmark_list_1000(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_list_1000: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 1000);
let br_stats = run_benchmark(config, || workspace.time_br(["list", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["list", "--json"]));
info!(
"benchmark_list_1000: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("list_1000", "List 1000 issues", br_stats, bd_stats)
}
fn benchmark_list_filtered(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_list_filtered: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 50);
let br_stats = run_benchmark(config, || {
workspace.time_br(["list", "--status=open", "--json"])
});
let bd_stats = run_benchmark(config, || {
workspace.time_bd(["list", "--status=open", "--json"])
});
info!(
"benchmark_list_filtered: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new(
"list_filtered",
"List with status filter (50 issues)",
br_stats,
bd_stats,
)
}
fn benchmark_search(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_search: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 50);
let br_stats = run_benchmark(config, || workspace.time_br(["search", "Issue", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["search", "Issue", "--json"]));
info!(
"benchmark_search: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("search", "Full-text search (50 issues)", br_stats, bd_stats)
}
fn benchmark_ready(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_ready: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 30);
let br_stats = run_benchmark(config, || workspace.time_br(["ready", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["ready", "--json"]));
info!(
"benchmark_ready: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("ready", "Get ready issues (30 issues)", br_stats, bd_stats)
}
fn benchmark_sync_flush(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_sync_flush: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 50);
let br_stats = run_benchmark(config, || workspace.time_br(["sync", "--flush-only"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["sync", "--flush-only"]));
info!(
"benchmark_sync_flush: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new(
"sync_flush",
"Sync flush to JSONL (50 issues)",
br_stats,
bd_stats,
)
}
fn benchmark_sync_import(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_sync_import: starting");
let jsonl_data = generate_import_jsonl(50);
let br_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
let init = workspace.run_br(["init"], "init");
assert!(init.success, "br init failed: {}", init.stderr);
let jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
fs::write(&jsonl_path, &jsonl_data).expect("write br issues.jsonl");
let result = workspace.run_br(["sync", "--import-only"], "sync_import");
assert!(result.success, "br sync import failed: {}", result.stderr);
result.duration
});
let bd_stats = run_benchmark(config, || {
let workspace = BenchmarkWorkspace::new();
let init = workspace.run_bd(["init"], "init");
assert!(init.success, "bd init failed: {}", init.stderr);
let jsonl_path = workspace.bd_root.join(".beads").join("issues.jsonl");
fs::write(&jsonl_path, &jsonl_data).expect("write bd issues.jsonl");
let result = workspace.run_bd(["sync", "--import-only"], "sync_import");
assert!(result.success, "bd sync import failed: {}", result.stderr);
result.duration
});
info!(
"benchmark_sync_import: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new(
"sync_import",
"Sync import from JSONL (50 issues)",
br_stats,
bd_stats,
)
}
fn benchmark_stats(config: &BenchmarkConfig) -> BenchmarkComparison {
info!("benchmark_stats: starting");
let workspace = BenchmarkWorkspace::new();
populate_workspace(&workspace, 30);
let br_stats = run_benchmark(config, || workspace.time_br(["stats", "--json"]));
let bd_stats = run_benchmark(config, || workspace.time_bd(["stats", "--json"]));
info!(
"benchmark_stats: br_mean={:.2}ms bd_mean={:.2}ms",
br_stats.mean_ms, bd_stats.mean_ms
);
BenchmarkComparison::new("stats", "Get project stats (30 issues)", br_stats, bd_stats)
}
#[test]
#[ignore]
fn benchmark_comparison_full() {
init_test_logging();
println!("\n");
println!("========================================");
println!("STARTING BR VS BD BENCHMARK COMPARISON");
println!("========================================");
println!("This will take several minutes...\n");
let config = BenchmarkConfig {
warmup_runs: 2,
timed_runs: 5,
outlier_threshold: 2.0,
};
let mut comparisons = Vec::new();
println!("[1/12] Running init benchmark...");
comparisons.push(benchmark_init(&config));
println!("[2/12] Running create_single benchmark...");
comparisons.push(benchmark_create_single(&config));
println!("[3/12] Running create_batch_100 benchmark (this takes a while)...");
comparisons.push(benchmark_create_batch_100(&config));
println!("[4/12] Running list_10 benchmark...");
comparisons.push(benchmark_list_10(&config));
println!("[5/12] Running list_100 benchmark...");
comparisons.push(benchmark_list_100(&config));
println!("[6/12] Running list_1000 benchmark...");
comparisons.push(benchmark_list_1000(&config));
println!("[7/12] Running list_filtered benchmark...");
comparisons.push(benchmark_list_filtered(&config));
println!("[8/12] Running search benchmark...");
comparisons.push(benchmark_search(&config));
println!("[9/12] Running ready benchmark...");
comparisons.push(benchmark_ready(&config));
println!("[10/12] Running sync_flush benchmark...");
comparisons.push(benchmark_sync_flush(&config));
println!("[11/12] Running sync_import benchmark...");
comparisons.push(benchmark_sync_import(&config));
println!("[12/12] Running stats benchmark...");
comparisons.push(benchmark_stats(&config));
let mut memory = Vec::new();
if let Some(entry) = benchmark_memory_usage_1000() {
memory.push(entry);
}
if let Some(entry) = benchmark_memory_sync_flush_1000() {
memory.push(entry);
}
if let Some(entry) = benchmark_memory_sync_import_1000() {
memory.push(entry);
}
let report = BenchmarkReport::new(&config, comparisons, memory);
report.print_summary();
let json_report = report.to_json();
println!("\n========================================");
println!("JSON REPORT");
println!("========================================");
println!("{}", json_report);
let report_path = std::env::temp_dir().join("br_bd_benchmark_report.json");
if let Err(e) = fs::write(&report_path, &json_report) {
eprintln!(
"Warning: Could not save report to {}: {}",
report_path.display(),
e
);
} else {
println!("\nReport saved to: {}", report_path.display());
}
}
#[test]
fn benchmark_comparison_quick() {
skip_if_no_bd!();
init_test_logging();
info!("benchmark_comparison_quick: starting");
let config = BenchmarkConfig {
warmup_runs: 1,
timed_runs: 3,
outlier_threshold: 3.0,
};
let init_result = benchmark_init(&config);
assert!(
init_result.br_stats.mean_ms > 0.0,
"br init should have positive timing"
);
assert!(
init_result.bd_stats.mean_ms > 0.0,
"bd init should have positive timing"
);
init_result.print();
let create_result = benchmark_create_single(&config);
assert!(
create_result.br_stats.mean_ms > 0.0,
"br create should have positive timing"
);
assert!(
create_result.bd_stats.mean_ms > 0.0,
"bd create should have positive timing"
);
create_result.print();
info!("benchmark_comparison_quick: completed successfully");
}
#[test]
fn benchmark_infrastructure_works() {
skip_if_no_bd!();
init_test_logging();
info!("benchmark_infrastructure_works: testing BenchmarkWorkspace");
let workspace = BenchmarkWorkspace::new();
assert!(workspace.br_root.exists());
assert!(workspace.bd_root.exists());
let (br_out, bd_out) = workspace.init_both();
assert!(br_out.success, "br init failed: {}", br_out.stderr);
assert!(bd_out.success, "bd init failed: {}", bd_out.stderr);
let br_create = workspace.run_br(["create", "Test issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Test issue", "--json"], "create");
assert!(br_create.success, "br create failed: {}", br_create.stderr);
assert!(bd_create.success, "bd create failed: {}", bd_create.stderr);
let br_duration = workspace.time_br(["list", "--json"]);
let bd_duration = workspace.time_bd(["list", "--json"]);
assert!(br_duration.as_millis() > 0, "br timing should be positive");
assert!(bd_duration.as_millis() > 0, "bd timing should be positive");
info!("benchmark_infrastructure_works: all checks passed");
}
#[test]
fn test_timing_stats_calculations() {
init_test_logging();
let durations = vec![
Duration::from_millis(10),
Duration::from_millis(12),
Duration::from_millis(11),
Duration::from_millis(13),
Duration::from_millis(11),
];
let stats = TimingStats::from_durations(&durations);
assert!(
(stats.mean_ms - 11.4).abs() < 0.1,
"mean_ms was {}",
stats.mean_ms
);
assert!(
(stats.median_ms - 11.0).abs() < 0.1,
"median_ms was {}",
stats.median_ms
);
assert!(
(stats.min_ms - 10.0).abs() < 0.1,
"min_ms was {}",
stats.min_ms
);
assert!(
(stats.max_ms - 13.0).abs() < 0.1,
"max_ms was {}",
stats.max_ms
);
assert_eq!(stats.run_count, 5);
info!("test_timing_stats_calculations: passed");
}
#[test]
fn test_outlier_filtering() {
init_test_logging();
let durations = vec![
Duration::from_millis(10),
Duration::from_millis(11),
Duration::from_millis(12),
Duration::from_millis(11),
Duration::from_millis(10),
Duration::from_millis(11),
Duration::from_millis(500), ];
let filtered = TimingStats::filter_outliers(&durations, 2.0);
assert!(
filtered.len() < durations.len(),
"Should have filtered an outlier"
);
let stats = TimingStats::from_durations(&filtered);
assert!(
stats.max_ms < 100.0,
"Outlier should be removed, max was {}",
stats.max_ms
);
info!("test_outlier_filtering: passed");
}
#[test]
fn test_benchmark_comparison_calculations() {
init_test_logging();
let br_stats = TimingStats {
mean_ms: 10.0,
median_ms: 10.0,
p95_ms: 12.0,
std_dev_ms: 1.0,
min_ms: 9.0,
max_ms: 12.0,
run_count: 5,
};
let bd_stats = TimingStats {
mean_ms: 20.0,
median_ms: 20.0,
p95_ms: 22.0,
std_dev_ms: 1.0,
min_ms: 19.0,
max_ms: 22.0,
run_count: 5,
};
let comparison = BenchmarkComparison::new("test", "Test benchmark", br_stats, bd_stats);
assert!(
(comparison.speedup_ratio - 0.5).abs() < 0.01,
"Speedup ratio should be 0.5, was {}",
comparison.speedup_ratio
);
assert!(
(comparison.speedup_percent - 50.0).abs() < 0.1,
"Speedup percent should be 50%, was {}",
comparison.speedup_percent
);
info!("test_benchmark_comparison_calculations: passed");
}
mod matched_arithmetic {
use super::*;
use common::baseline::{
BaselineStore, MATCHED_BLOCK_PROTOCOL, MATCHED_RUN_METADATA, MatchedRun, MatchedState,
OperationBaseline, RegressionConfig, RegressionResult, RegressionStatus, RegressionSummary,
compare_matched_runs, summarize_matched_samples,
};
fn control(samples_ms: Vec<f64>) -> MatchedRun {
let metadata = [
("command", "list --json"),
("issue_count", "1000"),
("flush_mode", "auto-flush-enabled"),
("cache_protocol", "warm-after-one-discarded-warmup"),
("host", "arithmetic-control-host"),
("host_boot_id", "11111111-1111-4111-8111-111111111111"),
("cpu", "arithmetic-control-cpu"),
("os", "arithmetic-control-os"),
("filesystem", "arithmetic-control-filesystem"),
("target", "x86_64-unknown-linux-gnu"),
("features", "self_update"),
("engine", "arithmetic-control-engine"),
("source_revision", "arithmetic-control-source"),
("build_profile", "release"),
("sampling_protocol", MATCHED_BLOCK_PROTOCOL),
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.chain([
("dataset_sha256".to_string(), "1".repeat(64)),
("lockfile_sha256".to_string(), "2".repeat(64)),
("binary_sha256".to_string(), "3".repeat(64)),
])
.collect();
MatchedRun {
exit_codes: vec![0; samples_ms.len()],
block_ids: (0..samples_ms.len()).map(|index| index / 2).collect(),
metadata,
samples_ms,
}
}
fn close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-10,
"expected {expected}, got {actual}"
);
}
#[test]
fn equal_positive_zero_variance_passes_without_dividing_by_variance() {
let baseline = control(vec![10.0; 198]);
let result = compare_matched_runs(Some(&baseline), &baseline, 0.0);
assert_eq!(result.state, MatchedState::Pass);
assert_eq!(result.exit_code(), 0);
close(result.median.unwrap().delta_ms, 0.0);
close(result.p95.unwrap().delta_pct, 0.0);
let interval = result.observed_support.unwrap();
close(interval.lower_pct.unwrap(), 0.0);
close(interval.upper_pct.unwrap(), 0.0);
assert_eq!(
interval.method,
"observed_support_extrema_not_confidence_interval"
);
let uncertainty = result.uncertainty.unwrap();
assert_eq!(uncertainty.block_count, 99);
close(uncertainty.p95.upper.unwrap().delta_pct, 0.0);
}
#[test]
fn inclusive_budget_boundary_and_small_variation_are_not_false_regressions() {
let baseline = control(vec![10.0; 198]);
let candidate = control(vec![11.0; 198]);
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 10.0).exit_code(),
0
);
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 9.99).exit_code(),
1
);
let varied = control((0..198).map(|n| 10.0 + f64::from(n % 20) / 100.0).collect());
assert_eq!(
compare_matched_runs(Some(&baseline), &varied, 10.0).exit_code(),
0
);
}
#[test]
fn quantiles_and_deltas_match_hand_calculation() {
let baseline = control((10..30).rev().map(f64::from).collect());
let candidate = control((15..35).map(f64::from).collect());
let result = compare_matched_runs(Some(&baseline), &candidate, 30.0);
let median = result.median.unwrap();
close(median.baseline_ms, 19.5);
close(median.candidate_ms, 24.5);
close(median.delta_ms, 5.0);
close(median.delta_pct, 5.0 / 19.5 * 100.0);
let p95 = result.p95.unwrap();
close(p95.baseline_ms, 28.0); close(p95.candidate_ms, 33.0);
close(p95.delta_ms, 5.0);
close(p95.delta_pct, 5.0 / 28.0 * 100.0);
let interval = result.observed_support.unwrap();
close(interval.lower_ms, -14.0); close(interval.upper_ms, 24.0); close(interval.lower_pct.unwrap(), (15.0 / 29.0 - 1.0) * 100.0);
close(interval.upper_pct.unwrap(), 240.0);
assert_eq!(result.state, MatchedState::Inconclusive);
let odd = summarize_matched_samples(&(1..=21).map(f64::from).collect::<Vec<_>>())
.expect("valid arithmetic samples");
close(odd.median_ms, 11.0);
close(odd.p95_ms, 20.0); close(odd.min_ms, 1.0);
close(odd.max_ms, 21.0);
assert_eq!(odd.sample_count, 21);
}
#[test]
fn degraded_receipt_fails_actual_gate_with_exit_one_and_diagnostic() {
let root = TempDir::new().expect("arithmetic control directory");
let baseline_path = root.path().join("baseline.json");
let candidate_path = root.path().join("degraded.json");
let baseline = control(vec![10.0; 198]);
let mut candidate = control(vec![15.0; 198]);
candidate
.metadata
.insert("binary_sha256".to_string(), "4".repeat(64));
candidate
.metadata
.insert("lockfile_sha256".to_string(), "5".repeat(64));
candidate.metadata.insert(
"source_revision".to_string(),
"arithmetic-control-candidate".to_string(),
);
fs::write(&baseline_path, serde_json::to_vec(&baseline).unwrap()).unwrap();
fs::write(&candidate_path, serde_json::to_vec(&candidate).unwrap()).unwrap();
let loaded_baseline = MatchedRun::load(&baseline_path).unwrap();
let loaded_candidate = MatchedRun::load(&candidate_path).unwrap();
let result = compare_matched_runs(Some(&loaded_baseline), &loaded_candidate, 20.0);
assert_eq!(result.state, MatchedState::Regression);
assert_eq!(result.exit_code(), 1);
close(result.p95.unwrap().delta_pct, 50.0);
assert!(result.diagnostic.contains("list --json"));
assert!(result.diagnostic.contains("budget 20.000%"));
assert!(
result
.diagnostic
.contains("p95 delta +5.000000 ms (+50.000%)")
);
let missing_path = root.path().join("missing.json");
let mismatched_path = root.path().join("mismatched.json");
let mut mismatched = baseline.clone();
mismatched.metadata.insert(
"host_boot_id".to_string(),
"another-control-boot".to_string(),
);
fs::write(&mismatched_path, serde_json::to_vec(&mismatched).unwrap()).unwrap();
for (candidate, reference, budget, exit, diagnostic) in [
(&baseline_path, &baseline_path, "20", 0, "Pass"),
(&candidate_path, &baseline_path, "20", 1, "Regression"),
(
&candidate_path,
&missing_path,
"20",
2,
"missing baseline receipt",
),
(
&mismatched_path,
&baseline_path,
"20",
2,
"mismatched metadata: host_boot_id",
),
(
&baseline_path,
&baseline_path,
"NaN",
2,
"budget unavailable",
),
] {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"matched_arithmetic::receipt_gate_process_driver",
"--ignored",
"--nocapture",
])
.env("BR_ARITHMETIC_GATE_CANDIDATE", candidate)
.env("BR_ARITHMETIC_GATE_BASELINE", reference)
.env("BR_ARITHMETIC_GATE_BUDGET", budget)
.output()
.expect("run the arithmetic receipt gate as a real subprocess");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output.status.code(), Some(exit), "{output:?}");
assert!(stdout.contains(diagnostic), "{output:?}");
assert!(stdout.contains("list --json"), "{output:?}");
}
}
#[test]
#[ignore = "subprocess driver; requires BR_ARITHMETIC_GATE_* receipt inputs"]
fn receipt_gate_process_driver() {
let candidate_path =
std::env::var_os("BR_ARITHMETIC_GATE_CANDIDATE").expect("candidate receipt path");
let baseline_path =
std::env::var_os("BR_ARITHMETIC_GATE_BASELINE").expect("baseline receipt path");
let budget = std::env::var("BR_ARITHMETIC_GATE_BUDGET")
.expect("explicit diagnostic budget")
.parse::<f64>()
.expect("numeric diagnostic budget");
let candidate = MatchedRun::load(Path::new(&candidate_path)).expect("candidate receipt");
let baseline = match MatchedRun::load(Path::new(&baseline_path)) {
Ok(run) => Some(run),
Err(error) => {
eprintln!("Baseline receipt unavailable: {error}");
None
}
};
let result = compare_matched_runs(baseline.as_ref(), &candidate, budget);
println!("{}", serde_json::to_string(&result).unwrap());
std::io::stdout().flush().unwrap();
std::process::exit(result.exit_code());
}
#[test]
fn missing_or_corrupt_receipts_cannot_pass() {
let baseline = control(vec![10.0; 20]);
let missing = compare_matched_runs(None, &baseline, 20.0);
assert_eq!(missing.state, MatchedState::Inconclusive);
assert_eq!(missing.exit_code(), 2);
assert!(missing.diagnostic.contains("missing baseline"));
let root = TempDir::new().unwrap();
let missing_path = root.path().join("missing.json");
assert!(MatchedRun::load(&missing_path).is_err());
assert!(BaselineStore::load(&missing_path).is_err());
for (index, bytes) in ["{", "{}", "{\"metadata\":null}", "null"]
.iter()
.enumerate()
{
let path = root.path().join(format!("corrupt-{index}.json"));
fs::write(&path, bytes).unwrap();
assert!(MatchedRun::load(&path).is_err());
assert!(BaselineStore::load(&path).is_err());
let empty = BaselineStore::load_or_default(&path);
assert!(empty.get_baseline("control", "list").is_none());
let config = RegressionConfig {
strict_mode: true,
..Default::default()
};
let summary = RegressionSummary::from_results(
vec![RegressionResult::no_baseline("list", "control", 1.0, None)],
&config,
);
assert!(!summary.passed);
}
}
#[test]
fn malformed_zero_nonfinite_and_insufficient_samples_are_inconclusive() {
let baseline = control(vec![10.0; 20]);
for value in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let invalid = control(vec![value; 20]);
for result in [
compare_matched_runs(Some(&invalid), &baseline, 20.0),
compare_matched_runs(Some(&baseline), &invalid, 20.0),
] {
assert_eq!(result.state, MatchedState::Inconclusive);
assert_eq!(result.exit_code(), 2);
}
}
for count in [0, 1, 19, 21] {
let candidate = control(vec![10.0; count]);
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 20.0).exit_code(),
2
);
}
for budget in [-1.0, f64::NAN, f64::INFINITY] {
let result = compare_matched_runs(Some(&baseline), &baseline, budget);
assert_eq!(result.exit_code(), 2);
assert!(result.budget_pct.is_none());
assert!(result.diagnostic.contains("budget unavailable"));
close(result.median.unwrap().delta_ms, 0.0);
close(result.p95.unwrap().delta_ms, 0.0);
close(result.observed_support.unwrap().upper_ms, 0.0);
}
let tiny = control(vec![f64::MIN_POSITIVE; 20]);
let huge = control(vec![f64::MAX; 20]);
assert_eq!(
compare_matched_runs(Some(&tiny), &huge, 20.0).exit_code(),
2
);
}
#[test]
fn noisy_samples_stay_present_and_cannot_claim_a_pass() {
let baseline = control(vec![10.0; 20]);
let mut candidate = baseline.clone();
candidate.samples_ms[19] = 1000.0;
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.state, MatchedState::Inconclusive);
assert_eq!(result.exit_code(), 2);
close(result.p95.unwrap().delta_ms, 0.0);
close(result.observed_support.unwrap().upper_ms, 990.0);
assert!(result.uncertainty.unwrap().p95.upper.is_none());
assert_eq!(candidate.samples_ms.len(), 20);
close(
summarize_matched_samples(&candidate.samples_ms)
.unwrap()
.max_ms,
1000.0,
);
}
#[test]
fn binomial_ranks_match_independent_integer_probability_calculations() {
for (blocks, median_ranks, p95_ranks) in [
(10, (1, 10), (7, 11)),
(98, (37, 62), (87, 99)),
(99, (37, 63), (88, 99)),
(200, (82, 119), (182, 198)),
(300, (128, 173), (275, 295)),
(1000, (461, 540), (932, 967)),
] {
let run = control(vec![100.0; blocks * 2]);
let result = compare_matched_runs(Some(&run), &run, 0.0);
assert_eq!(result.exit_code(), if blocks >= 99 { 0 } else { 2 });
let uncertainty = result.uncertainty.unwrap();
assert_eq!(uncertainty.block_count, blocks);
assert_eq!(
(uncertainty.median.lower_rank, uncertainty.median.upper_rank),
median_ranks,
);
assert_eq!(
(uncertainty.p95.lower_rank, uncertainty.p95.upper_rank),
p95_ranks,
);
assert_eq!(uncertainty.p95.upper.is_some(), blocks >= 99);
assert_eq!(uncertainty.p95.baseline_upper_ms.is_some(), blocks >= 99);
close(uncertainty.confidence_level, 0.95);
close(uncertainty.one_sided_error_probability, 1.0 / 160.0);
}
}
#[test]
fn median_regression_does_not_require_a_finite_p95_upper_bound() {
let baseline = control(vec![10.0; 20]);
let candidate = control(vec![15.0; 20]);
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.state, MatchedState::Regression);
let uncertainty = result.uncertainty.unwrap();
close(uncertainty.median.lower.unwrap().delta_pct, 50.0);
assert!(uncertainty.p95.upper.is_none());
assert!(uncertainty.p95.lower.is_none());
}
#[test]
fn median_and_tail_regressions_are_decided_separately() {
let baseline = control(vec![100.0; 400]);
let tail = control(
(0..200)
.flat_map(|block| [if block < 180 { 100.0 } else { 140.0 }; 2])
.collect(),
);
let result = compare_matched_runs(Some(&baseline), &tail, 20.0);
assert_eq!(result.state, MatchedState::Regression);
close(result.median.unwrap().delta_ms, 0.0);
close(result.p95.unwrap().delta_ms, 40.0);
let uncertainty = result.uncertainty.unwrap();
close(uncertainty.median.upper.unwrap().delta_pct, 0.0);
close(uncertainty.p95.lower.unwrap().delta_pct, 40.0);
let baseline = control(
(0..200)
.flat_map(|block| [if block < 180 { 100.0 } else { 200.0 }; 2])
.collect(),
);
let candidate = control(
(0..200)
.flat_map(|block| [if block < 180 { 140.0 } else { 200.0 }; 2])
.collect(),
);
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.state, MatchedState::Regression);
close(result.p95.unwrap().delta_ms, 0.0);
close(
result.uncertainty.unwrap().median.lower.unwrap().delta_pct,
40.0,
);
}
#[test]
fn sufficient_variable_samples_pass_without_discarding_extreme_observations() {
let varied = control(
(0..400)
.map(|n| 100.0 + f64::from(n % 10) / 100.0)
.collect(),
);
assert_eq!(
compare_matched_runs(Some(&varied), &varied, 1.0).exit_code(),
0
);
let baseline = control(vec![100.0; 400]);
let mut candidate = baseline.clone();
candidate.samples_ms[399] = 1000.0;
let result = compare_matched_runs(Some(&baseline), &candidate, 0.0);
assert_eq!(result.state, MatchedState::Pass);
close(result.observed_support.unwrap().upper_ms, 900.0);
close(result.uncertainty.unwrap().p95.upper.unwrap().delta_ms, 0.0);
assert_eq!(candidate.samples_ms.len(), 400);
close(candidate.samples_ms[399], 1000.0);
close(
summarize_matched_samples(&candidate.samples_ms)
.unwrap()
.max_ms,
1000.0,
);
}
#[test]
fn persistent_within_block_ambiguity_cannot_be_hidden_by_block_averages() {
let baseline = control(vec![100.0; 400]);
let candidate = control((0..200).flat_map(|_| [1.0, 199.0]).collect());
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.state, MatchedState::Inconclusive);
close(result.median.unwrap().delta_ms, 0.0);
close(result.p95.unwrap().delta_ms, 99.0);
let interval = result.uncertainty.unwrap().p95;
close(interval.candidate_lower_ms, 1.0);
close(interval.candidate_upper_ms.unwrap(), 199.0);
}
#[test]
fn descriptive_percentage_overflow_does_not_veto_finite_quantile_bounds() {
let baseline = control(vec![1.0; 400]);
let mut candidate = baseline.clone();
candidate.samples_ms[399] = f64::MAX;
let result = compare_matched_runs(Some(&baseline), &candidate, 0.0);
assert_eq!(result.state, MatchedState::Pass);
let json = serde_json::to_value(&result).unwrap();
assert!(json["observed_support"]["upper_pct"].is_null());
close(result.observed_support.unwrap().upper_ms, f64::MAX);
close(result.uncertainty.unwrap().p95.upper.unwrap().delta_ms, 0.0);
assert_eq!(candidate.samples_ms[399].to_bits(), f64::MAX.to_bits());
let baseline = control(vec![1.0; 198]);
let mut candidate = baseline.clone();
candidate.samples_ms[197] = f64::MAX;
let result = compare_matched_runs(Some(&baseline), &candidate, 0.0);
assert_eq!(result.state, MatchedState::Inconclusive);
assert!(
result
.diagnostic
.contains("quantile comparison arithmetic overflow")
);
}
#[test]
fn absent_malformed_or_mismatched_block_evidence_cannot_pass() {
let baseline = control(vec![100.0; 198]);
let mut malformed = Vec::new();
let mut absent = baseline.clone();
absent.block_ids.clear();
malformed.push(absent);
let mut orphan = baseline.clone();
orphan.block_ids.pop();
malformed.push(orphan);
let mut reused = baseline.clone();
reused.block_ids[2] = 0;
reused.block_ids[3] = 0;
malformed.push(reused);
let mut split = baseline.clone();
split.block_ids.swap(1, 2);
malformed.push(split);
let mut skipped = baseline.clone();
skipped.block_ids[196] = 99;
skipped.block_ids[197] = 99;
malformed.push(skipped);
let mut different = baseline.clone();
for block in &mut different.block_ids {
*block += 1;
}
malformed.push(different);
let mut protocol = baseline.clone();
protocol.metadata.remove("sampling_protocol");
malformed.push(protocol);
for candidate in malformed {
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.exit_code(), 2, "{}", result.diagnostic);
assert!(result.uncertainty.is_none());
}
let mut raw = serde_json::to_value(&baseline).unwrap();
raw.as_object_mut().unwrap().remove("block_ids");
raw["metadata"]
.as_object_mut()
.unwrap()
.remove("sampling_protocol");
let raw: MatchedRun = serde_json::from_value(raw).unwrap();
let result = compare_matched_runs(Some(&raw), &raw, 20.0);
assert_eq!(result.exit_code(), 2);
close(result.median.unwrap().delta_ms, 0.0);
close(result.observed_support.unwrap().upper_ms, 0.0);
assert!(result.uncertainty.is_none());
}
#[test]
fn missing_placeholder_and_mismatched_metadata_are_inconclusive() {
let baseline = control(vec![10.0; 198]);
assert_eq!(
compare_matched_runs(Some(&baseline), &baseline, 20.0).exit_code(),
0
);
for key in MATCHED_RUN_METADATA {
let mut candidate = baseline.clone();
candidate.metadata.remove(key);
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 20.0).exit_code(),
2,
"missing {key}"
);
candidate
.metadata
.insert(key.to_string(), "unknown".to_string());
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 20.0).exit_code(),
2,
"unknown {key}"
);
if !matches!(key, "source_revision" | "lockfile_sha256" | "binary_sha256") {
let replacement = if key == "dataset_sha256" {
"6".repeat(64)
} else if key == "issue_count" {
"10000".to_string()
} else {
"different-control-value".to_string()
};
candidate.metadata.insert(key.to_string(), replacement);
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.exit_code(), 2, "mismatch {key}");
assert!(result.diagnostic.contains(key));
}
}
for digest in ["0".repeat(64), "a".repeat(63), "g".repeat(64)] {
let mut candidate = baseline.clone();
candidate
.metadata
.insert("binary_sha256".to_string(), digest);
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 20.0).exit_code(),
2
);
}
}
#[test]
fn success_metadata_never_overrides_nonzero_or_missing_exit_codes() {
let mut baseline = control(vec![10.0; 20]);
baseline
.metadata
.insert("status".to_string(), "success".to_string());
let mut candidate = baseline.clone();
candidate.exit_codes[5] = 7;
let result = compare_matched_runs(Some(&baseline), &candidate, 20.0);
assert_eq!(result.exit_code(), 2);
assert!(
result
.diagnostic
.contains("sample 5 failed with exit code 7")
);
candidate.exit_codes.pop();
assert_eq!(
compare_matched_runs(Some(&baseline), &candidate, 20.0).exit_code(),
2
);
}
#[test]
fn legacy_unknown_and_empty_summary_cannot_pass_in_either_mode() {
for strict_mode in [false, true] {
let config = RegressionConfig {
strict_mode,
..Default::default()
};
assert!(!RegressionSummary::from_results(Vec::new(), &config).passed);
let result = RegressionResult::no_baseline("list", "arithmetic", 1.0, None);
assert_eq!(result.status, RegressionStatus::Inconclusive);
let summary = RegressionSummary::from_results(vec![result], &config);
assert_eq!(summary.ok_count, 0);
assert_eq!(summary.inconclusive_count, 1);
assert!(!summary.passed);
}
}
#[test]
fn legacy_invalid_ratios_and_unmatched_rss_are_inconclusive() {
let config = RegressionConfig::ci();
let mut baseline = OperationBaseline {
duration_ratio: 1.0,
rss_ratio: None,
br_duration_ms: 10,
bd_duration_ms: 10,
captured_at: "arithmetic-control".to_string(),
notes: None,
};
for ratio in [0.0, -1.0, f64::NAN, f64::INFINITY] {
baseline.duration_ratio = ratio;
let result = RegressionResult::check("list", "control", 1.0, None, &baseline, &config);
assert_eq!(result.status, RegressionStatus::Inconclusive);
assert!(!RegressionSummary::from_results(vec![result], &config).passed);
}
baseline.duration_ratio = 1.0;
baseline.rss_ratio = Some(1.0);
let result = RegressionResult::check("list", "control", 1.0, None, &baseline, &config);
assert_eq!(result.status, RegressionStatus::Inconclusive);
}
}