use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use anyhow::Result;
use clap::{Args, Parser, Subcommand, ValueEnum};
use newt_eval::{
cases, default_evaluators, evaluator_by_name, resolve_worker_bin, run_case, CaseScorecard,
EvalContext, RunnerConfig, Scorecard, TestCase,
};
#[path = "newt-eval/help_suite.rs"]
mod help_suite;
#[derive(Parser, Debug)]
#[command(
name = "newt-eval",
version,
about = "End-to-end evaluation framework for the newt worker"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
ListCases {
#[arg(long)]
cases_dir: Option<PathBuf>,
},
Run(RunArgs),
Score(ScoreArgs),
Manifest(ManifestArgs),
Verify(VerifyArgs),
Grade(GradeArgs),
}
#[derive(Args, Debug)]
struct ManifestArgs {
#[arg(long)]
workspace: PathBuf,
}
#[derive(Args, Debug)]
struct VerifyArgs {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
manifest_from: PathBuf,
}
#[derive(Args, Debug)]
struct ScoreArgs {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
surface_dir: Option<PathBuf>,
#[arg(long)]
json: bool,
}
#[derive(Args, Debug)]
struct GradeArgs {
#[arg(long)]
case: String,
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
cases_dir: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(long)]
pre_run: bool,
#[arg(long)]
spec_cid_before: Option<String>,
#[arg(long)]
spec_visible_before: bool,
}
#[derive(Args, Debug)]
struct RunArgs {
#[arg(long, value_enum, default_value_t = Mode::Live)]
mode: Mode,
#[arg(long)]
case: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
cases_dir: Option<PathBuf>,
#[arg(long)]
worker_bin: Option<PathBuf>,
#[arg(long)]
coder: bool,
#[arg(long, env = "NEWT_EVAL_WORKER_TIMEOUT_MS", default_value_t = 60_000)]
worker_timeout_ms: u64,
#[arg(long, value_delimiter = ',')]
difficulty: Vec<String>,
#[arg(long)]
legacy_exit_codes: bool,
#[arg(long)]
json: bool,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum Mode {
Live,
Mock,
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.init();
match real_main().await {
Ok(RunOutcomeStatus::AllPassed) => ExitCode::SUCCESS,
Ok(RunOutcomeStatus::RunnerFailed) => ExitCode::from(1),
Ok(RunOutcomeStatus::CaseFailed) => ExitCode::from(2),
Err(e) => {
eprintln!("newt-eval: {e:#}");
ExitCode::FAILURE
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RunOutcomeStatus {
AllPassed,
RunnerFailed,
CaseFailed,
}
async fn real_main() -> Result<RunOutcomeStatus> {
let cli = help_suite::parse_with_help::<Cli>()?;
match cli.command {
Command::ListCases { cases_dir } => {
let dir = cases_dir.unwrap_or_else(cases::default_cases_dir);
let cases = cases::load_all(&dir)?;
println!("{} bundled cases under {}:", cases.len(), dir.display());
for c in &cases {
println!(" {:<28} {} {}", c.name, c.language, c.description);
}
Ok(RunOutcomeStatus::AllPassed)
}
Command::Run(args) => run_command(args).await,
Command::Score(args) => score_command(args),
Command::Manifest(args) => manifest_command(args),
Command::Verify(args) => verify_command(args),
Command::Grade(args) => grade_command(args),
}
}
fn grade_command(args: GradeArgs) -> Result<RunOutcomeStatus> {
let cases_dir = args.cases_dir.unwrap_or_else(cases::default_cases_dir);
let case = cases::load_all(&cases_dir)?
.into_iter()
.find(|c| c.name == args.case)
.ok_or_else(|| {
anyhow::anyhow!("case '{}' not found in {}", args.case, cases_dir.display())
})?;
if args.pre_run {
let pre = newt_eval::pre_run(&case, &args.workspace)?;
println!("{}", serde_json::to_string(&pre)?);
return Ok(RunOutcomeStatus::AllPassed);
}
let pre = newt_eval::PreRun {
spec_cid: match args.spec_cid_before.as_deref() {
Some("none") => None,
Some(cid) => Some(cid.to_string()),
None => newt_eval::pre_run(&case, &args.workspace)?.spec_cid,
},
spec_visible: args.spec_visible_before,
};
let mut scorecard = Scorecard::new();
scorecard.push(newt_eval::grade_workspace(&case, &args.workspace, &pre)?);
if args.json {
println!("{}", serde_json::to_string_pretty(&scorecard)?);
} else {
print!("{scorecard}");
}
Ok(if scorecard.all_passed() {
RunOutcomeStatus::AllPassed
} else {
RunOutcomeStatus::CaseFailed
})
}
fn verify_command(args: VerifyArgs) -> Result<RunOutcomeStatus> {
let surface = newt_core::ffi_manifest::FfiManifest::from_workspace(&args.manifest_from)?;
if surface.is_empty() {
anyhow::bail!(
"no PyO3 bindings under {} to build a surface from",
args.manifest_from.display()
);
}
let report =
newt_core::verify_gate::gate_python_workspace(&args.workspace, &surface.known_modules())?;
let revert = report.revert_set();
if report.accept() {
println!(
"verify: ACCEPT — {} file(s), no fabricated imports",
report.files.len()
);
Ok(RunOutcomeStatus::AllPassed)
} else {
println!(
"verify: REVERT {} file(s), {} fabricated import(s):",
revert.len(),
report.fabrication_count()
);
for f in &report.files {
if !f.is_clean() {
let mods: Vec<&str> = f.fabrications.iter().map(|x| x.module.as_str()).collect();
println!(" {} [{}]", f.path.display(), mods.join(", "));
}
}
Ok(RunOutcomeStatus::CaseFailed)
}
}
fn manifest_command(args: ManifestArgs) -> Result<RunOutcomeStatus> {
let manifest = newt_core::ffi_manifest::FfiManifest::from_workspace(&args.workspace)?;
if manifest.is_empty() {
anyhow::bail!(
"no PyO3 bindings found under {} (looked for <crate>/src/pyo3_module.rs)",
args.workspace.display()
);
}
print!("{}", manifest.render_block());
Ok(RunOutcomeStatus::AllPassed)
}
fn score_command(args: ScoreArgs) -> Result<RunOutcomeStatus> {
let surface_dir = args.surface_dir.unwrap_or_else(|| args.workspace.clone());
let result = newt_eval::score::score_python_workspace(&args.workspace, &surface_dir)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!(
"{}: {} (score {:.2})",
result.evaluator,
if result.passed { "PASS" } else { "FAIL" },
result.score
);
println!(" {}", result.details);
}
Ok(if result.passed {
RunOutcomeStatus::AllPassed
} else {
RunOutcomeStatus::CaseFailed
})
}
async fn run_command(args: RunArgs) -> Result<RunOutcomeStatus> {
let RunArgs {
mode,
case: case_filter,
model,
cases_dir,
worker_bin,
coder,
worker_timeout_ms,
difficulty,
legacy_exit_codes,
json,
} = args;
if let Mode::Mock = mode {
anyhow::bail!(
"mock mode is implemented by `cargo test -p newt-eval --test mock_e2e` — \
this binary only runs live mode"
);
}
let dir = cases_dir.unwrap_or_else(cases::default_cases_dir);
let all = cases::load_all(&dir)?;
let by_name: Vec<TestCase> = match &case_filter {
Some(needle) => all
.into_iter()
.filter(|c| c.name.contains(needle))
.collect(),
None => all,
};
let cases = cases::filter_by_difficulty(by_name, &difficulty);
if cases.is_empty() {
anyhow::bail!("no cases matched filters (case={case_filter:?}, difficulty={difficulty:?})");
}
let resolution = resolve_worker_bin(worker_bin);
if !resolution.found {
let msg = format!(
"newt worker binary not found. Tried:\n{}\nPass --worker-bin <PATH>, \
set NEWT_WORKER_BIN, or run `cargo build --release --bin newt`.",
resolution.render_candidates()
);
eprintln!("newt-eval: {msg}");
let mut scorecard = Scorecard::new();
for case in &cases {
scorecard.cases.push(CaseScorecard {
case_name: case.name.clone(),
results: vec![newt_eval::EvalResult::fail("runner", msg.clone())],
behavioral: None,
});
}
return Ok(classify_outcome(&scorecard, legacy_exit_codes));
}
let mut config = RunnerConfig::new(&resolution.path);
if let Some(m) = model {
config = config.with_model(m);
}
if coder {
config = config.with_coder_mode(true);
}
config = config.with_timeout(Duration::from_millis(worker_timeout_ms));
let mut scorecard = Scorecard::new();
for case in &cases {
let outcome = match run_case(case, &config).await {
Ok(o) => o,
Err(e) => {
eprintln!("[{}] runner error: {e:#}", case.name);
let cs = CaseScorecard {
case_name: case.name.clone(),
results: vec![newt_eval::EvalResult::fail("runner", format!("{e:#}"))],
behavioral: None,
};
scorecard.push(cs);
continue;
}
};
let ctx = EvalContext {
case: outcome.case.clone(),
workspace: outcome.workspace.clone(),
baseline: outcome.baseline.clone(),
reply: outcome.reply.clone(),
};
let results = run_evaluators(&ctx)?;
let behavioral = newt_eval::grade_behavioral(case, &outcome.workspace, &outcome.pre_run);
scorecard.push(CaseScorecard {
case_name: case.name.clone(),
results,
behavioral: Some(behavioral),
});
}
if json {
println!("{}", serde_json::to_string_pretty(&scorecard)?);
} else {
print!("{scorecard}");
}
Ok(classify_outcome(&scorecard, legacy_exit_codes))
}
fn classify_outcome(scorecard: &Scorecard, legacy_exit_codes: bool) -> RunOutcomeStatus {
if scorecard.all_passed() {
return RunOutcomeStatus::AllPassed;
}
if !legacy_exit_codes && scorecard_has_runner_failure(scorecard) {
return RunOutcomeStatus::RunnerFailed;
}
RunOutcomeStatus::CaseFailed
}
fn scorecard_has_runner_failure(scorecard: &Scorecard) -> bool {
scorecard
.cases
.iter()
.flat_map(|c| c.results.iter())
.any(|r| r.evaluator == "runner" && !r.passed)
}
fn run_evaluators(ctx: &EvalContext) -> Result<Vec<newt_eval::EvalResult>> {
if ctx.case.evaluators.is_empty() {
return Ok(default_evaluators()
.iter()
.map(|ev| ev.evaluate(ctx))
.collect());
}
let mut results = Vec::with_capacity(ctx.case.evaluators.len());
for name in &ctx.case.evaluators {
let ev = evaluator_by_name(name).ok_or_else(|| {
anyhow::anyhow!("unknown evaluator '{name}' in case {}", ctx.case.name)
})?;
results.push(ev.evaluate(ctx));
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use newt_eval::EvalResult;
fn sc_with(rows: Vec<(&str, &str, bool)>) -> Scorecard {
let mut sc = Scorecard::new();
for (case_name, evaluator, passed) in rows {
let r = if passed {
EvalResult::pass(evaluator, "ok")
} else {
EvalResult::fail(evaluator, "nope")
};
sc.push(CaseScorecard {
case_name: case_name.into(),
results: vec![r],
behavioral: None,
});
}
sc
}
#[test]
fn classify_outcome_all_passed() {
let sc = sc_with(vec![("a", "diff_applies", true)]);
assert_eq!(classify_outcome(&sc, false), RunOutcomeStatus::AllPassed);
}
#[test]
fn classify_outcome_runner_failure_exits_one() {
let sc = sc_with(vec![("a", "runner", false)]);
assert_eq!(classify_outcome(&sc, false), RunOutcomeStatus::RunnerFailed);
}
#[test]
fn classify_outcome_non_runner_failure_exits_two() {
let sc = sc_with(vec![("a", "diff_applies", false)]);
assert_eq!(classify_outcome(&sc, false), RunOutcomeStatus::CaseFailed);
}
#[test]
fn classify_outcome_legacy_flag_collapses_runner_to_case_failed() {
let sc = sc_with(vec![("a", "runner", false)]);
assert_eq!(classify_outcome(&sc, true), RunOutcomeStatus::CaseFailed);
}
#[test]
fn scorecard_has_runner_failure_detects_buried_fail() {
let mut sc = Scorecard::new();
sc.push(CaseScorecard {
case_name: "a".into(),
results: vec![EvalResult::pass("diff_applies", "ok")],
behavioral: None,
});
sc.push(CaseScorecard {
case_name: "b".into(),
results: vec![
EvalResult::pass("diff_applies", "ok"),
EvalResult::fail("runner", "spawn failed"),
],
behavioral: None,
});
assert!(scorecard_has_runner_failure(&sc));
}
#[test]
fn scorecard_has_runner_failure_ignores_runner_passes() {
let mut sc = Scorecard::new();
sc.push(CaseScorecard {
case_name: "a".into(),
results: vec![EvalResult::pass("runner", "all good")],
behavioral: None,
});
assert!(!scorecard_has_runner_failure(&sc));
}
}