use anyhow::Result;
use serde::Serialize;
use std::process::Command;
use std::time::Instant;
#[derive(Debug, Clone, Serialize)]
pub struct CompetitorResult {
pub label: String,
pub command: String,
pub wall_time_ms: f64,
pub exit_code: i32,
}
fn run_timed(command: &str) -> Result<CompetitorResult> {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.is_empty() {
anyhow::bail!("Empty command");
}
let start = Instant::now();
let status = Command::new(parts[0])
.args(&parts[1..])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map_err(|e| anyhow::anyhow!("Failed to run '{}': {}", parts[0], e))?;
let elapsed = start.elapsed();
Ok(CompetitorResult {
label: String::new(),
command: command.to_string(),
wall_time_ms: elapsed.as_secs_f64() * 1000.0,
exit_code: status.code().unwrap_or(-1),
})
}
pub fn run_compete(
workload: &str,
ours: &str,
theirs: &[String],
label: Option<&str>,
json: bool,
) -> Result<()> {
let labels = build_labels(label, theirs.len());
println!("\n=== CGP Head-to-Head: {workload} ===\n");
let mut results: Vec<CompetitorResult> = Vec::new();
let default_ours = "ours".to_string();
let ours_label = labels.first().unwrap_or(&default_ours).clone();
results.push(run_and_record(&ours_label, ours));
for (i, cmd) in theirs.iter().enumerate() {
let default_theirs = format!("theirs-{}", i + 1);
let lbl = labels.get(i + 1).unwrap_or(&default_theirs).clone();
results.push(run_and_record(&lbl, cmd));
}
if json {
println!("{}", serde_json::to_string_pretty(&results)?);
return Ok(());
}
print_compete_table(&results);
print_compete_winner(&results);
println!();
Ok(())
}
fn build_labels(label: Option<&str>, theirs_len: usize) -> Vec<String> {
match label {
Some(l) => l.split(',').map(String::from).collect(),
None => {
let mut v = vec!["ours".to_string()];
v.extend((0..theirs_len).map(|i| format!("theirs-{}", i + 1)));
v
}
}
}
fn run_and_record(label: &str, command: &str) -> CompetitorResult {
eprint!(" Running: {label} ...");
match run_timed(command) {
Ok(mut r) => {
r.label = label.to_string();
eprintln!(" {:.1}ms", r.wall_time_ms);
r
}
Err(e) => {
eprintln!(" FAILED: {e}");
CompetitorResult {
label: label.to_string(),
command: command.to_string(),
wall_time_ms: f64::INFINITY,
exit_code: -1,
}
}
}
}
fn best_finite_time(results: &[CompetitorResult]) -> f64 {
results
.iter()
.filter(|r| r.wall_time_ms.is_finite())
.map(|r| r.wall_time_ms)
.fold(f64::INFINITY, f64::min)
}
fn print_compete_table(results: &[CompetitorResult]) {
let best_time = best_finite_time(results);
println!();
println!(
" {:20} {:>12} {:>8} {:>10}",
"Competitor", "Time (ms)", "Exit", "vs Best"
);
println!(" {}", "-".repeat(54));
for r in results {
let ratio = if best_time > 0.0 && r.wall_time_ms.is_finite() {
format!("{:.2}x", r.wall_time_ms / best_time)
} else {
"N/A".to_string()
};
let time_str = if r.wall_time_ms.is_finite() {
format!("{:.1}", r.wall_time_ms)
} else {
"FAILED".to_string()
};
println!(
" {:20} {:>12} {:>8} {:>10}",
r.label, time_str, r.exit_code, ratio
);
}
}
fn print_compete_winner(results: &[CompetitorResult]) {
let Some(winner) = results
.iter()
.filter(|r| r.wall_time_ms.is_finite())
.min_by(|a, b| {
a.wall_time_ms
.partial_cmp(&b.wall_time_ms)
.unwrap_or(std::cmp::Ordering::Equal)
})
else {
return;
};
println!(
"\n Winner: {} ({:.1}ms)",
winner.label, winner.wall_time_ms
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_timed_true() {
let result = run_timed("true").unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.wall_time_ms < 1000.0);
}
#[test]
fn test_run_timed_false() {
let result = run_timed("false").unwrap();
assert_ne!(result.exit_code, 0);
}
#[test]
fn test_run_timed_nonexistent() {
let result = run_timed("nonexistent_binary_xyz_123");
assert!(result.is_err());
}
}