1use anyhow::Result;
6use serde::Serialize;
7use std::process::Command;
8use std::time::Instant;
9
10#[derive(Debug, Clone, Serialize)]
12pub struct CompetitorResult {
13 pub label: String,
14 pub command: String,
15 pub wall_time_ms: f64,
16 pub exit_code: i32,
17}
18
19fn run_timed(command: &str) -> Result<CompetitorResult> {
21 let parts: Vec<&str> = command.split_whitespace().collect();
23 if parts.is_empty() {
24 anyhow::bail!("Empty command");
25 }
26
27 let start = Instant::now();
28 let status = Command::new(parts[0])
29 .args(&parts[1..])
30 .stdout(std::process::Stdio::null())
31 .stderr(std::process::Stdio::null())
32 .status()
33 .map_err(|e| anyhow::anyhow!("Failed to run '{}': {}", parts[0], e))?;
34 let elapsed = start.elapsed();
35
36 Ok(CompetitorResult {
37 label: String::new(),
38 command: command.to_string(),
39 wall_time_ms: elapsed.as_secs_f64() * 1000.0,
40 exit_code: status.code().unwrap_or(-1),
41 })
42}
43
44pub fn run_compete(
46 workload: &str,
47 ours: &str,
48 theirs: &[String],
49 label: Option<&str>,
50 json: bool,
51) -> Result<()> {
52 let labels = build_labels(label, theirs.len());
53
54 println!("\n=== CGP Head-to-Head: {workload} ===\n");
55
56 let mut results: Vec<CompetitorResult> = Vec::new();
57 let default_ours = "ours".to_string();
58 let ours_label = labels.first().unwrap_or(&default_ours).clone();
59 results.push(run_and_record(&ours_label, ours));
60
61 for (i, cmd) in theirs.iter().enumerate() {
62 let default_theirs = format!("theirs-{}", i + 1);
63 let lbl = labels.get(i + 1).unwrap_or(&default_theirs).clone();
64 results.push(run_and_record(&lbl, cmd));
65 }
66
67 if json {
68 println!("{}", serde_json::to_string_pretty(&results)?);
69 return Ok(());
70 }
71
72 print_compete_table(&results);
73 print_compete_winner(&results);
74 println!();
75 Ok(())
76}
77
78fn build_labels(label: Option<&str>, theirs_len: usize) -> Vec<String> {
80 match label {
81 Some(l) => l.split(',').map(String::from).collect(),
82 None => {
83 let mut v = vec!["ours".to_string()];
84 v.extend((0..theirs_len).map(|i| format!("theirs-{}", i + 1)));
85 v
86 }
87 }
88}
89
90fn run_and_record(label: &str, command: &str) -> CompetitorResult {
92 eprint!(" Running: {label} ...");
93 match run_timed(command) {
94 Ok(mut r) => {
95 r.label = label.to_string();
96 eprintln!(" {:.1}ms", r.wall_time_ms);
97 r
98 }
99 Err(e) => {
100 eprintln!(" FAILED: {e}");
101 CompetitorResult {
102 label: label.to_string(),
103 command: command.to_string(),
104 wall_time_ms: f64::INFINITY,
105 exit_code: -1,
106 }
107 }
108 }
109}
110
111fn best_finite_time(results: &[CompetitorResult]) -> f64 {
112 results
113 .iter()
114 .filter(|r| r.wall_time_ms.is_finite())
115 .map(|r| r.wall_time_ms)
116 .fold(f64::INFINITY, f64::min)
117}
118
119fn print_compete_table(results: &[CompetitorResult]) {
120 let best_time = best_finite_time(results);
121 println!();
122 println!(
123 " {:20} {:>12} {:>8} {:>10}",
124 "Competitor", "Time (ms)", "Exit", "vs Best"
125 );
126 println!(" {}", "-".repeat(54));
127 for r in results {
128 let ratio = if best_time > 0.0 && r.wall_time_ms.is_finite() {
129 format!("{:.2}x", r.wall_time_ms / best_time)
130 } else {
131 "N/A".to_string()
132 };
133 let time_str = if r.wall_time_ms.is_finite() {
134 format!("{:.1}", r.wall_time_ms)
135 } else {
136 "FAILED".to_string()
137 };
138 println!(
139 " {:20} {:>12} {:>8} {:>10}",
140 r.label, time_str, r.exit_code, ratio
141 );
142 }
143}
144
145fn print_compete_winner(results: &[CompetitorResult]) {
146 let Some(winner) = results
147 .iter()
148 .filter(|r| r.wall_time_ms.is_finite())
149 .min_by(|a, b| {
150 a.wall_time_ms
151 .partial_cmp(&b.wall_time_ms)
152 .unwrap_or(std::cmp::Ordering::Equal)
153 })
154 else {
155 return;
156 };
157 println!(
158 "\n Winner: {} ({:.1}ms)",
159 winner.label, winner.wall_time_ms
160 );
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_run_timed_true() {
169 let result = run_timed("true").unwrap();
170 assert_eq!(result.exit_code, 0);
171 assert!(result.wall_time_ms < 1000.0);
172 }
173
174 #[test]
175 fn test_run_timed_false() {
176 let result = run_timed("false").unwrap();
177 assert_ne!(result.exit_code, 0);
178 }
179
180 #[test]
181 fn test_run_timed_nonexistent() {
182 let result = run_timed("nonexistent_binary_xyz_123");
183 assert!(result.is_err());
184 }
185}