1mod render;
2
3use crate::output::OutputFormat;
4use aether_evals::{
5 EvalFileError, EvalFilesReport, EvalSpec, EvalSpecLoadOptions, EvalStreamEvent, ResolvedEvalSpec,
6 WorkspaceRetention, run_eval_spec_streaming, run_eval_specs,
7};
8use render::render;
9use std::env::current_dir;
10use std::fs::read_to_string;
11use std::io::{Read, Write, stdin, stdout};
12use std::num::NonZeroUsize;
13use std::path::PathBuf;
14use std::process::ExitCode;
15
16const DEFAULT_MAX_CONCURRENCY: NonZeroUsize = NonZeroUsize::new(5).unwrap();
17
18#[derive(clap::Args)]
19pub struct EvalArgs {
20 pub paths: Vec<PathBuf>,
22
23 #[arg(long)]
25 pub name: Option<String>,
26
27 #[arg(long, default_value_t = DEFAULT_MAX_CONCURRENCY)]
29 pub max_concurrency: NonZeroUsize,
30
31 #[arg(long, default_value = "text")]
33 pub output: OutputFormat,
34
35 #[arg(long, value_name = "PATH_OR_DASH")]
39 pub spec_file: Option<String>,
40
41 #[arg(long)]
44 pub retain_workspace: bool,
45
46 #[arg(long)]
49 pub base_dir: Option<PathBuf>,
50}
51
52pub async fn run(args: EvalArgs) -> Result<ExitCode, EvalFileError> {
53 if let Some(source) = args.spec_file {
54 let base_dir = args.base_dir.or_else(|| current_dir().ok()).unwrap_or_else(|| PathBuf::from("."));
55 let json = read_eval(&source)?;
56 let retention = if args.retain_workspace { WorkspaceRetention::Retain } else { WorkspaceRetention::Discard };
57 let spec: EvalSpec = serde_json::from_str(&json).map_err(|source| EvalFileError::ParseSpec { source })?;
58 let case = ResolvedEvalSpec::resolve(spec, base_dir)?;
59 let outcome = run_eval_spec_streaming(case, retention, |message| {
60 print_json_event(&EvalStreamEvent::AgentMessage { message: message.clone() });
61 })
62 .await?;
63 print_json_event(&EvalStreamEvent::Outcome { outcome });
64 return Ok(ExitCode::SUCCESS);
65 }
66
67 let cases = ResolvedEvalSpec::load(EvalSpecLoadOptions { paths: args.paths, filter: args.name })?;
68 let evals = run_eval_specs(cases, WorkspaceRetention::Discard, args.max_concurrency).await?;
69 let report = EvalFilesReport { evals };
70
71 println!("{}", render(&report, args.output));
72 Ok(if report.passed() { ExitCode::SUCCESS } else { ExitCode::FAILURE })
73}
74
75fn print_json_event(event: &EvalStreamEvent) {
76 println!("{}", serde_json::to_string(event).expect("EvalStreamEvent serializes to JSON"));
77 let _ = stdout().flush();
78}
79
80fn read_eval(source: &str) -> Result<String, EvalFileError> {
81 if source == "-" {
82 let mut json = String::new();
83 stdin()
84 .read_to_string(&mut json)
85 .map_err(|source| EvalFileError::ReadEvalFile { path: PathBuf::from("-"), source })?;
86 return Ok(json);
87 }
88
89 let path = PathBuf::from(source);
90 read_to_string(&path).map_err(|source| EvalFileError::ReadEvalFile { path, source })
91}