judge_framework/backend/
runner.rs

1use 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    /// Create a unique project using the hash of time and code
57    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        // TODO: use exact exit signal
111        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}