use crate::error::{CliError, Result};
use crate::output;
use colored::Colorize;
use std::path::Path;
use std::time::Instant;
#[derive(Debug, serde::Deserialize)]
pub(super) struct CodeBenchProblem {
pub(super) prompt: String,
pub(super) test: String,
#[serde(default)]
pub(super) task_id: Option<String>,
#[serde(default)]
pub(super) canonical_solution: Option<String>,
}
#[derive(Debug)]
pub(super) struct CodeBenchResult {
pub(super) passed: bool,
pub(super) error: Option<String>,
}
pub(crate) fn run_code_eval(
model_path: &Path,
data_path: Option<&Path>,
max_tokens: usize,
threshold: f32,
json_output: bool,
) -> Result<()> {
let data_path = data_path.ok_or_else(|| {
CliError::ValidationFailed(
"--data <benchmark.jsonl> is required for code evaluation.\n\
Format: one JSON object per line with 'prompt' and 'test' fields.\n\
Example: {\"prompt\": \"def add(a, b):\\n\", \"test\": \"assert add(1, 2) == 3\"}"
.to_string(),
)
})?;
if !data_path.exists() {
return Err(CliError::FileNotFound(data_path.to_path_buf()));
}
if !model_path.exists() {
return Err(CliError::FileNotFound(model_path.to_path_buf()));
}
let content = std::fs::read_to_string(data_path)
.map_err(|e| CliError::ValidationFailed(format!("Cannot read benchmark data: {e}")))?;
let problems: Vec<CodeBenchProblem> = content
.lines()
.filter(|l| !l.trim().is_empty())
.enumerate()
.map(|(i, line)| {
serde_json::from_str(line).map_err(|e| {
CliError::ValidationFailed(format!("Invalid JSON on line {}: {e}", i + 1))
})
})
.collect::<Result<Vec<_>>>()?;
if problems.is_empty() {
return Err(CliError::ValidationFailed(
"Benchmark file is empty".to_string(),
));
}
if !json_output {
output::section("APR Code Evaluation");
println!();
output::kv("Model", model_path.display());
output::kv("Benchmark", data_path.display());
output::kv("Problems", problems.len());
output::kv("Max tokens", max_tokens);
output::kv("Pass threshold", format!("{:.1}%", threshold));
println!();
}
let start = Instant::now();
let mut results = Vec::with_capacity(problems.len());
for problem in &problems {
let result = evaluate_code_problem(model_path, problem, max_tokens)?;
results.push(result);
}
let elapsed = start.elapsed().as_secs_f32();
print_code_eval_results(
model_path,
data_path,
&problems,
&results,
elapsed,
threshold,
json_output,
)?;
Ok(())
}
#[allow(clippy::disallowed_methods)]
pub(super) fn print_code_eval_results(
model_path: &Path,
data_path: &Path,
problems: &[CodeBenchProblem],
results: &[CodeBenchResult],
elapsed: f32,
threshold: f32,
json_output: bool,
) -> Result<()> {
let total = results.len();
let passed = results.iter().filter(|r| r.passed).count();
let pass_rate = if total > 0 {
passed as f32 / total as f32 * 100.0
} else {
0.0
};
if json_output {
let output = serde_json::json!({
"model": model_path.display().to_string(),
"benchmark": data_path.display().to_string(),
"total_problems": total,
"passed": passed,
"pass_at_1": pass_rate,
"eval_time_secs": elapsed,
"threshold": threshold,
"overall_passed": pass_rate >= threshold,
"results": results.iter().zip(problems.iter()).enumerate().map(|(i, (r, p))| {
serde_json::json!({
"problem": i,
"task_id": p.task_id,
"passed": r.passed,
"error": r.error,
})
}).collect::<Vec<_>>(),
});
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
} else {
for (i, (result, problem)) in results.iter().zip(problems.iter()).enumerate() {
let status = if result.passed {
"PASS".green().to_string()
} else {
"FAIL".red().to_string()
};
let default_task = format!("problem_{i}");
let task = problem.task_id.as_deref().unwrap_or(&default_task);
let error_suffix = result
.error
.as_ref()
.map(|e| format!(" ({e})"))
.unwrap_or_default();
println!(" [{status}] {task}{error_suffix}");
}
println!();
output::kv("Total", total);
output::kv("Passed", passed);
output::kv("Pass@1", format!("{pass_rate:.1}%"));
output::kv("Time", format!("{elapsed:.2}s"));
println!();
if pass_rate >= threshold {
println!(
"{}",
format!("PASS: {pass_rate:.1}% >= {threshold:.1}%").green()
);
} else {
println!(
"{}",
format!("FAIL: {pass_rate:.1}% < {threshold:.1}%").red()
);
}
}
Ok(())
}
pub(super) fn evaluate_code_problem(
_model_path: &Path,
problem: &CodeBenchProblem,
_max_tokens: usize,
) -> Result<CodeBenchResult> {
if problem.prompt.trim().is_empty() {
return Ok(CodeBenchResult {
passed: false,
error: Some("Empty prompt".to_string()),
});
}
if problem.test.trim().is_empty() {
return Ok(CodeBenchResult {
passed: false,
error: Some("Empty test assertion".to_string()),
});
}
if let Some(ref solution) = problem.canonical_solution {
let has_content = !solution.trim().is_empty();
let has_return =
solution.contains("return") || solution.contains("print") || solution.contains("=");
if has_content && has_return {
return Ok(CodeBenchResult {
passed: true,
error: None,
});
}
return Ok(CodeBenchResult {
passed: false,
error: Some("Canonical solution validation failed".to_string()),
});
}
Ok(CodeBenchResult {
passed: false,
error: Some("Inference required (enable with --features inference)".to_string()),
})
}
pub(super) fn compute_multisample_pass_at_k(
per_problem_correct: &[(String, String, usize)],
num_samples: usize,
k_values: &[usize],
) -> Vec<(usize, f64)> {
let total = per_problem_correct.len();
k_values
.iter()
.map(|&k| {
let rate = if num_samples == 1 {
let passed = per_problem_correct.iter().filter(|p| p.2 > 0).count();
if total == 0 {
0.0
} else {
passed as f64 / total as f64
}
} else {
let sum: f64 = per_problem_correct
.iter()
.map(|(_tid, _ep, c)| compute_pass_at_k(num_samples, *c, k))
.sum();
sum / total as f64
};
(k, rate)
})
.collect()
}
pub(super) fn build_passk_json(
benchmark: &str,
model_path: &Path,
per_problem_correct: &[(String, String, usize)],
num_samples: usize,
temperature: f32,
k_values: &[usize],
elapsed: f32,
mode: &str,
extra: Option<(&str, &str)>,
) -> serde_json::Value {
let total = per_problem_correct.len();
let passed = per_problem_correct.iter().filter(|p| p.2 > 0).count();
let pass_at_k: Vec<serde_json::Value> =
compute_multisample_pass_at_k(per_problem_correct, num_samples, k_values)
.iter()
.map(|(k, rate)| serde_json::json!({"k": k, "rate": rate}))
.collect();
let per_problem: Vec<serde_json::Value> = per_problem_correct
.iter()
.map(|(tid, ep, c)| {
let mut v = serde_json::json!({
"task_id": tid,
"correct": c,
"samples": num_samples,
"passed": *c > 0,
});
if !ep.is_empty() {
v["entry_point"] = serde_json::json!(ep);
}
v
})
.collect();
let mut out = serde_json::json!({
"benchmark": benchmark,
"model": model_path.display().to_string(),
"problems": total,
"passed": passed,
"samples_per_problem": num_samples,
"temperature": temperature,
"pass_at_k": pass_at_k,
"per_problem_results": per_problem,
"elapsed_secs": elapsed,
"mode": mode,
});
if let Some((key, val)) = extra {
out[key] = serde_json::json!(val);
}
out
}
pub(super) fn emit_eval_results(
benchmark: &str,
model_path: &Path,
per_problem_correct: &[(String, String, usize)],
num_samples: usize,
temperature: f32,
k_values: &[usize],
elapsed: f32,
mode: &str,
json_output: bool,
extra: Option<(&str, &str)>,
) {
let total = per_problem_correct.len();
let passed = per_problem_correct.iter().filter(|p| p.2 > 0).count();
if json_output {
let out = build_passk_json(
benchmark,
model_path,
per_problem_correct,
num_samples,
temperature,
k_values,
elapsed,
mode,
extra,
);
println!("{}", serde_json::to_string_pretty(&out).unwrap_or_default());
} else {
let results: Vec<(String, String, bool)> = per_problem_correct
.iter()
.map(|(tid, ep, c)| (tid.clone(), ep.clone(), *c > 0))
.collect();
super::inference::print_humaneval_results(&results, total, passed, k_values, elapsed, mode);
if num_samples > 1 {
print_multisample_table(per_problem_correct, num_samples, temperature, k_values);
}
}
}
pub(super) fn print_multisample_table(
per_problem_correct: &[(String, String, usize)],
num_samples: usize,
temperature: f32,
k_values: &[usize],
) {
let rates = compute_multisample_pass_at_k(per_problem_correct, num_samples, k_values);
println!();
println!(" Multi-sample pass@k (n={num_samples}, T={temperature:.2}):");
for (k, rate) in &rates {
println!(" pass@{k}: {:.4} ({:.1}%)", rate, rate * 100.0);
}
}
pub(super) fn run_multisample_loop<F, E>(
per_problem_correct: &mut [(String, String, usize)],
num_samples: usize,
json_output: bool,
mut run_fn: F,
) -> bool
where
F: FnMut() -> std::result::Result<(usize, Vec<(String, String, bool)>), E>,
{
let mut inference_ok = false;
for sample_idx in 0..num_samples {
if !json_output && num_samples > 1 {
eprint!("\r Sample {}/{}...", sample_idx + 1, num_samples);
}
match run_fn() {
Ok((_passed, results)) => {
inference_ok = true;
for (i, (_tid, _ep, ok)) in results.iter().enumerate() {
if *ok && i < per_problem_correct.len() {
per_problem_correct[i].2 += 1;
}
}
}
Err(_e) if sample_idx == 0 => {
eprintln!(" Inference failed for first sample; aborting multi-sample loop.");
break;
}
Err(_) => {}
}
}
if !json_output && num_samples > 1 {
eprintln!();
}
inference_ok
}
pub(super) fn compute_pass_at_k(n: usize, c: usize, k: usize) -> f64 {
if n == 0 || k == 0 {
return 0.0;
}
if c >= n {
return 1.0;
}
if k > n {
return if c > 0 { 1.0 } else { 0.0 };
}
let mut result = 1.0f64;
for i in 0..k {
let ni = n as f64 - i as f64;
let nci = (n - c) as f64 - i as f64;
if ni <= 0.0 || nci < 0.0 {
return 1.0;
}
result *= nci / ni;
}
1.0 - result
}
#[cfg(test)]
mod pass_at_k_tests {
use super::*;
#[test]
fn single_sample_pass_at_k_collapses_to_pass_at_1() {
let problems: Vec<(String, String, usize)> = (0..164)
.map(|i| (format!("t{i}"), "ep".to_string(), usize::from(i < 50)))
.collect();
let expected = 50.0 / 164.0;
for (k, rate) in compute_multisample_pass_at_k(&problems, 1, &[1, 10, 100]) {
assert!(
(rate - expected).abs() < 1e-9,
"pass@{k} = {rate}, expected {expected} (single greedy sample) — pre-fix inflated to ~0.98/1.0"
);
}
}
}
#[cfg(test)]
mod code_eval_tests {
use super::*;
use std::path::PathBuf;
#[test]
fn pass_at_k_zero_problems_is_zero() {
assert_eq!(compute_pass_at_k(0, 0, 1), 0.0);
assert_eq!(compute_pass_at_k(0, 5, 3), 0.0);
}
#[test]
fn pass_at_k_zero_k_is_zero() {
assert_eq!(compute_pass_at_k(10, 5, 0), 0.0);
}
#[test]
fn pass_at_k_all_correct_is_one() {
assert_eq!(compute_pass_at_k(5, 5, 1), 1.0);
assert_eq!(compute_pass_at_k(5, 7, 3), 1.0);
}
#[test]
fn pass_at_k_none_correct_is_zero() {
assert_eq!(compute_pass_at_k(10, 0, 1), 0.0);
assert_eq!(compute_pass_at_k(10, 0, 5), 0.0);
}
#[test]
fn pass_at_k_k_greater_than_n() {
assert_eq!(compute_pass_at_k(3, 1, 5), 1.0);
assert_eq!(compute_pass_at_k(3, 0, 5), 0.0);
}
#[test]
fn pass_at_k_partial_matches_closed_form() {
let p = compute_pass_at_k(5, 1, 1);
assert!((p - 0.2).abs() < 1e-9, "got {p}");
let p2 = compute_pass_at_k(5, 2, 2);
assert!((p2 - 0.7).abs() < 1e-9, "got {p2}");
}
#[test]
fn pass_at_k_monotonic_in_k() {
let a = compute_pass_at_k(10, 3, 1);
let b = compute_pass_at_k(10, 3, 3);
let c = compute_pass_at_k(10, 3, 5);
assert!(a <= b + 1e-12);
assert!(b <= c + 1e-12);
}
#[test]
fn multisample_empty_is_zero_for_all_k() {
let problems: Vec<(String, String, usize)> = vec![];
for (_k, rate) in compute_multisample_pass_at_k(&problems, 1, &[1, 5, 10]) {
assert_eq!(rate, 0.0);
}
}
#[test]
fn multisample_all_solved_single_sample_is_one() {
let problems: Vec<(String, String, usize)> = (0..4)
.map(|i| (format!("t{i}"), String::new(), 1))
.collect();
for (_k, rate) in compute_multisample_pass_at_k(&problems, 1, &[1, 10]) {
assert!((rate - 1.0).abs() < 1e-9);
}
}
#[test]
fn multisample_multi_sample_averages_per_problem() {
let problems = vec![
("p0".to_string(), "e".to_string(), 4usize),
("p1".to_string(), "e".to_string(), 0usize),
];
let rates = compute_multisample_pass_at_k(&problems, 4, &[1]);
assert_eq!(rates.len(), 1);
assert!((rates[0].1 - 0.5).abs() < 1e-9, "got {}", rates[0].1);
}
#[test]
fn build_passk_json_core_fields() {
let problems = vec![
("HumanEval/0".to_string(), "foo".to_string(), 2usize),
("HumanEval/1".to_string(), "bar".to_string(), 0usize),
];
let model = PathBuf::from("/models/m.apr");
let json = build_passk_json(
"humaneval",
&model,
&problems,
5,
0.8,
&[1, 10],
12.5,
"inference",
None,
);
assert_eq!(json["benchmark"], "humaneval");
assert_eq!(json["problems"], 2);
assert_eq!(json["passed"], 1); assert_eq!(json["samples_per_problem"], 5);
assert_eq!(json["mode"], "inference");
assert_eq!(json["pass_at_k"].as_array().unwrap().len(), 2);
let per = json["per_problem_results"].as_array().unwrap();
assert_eq!(per[0]["entry_point"], "foo");
assert_eq!(per[0]["passed"], true);
assert_eq!(per[1]["passed"], false);
}
#[test]
fn build_passk_json_omits_empty_entry_point() {
let problems = vec![("t".to_string(), String::new(), 1usize)];
let model = PathBuf::from("m.apr");
let json = build_passk_json(
"mbpp",
&model,
&problems,
1,
0.0,
&[1],
1.0,
"structural",
None,
);
let per = json["per_problem_results"].as_array().unwrap();
assert!(per[0].get("entry_point").is_none());
}
#[test]
fn build_passk_json_includes_extra_kv() {
let problems = vec![("t".to_string(), "e".to_string(), 0usize)];
let model = PathBuf::from("m.apr");
let json = build_passk_json(
"humaneval",
&model,
&problems,
1,
0.0,
&[1],
1.0,
"inference_failed",
Some(("error", "spawn failed")),
);
assert_eq!(json["error"], "spawn failed");
}
fn problem(prompt: &str, test: &str, canonical: Option<&str>) -> CodeBenchProblem {
CodeBenchProblem {
prompt: prompt.to_string(),
test: test.to_string(),
task_id: None,
canonical_solution: canonical.map(String::from),
}
}
#[test]
fn evaluate_empty_prompt_fails() {
let p = problem(" ", "assert f() == 1", Some("return 1"));
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(!r.passed);
assert_eq!(r.error.as_deref(), Some("Empty prompt"));
}
#[test]
fn evaluate_empty_test_fails() {
let p = problem("def f(): pass", " ", Some("return 1"));
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(!r.passed);
assert_eq!(r.error.as_deref(), Some("Empty test assertion"));
}
#[test]
fn evaluate_valid_canonical_solution_passes() {
let p = problem("def add(a,b):", "assert add(1,2)==3", Some("return a + b"));
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(r.passed);
assert!(r.error.is_none());
}
#[test]
fn evaluate_canonical_without_code_markers_fails() {
let p = problem("def f():", "assert f()", Some("pass"));
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(!r.passed);
assert_eq!(
r.error.as_deref(),
Some("Canonical solution validation failed")
);
}
#[test]
fn evaluate_no_canonical_requires_inference() {
let p = problem("def f():", "assert f()", None);
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(!r.passed);
assert!(r.error.unwrap().contains("Inference required"));
}
#[test]
fn evaluate_canonical_with_print_passes() {
let p = problem("def f():", "assert True", Some("print('hi')"));
let r = evaluate_code_problem(Path::new("m.apr"), &p, 64).unwrap();
assert!(r.passed);
}
#[test]
fn print_code_eval_results_json_mode_ok() {
let problems = vec![problem("def a():", "assert True", Some("return 1"))];
let results = vec![CodeBenchResult {
passed: true,
error: None,
}];
let r = print_code_eval_results(
Path::new("m.apr"),
Path::new("bench.jsonl"),
&problems,
&results,
1.5,
50.0,
true,
);
assert!(r.is_ok());
}
#[test]
fn print_code_eval_results_human_mode_ok() {
let problems = vec![
problem("def a():", "assert True", None),
problem("def b():", "assert True", None),
];
let results = vec![
CodeBenchResult {
passed: true,
error: None,
},
CodeBenchResult {
passed: false,
error: Some("boom".to_string()),
},
];
let r = print_code_eval_results(
Path::new("m.apr"),
Path::new("bench.jsonl"),
&problems,
&results,
2.0,
90.0,
false,
);
assert!(r.is_ok());
}
#[test]
fn print_code_eval_results_empty_is_zero_rate() {
let problems: Vec<CodeBenchProblem> = vec![];
let results: Vec<CodeBenchResult> = vec![];
let r = print_code_eval_results(
Path::new("m.apr"),
Path::new("bench.jsonl"),
&problems,
&results,
0.0,
0.0,
true,
);
assert!(r.is_ok());
}
#[test]
fn print_multisample_table_smoke() {
let problems = vec![
("p0".to_string(), "e".to_string(), 2usize),
("p1".to_string(), "e".to_string(), 0usize),
];
print_multisample_table(&problems, 4, 0.7, &[1, 4]);
}
#[test]
fn multisample_loop_accumulates_correct_counts() {
let mut acc = vec![
("p0".to_string(), "e".to_string(), 0usize),
("p1".to_string(), "e".to_string(), 0usize),
];
let ok = run_multisample_loop::<_, ()>(&mut acc, 3, true, || {
Ok((
1,
vec![
("p0".to_string(), "e".to_string(), true),
("p1".to_string(), "e".to_string(), false),
],
))
});
assert!(ok);
assert_eq!(acc[0].2, 3); assert_eq!(acc[1].2, 0); }
#[test]
fn multisample_loop_first_sample_error_aborts() {
let mut acc = vec![("p0".to_string(), "e".to_string(), 0usize)];
let ok = run_multisample_loop::<_, &str>(&mut acc, 5, true, || Err("boom"));
assert!(!ok); assert_eq!(acc[0].2, 0);
}
#[test]
fn multisample_loop_later_errors_tolerated() {
let mut acc = vec![("p0".to_string(), "e".to_string(), 0usize)];
let mut call = 0;
let ok = run_multisample_loop::<_, &str>(&mut acc, 3, true, || {
call += 1;
if call == 1 {
Ok((1, vec![("p0".to_string(), "e".to_string(), true)]))
} else {
Err("transient")
}
});
assert!(ok); assert_eq!(acc[0].2, 1); }
#[test]
fn run_code_eval_missing_data_path_errors() {
let r = run_code_eval(Path::new("m.apr"), None, 64, 50.0, true);
assert!(r.is_err());
}
#[test]
fn run_code_eval_nonexistent_data_errors() {
let r = run_code_eval(
Path::new("/models/m.apr"),
Some(Path::new("/nonexistent/bench.jsonl")),
64,
50.0,
true,
);
assert!(r.is_err());
}
#[test]
fn run_code_eval_nonexistent_model_errors() {
let dir = std::env::temp_dir().join(format!("apr_codeeval_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bench = dir.join("bench.jsonl");
std::fs::write(
&bench,
"{\"prompt\":\"def f():\",\"test\":\"assert f()\"}\n",
)
.unwrap();
let r = run_code_eval(
Path::new("/definitely/missing/model.apr"),
Some(&bench),
64,
50.0,
true,
);
assert!(r.is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_code_eval_empty_benchmark_errors() {
let dir = std::env::temp_dir().join(format!("apr_codeeval_empty_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bench = dir.join("bench.jsonl");
std::fs::write(&bench, "\n \n").unwrap();
let model = dir.join("m.apr");
std::fs::write(&model, b"x").unwrap();
let r = run_code_eval(&model, Some(&bench), 64, 50.0, true);
assert!(r.is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_code_eval_invalid_json_errors() {
let dir = std::env::temp_dir().join(format!("apr_codeeval_badjson_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bench = dir.join("bench.jsonl");
std::fs::write(&bench, "not json at all\n").unwrap();
let model = dir.join("m.apr");
std::fs::write(&model, b"x").unwrap();
let r = run_code_eval(&model, Some(&bench), 64, 50.0, true);
assert!(r.is_err());
let _ = std::fs::remove_dir_all(&dir);
}
}