use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub tool: String,
pub lint_duration: Option<Duration>,
pub format_duration: Option<Duration>,
pub file_count: usize,
pub available: bool,
}
impl BenchmarkResult {
pub fn new(tool: &str) -> Self {
Self {
tool: tool.to_string(),
lint_duration: None,
format_duration: None,
file_count: 0,
available: false,
}
}
pub fn total_duration(&self) -> Duration {
let lint = self.lint_duration.unwrap_or(Duration::ZERO);
let format = self.format_duration.unwrap_or(Duration::ZERO);
lint + format
}
}
#[derive(Debug)]
pub struct BenchmarkComparison {
pub ruff: BenchmarkResult,
pub legacy: BenchmarkResult,
}
impl BenchmarkComparison {
pub fn speedup(&self) -> Option<f64> {
let ruff_total = self.ruff.total_duration().as_secs_f64();
let legacy_total = self.legacy.total_duration().as_secs_f64();
if ruff_total > 0.0 && legacy_total > 0.0 {
Some(legacy_total / ruff_total)
} else {
None
}
}
pub fn lint_speedup(&self) -> Option<f64> {
match (self.ruff.lint_duration, self.legacy.lint_duration) {
(Some(ruff), Some(legacy)) => {
let ruff_secs = ruff.as_secs_f64();
let legacy_secs = legacy.as_secs_f64();
if ruff_secs > 0.0 {
Some(legacy_secs / ruff_secs)
} else {
None
}
}
_ => None,
}
}
pub fn format_speedup(&self) -> Option<f64> {
match (self.ruff.format_duration, self.legacy.format_duration) {
(Some(ruff), Some(legacy)) => {
let ruff_secs = ruff.as_secs_f64();
let legacy_secs = legacy.as_secs_f64();
if ruff_secs > 0.0 {
Some(legacy_secs / ruff_secs)
} else {
None
}
}
_ => None,
}
}
}
fn is_tool_available(tool: &str) -> bool {
Command::new(tool)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn benchmark_ruff_check(files: &[&Path]) -> Option<Duration> {
if files.is_empty() {
return Some(Duration::ZERO);
}
let start = Instant::now();
for file in files {
let _ = Command::new("ruff")
.args(["check", "--output-format", "json"])
.arg(file)
.output();
}
Some(start.elapsed())
}
fn benchmark_ruff_format(files: &[&Path]) -> Option<Duration> {
if files.is_empty() {
return Some(Duration::ZERO);
}
let start = Instant::now();
for file in files {
let _ = Command::new("ruff")
.args(["format", "--check"])
.arg(file)
.output();
}
Some(start.elapsed())
}
fn benchmark_flake8(files: &[&Path]) -> Option<Duration> {
if files.is_empty() {
return Some(Duration::ZERO);
}
let start = Instant::now();
for file in files {
let _ = Command::new("flake8")
.arg("--format=default")
.arg(file)
.output();
}
Some(start.elapsed())
}
fn benchmark_black(files: &[&Path]) -> Option<Duration> {
if files.is_empty() {
return Some(Duration::ZERO);
}
let start = Instant::now();
for file in files {
let _ = Command::new("black")
.args(["--check", "--quiet"])
.arg(file)
.output();
}
Some(start.elapsed())
}
pub fn run_python_benchmark(files: &[&Path]) -> BenchmarkComparison {
let file_count = files.len();
let mut ruff_result = BenchmarkResult::new("ruff");
ruff_result.available = is_tool_available("ruff");
ruff_result.file_count = file_count;
if ruff_result.available {
ruff_result.lint_duration = benchmark_ruff_check(files);
ruff_result.format_duration = benchmark_ruff_format(files);
}
let mut legacy_result = BenchmarkResult::new("flake8+black");
let flake8_available = is_tool_available("flake8");
let black_available = is_tool_available("black");
legacy_result.available = flake8_available && black_available;
legacy_result.file_count = file_count;
if flake8_available {
legacy_result.lint_duration = benchmark_flake8(files);
}
if black_available {
legacy_result.format_duration = benchmark_black(files);
}
BenchmarkComparison {
ruff: ruff_result,
legacy: legacy_result,
}
}
fn format_duration_ms(duration: Option<Duration>) -> String {
match duration {
Some(d) => format!("{:.0}", d.as_secs_f64() * 1000.0),
None => "N/A".to_string(),
}
}
fn format_speedup(speedup: Option<f64>) -> String {
match speedup {
Some(s) => format!("{:.1}x", s),
None => "N/A".to_string(),
}
}
pub fn format_benchmark_table(comparison: &BenchmarkComparison) -> String {
let file_count = comparison.ruff.file_count;
let mut output = String::new();
output.push_str(&format!(
"\nPython Linting/Formatting Benchmark ({} files)\n",
file_count
));
output.push_str("┌──────────────┬─────────────┬─────────────┬──────────────┐\n");
output.push_str("│ Tool │ Lint (ms) │ Format (ms) │ Total (ms) │\n");
output.push_str("├──────────────┼─────────────┼─────────────┼──────────────┤\n");
if comparison.legacy.available {
output.push_str(&format!(
"│ {:<12} │ {:>11} │ {:>11} │ {:>12} │\n",
"flake8+black",
format_duration_ms(comparison.legacy.lint_duration),
format_duration_ms(comparison.legacy.format_duration),
format_duration_ms(Some(comparison.legacy.total_duration())),
));
} else {
output.push_str("│ flake8+black │ N/A │ N/A │ N/A │\n");
}
if comparison.ruff.available {
output.push_str(&format!(
"│ {:<12} │ {:>11} │ {:>11} │ {:>12} │\n",
"ruff",
format_duration_ms(comparison.ruff.lint_duration),
format_duration_ms(comparison.ruff.format_duration),
format_duration_ms(Some(comparison.ruff.total_duration())),
));
} else {
output.push_str("│ ruff │ N/A │ N/A │ N/A │\n");
}
output.push_str("├──────────────┼─────────────┼─────────────┼──────────────┤\n");
output.push_str(&format!(
"│ {:<12} │ {:>11} │ {:>11} │ {:>12} │\n",
"Speedup",
format_speedup(comparison.lint_speedup()),
format_speedup(comparison.format_speedup()),
format_speedup(comparison.speedup()),
));
output.push_str("└──────────────┴─────────────┴─────────────┴──────────────┘\n");
if !comparison.ruff.available {
output.push_str(&format!("\n⚠ ruff not installed. {}\n", crate::python_tool_install_hint("ruff")));
}
if !comparison.legacy.available {
if !is_tool_available("flake8") {
output.push_str(&format!("\n⚠ flake8 not installed. {}\n", crate::python_tool_install_hint("flake8")));
}
if !is_tool_available("black") {
output.push_str(&format!("\n⚠ black not installed. {}\n", crate::python_tool_install_hint("black")));
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benchmark_result_total_duration() {
let mut result = BenchmarkResult::new("test");
result.lint_duration = Some(Duration::from_millis(100));
result.format_duration = Some(Duration::from_millis(50));
assert_eq!(result.total_duration(), Duration::from_millis(150));
}
#[test]
fn test_speedup_calculation() {
let mut ruff = BenchmarkResult::new("ruff");
ruff.lint_duration = Some(Duration::from_millis(100));
ruff.format_duration = Some(Duration::from_millis(50));
ruff.available = true;
let mut legacy = BenchmarkResult::new("flake8+black");
legacy.lint_duration = Some(Duration::from_millis(1000));
legacy.format_duration = Some(Duration::from_millis(500));
legacy.available = true;
let comparison = BenchmarkComparison { ruff, legacy };
assert!((comparison.speedup().unwrap() - 10.0).abs() < 0.1);
assert!((comparison.lint_speedup().unwrap() - 10.0).abs() < 0.1);
assert!((comparison.format_speedup().unwrap() - 10.0).abs() < 0.1);
}
#[test]
fn test_format_duration_ms() {
assert_eq!(format_duration_ms(Some(Duration::from_millis(100))), "100");
assert_eq!(format_duration_ms(Some(Duration::from_secs(1))), "1000");
assert_eq!(format_duration_ms(None), "N/A");
}
#[test]
fn test_format_speedup() {
assert_eq!(format_speedup(Some(10.0)), "10.0x");
assert_eq!(format_speedup(Some(1.5)), "1.5x");
assert_eq!(format_speedup(None), "N/A");
}
}