use anyhow::{Context, Result};
use ruchy::Parser as RuchyParser;
use std::fs;
use std::path::Path;
use std::time::Instant;
pub fn handle_bench_command(
file: &Path,
iterations: usize,
warmup: usize,
format: &str,
output: Option<&Path>,
verbose: bool,
) -> Result<()> {
let source = fs::read_to_string(file)
.with_context(|| format!("Failed to read file: {}", file.display()))?;
let _parser = RuchyParser::new(&source);
if verbose {
println!("📊 Benchmarking: {}", file.display());
println!("🔥 Warmup: {} iterations", warmup);
println!("🏃 Benchmark: {} iterations", iterations);
}
if verbose && warmup > 0 {
println!("\n⏱️ Running warmup...");
}
for i in 0..warmup {
let mut repl = super::create_repl()?;
repl.eval(&source)?;
if verbose {
println!(" Warmup iteration {}/{}", i + 1, warmup);
}
}
if verbose {
println!("\n⏱️ Running benchmark...");
}
let mut timings = Vec::with_capacity(iterations);
for i in 0..iterations {
let start = Instant::now();
let mut repl = super::create_repl()?;
repl.eval(&source)?;
let duration = start.elapsed();
timings.push(duration.as_secs_f64() * 1000.0);
if verbose {
println!(" Iteration {}/{}: {:.3} ms", i + 1, iterations, timings[i]);
}
}
let min = timings.iter().copied().fold(f64::INFINITY, f64::min);
let max = timings.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let sum: f64 = timings.iter().sum();
let mean = sum / timings.len() as f64;
let variance: f64 =
timings.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / timings.len() as f64;
let stddev = variance.sqrt();
let report = match format {
"json" => {
generate_bench_json_output(file, iterations, warmup, &timings, min, max, mean, stddev)
}
"csv" => {
generate_bench_csv_output(file, iterations, warmup, &timings, min, max, mean, stddev)
}
_ => generate_bench_text_output(file, iterations, warmup, &timings, min, max, mean, stddev),
};
if let Some(output_path) = output {
fs::write(output_path, &report)
.with_context(|| format!("Failed to write output to: {}", output_path.display()))?;
if verbose {
println!("\n💾 Results saved to: {}", output_path.display());
}
} else {
println!("{}", report);
}
Ok(())
}
pub fn generate_bench_text_output(
file: &Path,
iterations: usize,
warmup: usize,
_timings: &[f64],
min: f64,
max: f64,
mean: f64,
stddev: f64,
) -> String {
format!(
"=== Benchmark Results ===\n\
File: {}\n\
Warmup: {} iterations\n\
Benchmark: {} iterations\n\
\n\
Statistics:\n\
├─ Min: {:.3} ms\n\
├─ Max: {:.3} ms\n\
├─ Average: {:.3} ms\n\
└─ StdDev: {:.3} ms\n",
file.display(),
warmup,
iterations,
min,
max,
mean,
stddev
)
}
pub fn generate_bench_json_output(
file: &Path,
iterations: usize,
warmup: usize,
timings: &[f64],
min: f64,
max: f64,
mean: f64,
stddev: f64,
) -> String {
format!(
"{{\n\
\"file\": \"{}\",\n\
\"warmup\": {},\n\
\"iterations\": {},\n\
\"timings_ms\": {:?},\n\
\"statistics\": {{\n\
\"min_ms\": {:.3},\n\
\"max_ms\": {:.3},\n\
\"mean_ms\": {:.3},\n\
\"stddev_ms\": {:.3}\n\
}}\n\
}}",
file.display(),
warmup,
iterations,
timings,
min,
max,
mean,
stddev
)
}
pub fn generate_bench_csv_output(
file: &Path,
iterations: usize,
warmup: usize,
_timings: &[f64],
min: f64,
max: f64,
mean: f64,
stddev: f64,
) -> String {
format!(
"file,warmup,iterations,min_ms,max_ms,mean_ms,stddev_ms\n\
\"{}\",{},{},{:.3},{:.3},{:.3},{:.3}\n",
file.display(),
warmup,
iterations,
min,
max,
mean,
stddev
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{NamedTempFile, TempDir};
#[test]
fn test_generate_text_output() {
let file = Path::new("test.ruchy");
let output = generate_bench_text_output(file, 10, 3, &[], 1.0, 5.0, 2.5, 1.0);
assert!(output.contains("Benchmark Results"));
assert!(output.contains("test.ruchy"));
assert!(output.contains("10"));
assert!(output.contains("3"));
}
#[test]
fn test_generate_json_output() {
let file = Path::new("test.ruchy");
let timings = vec![1.0, 2.0, 3.0];
let output = generate_bench_json_output(file, 3, 1, &timings, 1.0, 3.0, 2.0, 0.8);
assert!(output.contains("\"file\":"));
assert!(output.contains("\"iterations\":"));
assert!(output.contains("\"statistics\":"));
}
#[test]
fn test_generate_csv_output() {
let file = Path::new("test.ruchy");
let output = generate_bench_csv_output(file, 5, 2, &[], 1.0, 5.0, 2.5, 1.0);
assert!(output.contains("file,warmup,iterations"));
assert!(output.contains("test.ruchy"));
}
#[test]
fn test_csv_header() {
let file = Path::new("any.ruchy");
let output = generate_bench_csv_output(file, 1, 1, &[], 0.0, 0.0, 0.0, 0.0);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(
lines[0],
"file,warmup,iterations,min_ms,max_ms,mean_ms,stddev_ms"
);
}
#[test]
fn test_generate_text_output_zero_values() {
let file = Path::new("test.ruchy");
let output = generate_bench_text_output(file, 0, 0, &[], 0.0, 0.0, 0.0, 0.0);
assert!(output.contains("Benchmark Results"));
}
#[test]
fn test_generate_text_output_large_values() {
let file = Path::new("test.ruchy");
let output =
generate_bench_text_output(file, 1000000, 100000, &[], 0.001, 10000.0, 500.0, 100.0);
assert!(output.contains("Benchmark Results"));
}
#[test]
fn test_generate_json_output_empty_timings() {
let file = Path::new("test.ruchy");
let output = generate_bench_json_output(file, 0, 0, &[], 0.0, 0.0, 0.0, 0.0);
assert!(output.contains("\"timings_ms\": []"));
}
#[test]
fn test_generate_json_output_many_timings() {
let file = Path::new("test.ruchy");
let timings: Vec<f64> = (0..100).map(|x| x as f64 * 0.1).collect();
let output = generate_bench_json_output(file, 100, 10, &timings, 0.0, 9.9, 4.95, 2.0);
assert!(output.contains("\"iterations\": 100"));
}
#[test]
fn test_generate_csv_output_special_chars_in_path() {
let file = Path::new("path with spaces/test.ruchy");
let output = generate_bench_csv_output(file, 5, 2, &[], 1.0, 5.0, 2.5, 1.0);
assert!(output.contains("path with spaces"));
}
#[test]
fn test_handle_bench_command_nonexistent_file() {
let result = handle_bench_command(
Path::new("/nonexistent/file.ruchy"),
1,
0,
"text",
None,
false,
);
assert!(result.is_err());
}
#[test]
fn test_handle_bench_command_basic() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(temp.path(), 1, 0, "text", None, false);
assert!(result.is_ok());
}
#[test]
fn test_handle_bench_command_with_warmup() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(
temp.path(),
2,
1, "text",
None,
false,
);
assert!(result.is_ok());
}
#[test]
fn test_handle_bench_command_json_format() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(temp.path(), 1, 0, "json", None, false);
assert!(result.is_ok());
}
#[test]
fn test_handle_bench_command_csv_format() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(temp.path(), 1, 0, "csv", None, false);
assert!(result.is_ok());
}
#[test]
fn test_handle_bench_command_with_output() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.txt");
let result = handle_bench_command(temp.path(), 1, 0, "text", Some(&output_path), false);
assert!(result.is_ok());
assert!(output_path.exists());
}
#[test]
fn test_handle_bench_command_verbose() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(
temp.path(),
1,
0,
"text",
None,
true, );
assert!(result.is_ok());
}
#[test]
fn test_handle_bench_command_multiple_iterations() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "42").unwrap();
let result = handle_bench_command(
temp.path(),
5, 2,
"text",
None,
false,
);
assert!(result.is_ok());
}
}