judge_framework/backend/
runner.rs1use std::{
2 fmt::Write,
3 path::{Path, PathBuf},
4 process::Stdio,
5 time::Duration,
6};
7
8use nix::libc::{EXIT_SUCCESS, SIGALRM};
9use rayon::iter::{IntoParallelRefIterator, ParallelIterator as _};
10use similar::{ChangeTag, TextDiff};
11use tempfile::NamedTempFile;
12use tokio::{fs, io::AsyncWriteExt as _};
13
14use super::{
15 sandbox::{RlimitConfig, Sandbox},
16 util, Command, Error, JudgeResult, JudgeVerdict, RawCommand, Result, TestCase, MAIN_FILE_NAME,
17};
18
19#[derive(Debug)]
20pub struct Runner {
21 pub extension: &'static str,
22 pub compile_command: Option<Command>,
23 pub run_command: RawCommand,
24 pub time_out: u32,
25 pub rlimit_config: &'static [RlimitConfig],
26 pub scmp_black_list: &'static [&'static str],
27 #[cfg(test)]
28 pub infinite_loop_code: &'static str,
29 #[cfg(test)]
30 pub compilation_error_code: &'static str,
31 #[cfg(test)]
32 pub runtime_error_code: &'static str,
33}
34
35impl Runner {
36 async fn create_unique_project(&self, code: &str) -> Result<PathBuf> {
37 let project_path = util::generate_unique_path(code);
38
39 fs::create_dir_all(&project_path).await?;
40
41 let mut main_file_path = project_path.clone();
42 main_file_path.push(MAIN_FILE_NAME);
43 main_file_path.set_extension(self.extension);
44
45 let mut main_file = fs::OpenOptions::new()
46 .create_new(true)
47 .write(true)
48 .open(&main_file_path)
49 .await?;
50
51 main_file.write_all(code.as_bytes()).await?;
52
53 Ok(project_path)
54 }
55
56 async fn compile(&self, project_path: &Path) -> Result<()> {
58 let Some(Command {
59 binary: compiler,
60 args,
61 }) = self.compile_command
62 else {
63 return Ok(());
64 };
65
66 let process = tokio::process::Command::new(compiler)
67 .args(args)
68 .current_dir(project_path)
69 .stderr(Stdio::piped())
70 .spawn()?;
71
72 let compilation_error = process.wait_with_output().await?.stderr;
73
74 if !compilation_error.is_empty() {
75 return Err(Error::Compilation {
76 message: String::from_utf8(compilation_error)?,
77 });
78 }
79
80 Ok(())
81 }
82
83 fn run_one(&self, project_path: &Path, test_case: &TestCase) -> Result<JudgeResult> {
84 let user_output_path = NamedTempFile::new()?;
85 let user_error_path = NamedTempFile::new()?;
86
87 let mut sandbox = Sandbox::new(
88 self.scmp_black_list,
89 self.rlimit_config,
90 self.time_out,
91 project_path.to_path_buf(),
92 self.run_command,
93 &test_case.input_path,
94 user_output_path.path(),
95 user_error_path.path(),
96 )?;
97
98 sandbox.spawn()?;
99 let raw_run_result = sandbox.wait()?;
100 if raw_run_result.exit_signal == SIGALRM {
101 return Ok(JudgeResult {
102 verdict: JudgeVerdict::TimeLimitExceeded,
103 run_time: Duration::from_secs(self.time_out as u64),
104 });
105 }
106
107 let run_time = raw_run_result.real_time_cost;
108
109 let runtime_error = std::fs::read_to_string(&user_error_path)?;
110 if !runtime_error.is_empty() || raw_run_result.exit_signal != EXIT_SUCCESS {
112 return Ok(JudgeResult {
113 verdict: JudgeVerdict::RuntimeError,
114 run_time,
115 });
116 }
117
118 let output = std::fs::read_to_string(&user_output_path)?;
119 let correct_output = std::fs::read_to_string(&test_case.output_path)?;
120 let diff = TextDiff::from_lines(output.as_str().trim(), correct_output.as_str().trim())
121 .iter_all_changes()
122 .filter(|change| change.tag() != ChangeTag::Equal)
123 .fold(String::with_capacity(1000), |mut output, change| {
124 let sign = match change.tag() {
125 ChangeTag::Delete => "-",
126 ChangeTag::Insert => "+",
127 _ => unreachable!()
128 };
129 let _ = writeln!(output, "{}{}", sign, change);
130 output
131 });
132
133 if !diff.is_empty() {
134 return Ok(JudgeResult {
135 verdict: JudgeVerdict::WrongAnswer { diff },
136 run_time,
137 });
138 }
139
140 Ok(JudgeResult {
141 verdict: JudgeVerdict::Accepted,
142 run_time,
143 })
144 }
145
146 pub async fn run(&self, code: &str, test_cases: Vec<TestCase>) -> Result<Vec<JudgeResult>> {
147 let project_path = self.create_unique_project(code).await?;
148
149 self.compile(&project_path).await?;
150
151 let judge_results: Vec<JudgeResult> = test_cases
152 .par_iter()
153 .map(|test_case| self.run_one(&project_path, test_case))
154 .collect::<Result<_>>()?;
155
156 Ok(judge_results)
157 }
158}