ahc_evaluation/evaluation/
mod.rs1mod record;
2mod stop_watch;
3
4use std::{
5 fs::{create_dir_all, read_to_string, File},
6 io::Write,
7 process::{Command, Stdio},
8};
9
10use anyhow::{bail, ensure, Context};
11use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
12use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
13use regex::Regex;
14
15use crate::{
16 config::Config,
17 evaluation::{record::EvaluationRecord, stop_watch::Stopwatch},
18};
19
20pub use crate::evaluation::record::{show_statistics, write_to_csv};
21
22pub fn evaluate(config: &Config, seeds: &[usize]) -> anyhow::Result<Vec<EvaluationRecord>> {
24 let progress_style = ProgressStyle::template(
26 ProgressStyle::default_bar(),
27 "{prefix}\n{wide_bar} {pos:>3}/{len:3} {percent:>3}% [{elapsed_precise}<{eta_precise}]",
28 )
29 .with_context(|| "Failed to create progress bar style.")?;
30
31 let progress_bar = ProgressBar::new(seeds.len() as u64);
33 progress_bar.set_style(progress_style);
34 progress_bar.set_prefix("Running...");
35
36 create_dir_all(&config.path.output_dir)
38 .with_context(|| "Failed to create output directory.")?;
39
40 seeds
42 .par_iter()
43 .progress_with(progress_bar)
44 .map(|&seed| {
45 if config.command.execute.integrated {
46 execute_integrated_process(config, seed)
47 } else {
48 execute_independent_processes(config, seed)
49 }
50 })
51 .collect::<Result<Vec<EvaluationRecord>, _>>()
52}
53
54fn execute_integrated_process(config: &Config, seed: usize) -> anyhow::Result<EvaluationRecord> {
56 let input_file_path = config.input_file_path(seed);
58 let input_text = read_to_string(&input_file_path)
59 .with_context(|| format!("Failed to read text from `{:?}`.", input_file_path))?;
60
61 let cmd_args = config.cmd_args_for_execute_tester(seed);
63 let process_handle = spawn_process(&cmd_args)?;
64
65 let stopwatch = Stopwatch::start();
67
68 process_handle
70 .stdin
71 .as_ref()
72 .unwrap()
73 .write_all(input_text.as_bytes())
74 .with_context(|| "Failed to input text.")?;
75
76 let output = process_handle.wait_with_output()?;
78
79 ensure!(
80 output.status.success(),
81 ExecuteCommandError {
82 seed,
83 cmd_args,
84 output
85 }
86 );
87
88 let execution_time = stopwatch.elapsed_time();
90
91 let output_file_path = config.output_file_path(seed);
93 File::create(&output_file_path)
94 .with_context(|| format!("Failed to create output file `{:?}`.", output_file_path))?
95 .write_all(&output.stdout)
96 .with_context(|| format!("Failed to write to output file {:?}.", output_file_path))?;
97
98 let score_regex = Regex::new(r"\bScore *= *(?<score>[0-9]*)\b")
100 .with_context(|| "Failed to compile regular expression.")?;
101
102 let stderr = String::from_utf8(output.stderr.clone())?;
103
104 let Some(score) = score_regex
106 .captures(&stderr)
107 .and_then(|caps| caps["score"].parse::<i64>().ok())
108 else {
109 bail!(format!(
110 "
111Failed to retrieve score.
112
113Seed: {}
114
115---------------------------- Standard Error Output -----------------------------
116{:?}
117--------------------------------------------------------------------------------
118",
119 seed, output.stderr,
120 ));
121 };
122
123 Ok(EvaluationRecord {
124 seed,
125 score,
126 execution_time,
127 })
128}
129
130fn execute_independent_processes(config: &Config, seed: usize) -> anyhow::Result<EvaluationRecord> {
132 let input_file_path = config.input_file_path(seed);
134 let input_text = read_to_string(&input_file_path)
135 .with_context(|| format!("Failed to read text from `{:?}`.", input_file_path))?;
136
137 let cmd_args_for_execute_submission = &config.command.execute.submission;
139 let submission_process_handle = spawn_process(cmd_args_for_execute_submission)?;
140
141 let stopwatch = Stopwatch::start();
143
144 submission_process_handle
146 .stdin
147 .as_ref()
148 .unwrap()
149 .write_all(input_text.as_bytes())
150 .with_context(|| "Failed to input text.")?;
151
152 let submission_process_output =
154 submission_process_handle
155 .wait_with_output()
156 .with_context(|| {
157 format!(
158 "
159Failed to execute the submission code.
160List of arguments: {:?}
161",
162 cmd_args_for_execute_submission
163 )
164 })?;
165
166 let execution_time = stopwatch.elapsed_time();
168
169 ensure!(
170 submission_process_output.status.success(),
171 ExecuteCommandError {
172 seed,
173 cmd_args: cmd_args_for_execute_submission.to_owned(),
174 output: submission_process_output
175 }
176 );
177
178 let output_file_path = config.output_file_path(seed);
180 File::create(&output_file_path)
181 .with_context(|| format!("Failed to create output file `{:?}`.", output_file_path))?
182 .write_all(&submission_process_output.stdout)
183 .with_context(|| format!("Failed to write to output file {:?}.", output_file_path))?;
184
185 let cmd_args_for_execute_tester = config.cmd_args_for_execute_tester(seed);
187
188 let tester_process_output = spawn_process(&cmd_args_for_execute_tester)?
190 .wait_with_output()
191 .with_context(|| {
192 format!(
193 "
194Failed to execute the local tester.
195List of arguments: {:?}
196",
197 cmd_args_for_execute_tester
198 )
199 })?;
200
201 ensure!(
202 tester_process_output.status.success(),
203 ExecuteCommandError {
204 seed,
205 cmd_args: cmd_args_for_execute_submission.to_owned(),
206 output: tester_process_output
207 }
208 );
209
210 let score_regex = Regex::new(r"\bScore *= *(?<score>[0-9]*)\b")
212 .with_context(|| "Failed to compile regular expression.")?;
213
214 let retrieve_score = |text: &str| {
216 score_regex
217 .captures(text)
218 .and_then(|caps| caps["score"].parse::<i64>().ok())
219 };
220
221 let stdout = String::from_utf8(tester_process_output.stdout.clone())?;
222 let stderr = String::from_utf8(tester_process_output.stderr.clone())?;
223
224 let Some(score) = retrieve_score(&stdout).or(retrieve_score(&stderr)) else {
226 bail!(format!(
227 "
228Failed to retrieve score.
229
230Seed: {}
231
232------------------------------- Standard Output --------------------------------
233{:?}
234--------------------------------------------------------------------------------
235
236---------------------------- Standard Error Output -----------------------------
237{:?}
238--------------------------------------------------------------------------------
239",
240 seed, tester_process_output.stdout, tester_process_output.stderr,
241 ));
242 };
243
244 Ok(EvaluationRecord {
245 seed,
246 score,
247 execution_time,
248 })
249}
250
251fn spawn_process(cmd_args: &[String]) -> anyhow::Result<std::process::Child> {
253 let program = cmd_args
254 .first()
255 .with_context(|| "The execution command is empty.")?;
256
257 Command::new(program)
258 .args(&cmd_args[1..])
259 .stdin(Stdio::piped())
260 .stdout(Stdio::piped())
261 .stderr(Stdio::piped())
262 .spawn()
263 .with_context(|| {
264 format!(
265 "
266Failed to start the child process.
267List of arguments: {:?}
268",
269 cmd_args
270 )
271 })
272}
273
274#[derive(Debug)]
275pub struct ExecuteCommandError {
276 pub seed: usize,
277 pub cmd_args: Vec<String>,
278 pub output: std::process::Output,
279}
280
281impl std::fmt::Display for ExecuteCommandError {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 let stdout = String::from_utf8(self.output.stdout.clone()).unwrap();
284 let stderr = String::from_utf8(self.output.stderr.clone()).unwrap();
285
286 write!(
287 f,
288 "
289Failed to execute command.
290
291Exit code: {}
292
293Seed: {}
294
295Command line arguments: {:?},
296
297------------------------------- Standard Output --------------------------------
298{}
299--------------------------------------------------------------------------------
300
301---------------------------- Standard Error Output -----------------------------
302{}
303--------------------------------------------------------------------------------
304",
305 self.output.status, self.seed, self.cmd_args, stdout, stderr,
306 )
307 }
308}
309
310impl std::error::Error for ExecuteCommandError {}