judge_framework/backend/
mod.rs

1mod error;
2mod runner;
3mod sandbox;
4mod util;
5
6use std::{ffi::CStr, path::PathBuf, time::Duration};
7
8use const_format::formatcp;
9pub use error::{Error, Result};
10use nix::sys::resource::Resource;
11pub use runner::Runner;
12pub use sandbox::RlimitConfig;
13
14pub struct TestCase {
15    input_path: PathBuf,
16    output_path: PathBuf,
17}
18
19#[derive(Debug, PartialEq)]
20pub enum JudgeVerdict {
21    Accepted,
22    WrongAnswer { diff: String },
23    TimeLimitExceeded,
24    RuntimeError,
25}
26
27#[derive(Debug)]
28pub struct JudgeResult {
29    pub verdict: JudgeVerdict,
30    pub run_time: Duration,
31}
32
33#[derive(Debug, Clone, Copy)]
34pub struct Command {
35    pub binary: &'static str,
36    pub args: &'static [&'static str],
37}
38
39#[derive(Debug, Clone, Copy)]
40pub struct RawCommand {
41    pub binary: &'static CStr,
42    pub args: &'static [&'static CStr],
43}
44
45const MAIN_FILE_NAME: &str = "Main";
46
47const DEFAULT_RLIMIT_CONFIGS: [RlimitConfig; 5] = [
48    RlimitConfig {
49        resource: Resource::RLIMIT_STACK,
50        soft_limit: 1024 * 1024 * 1024 * 1024,
51        hard_limit: 1024 * 1024 * 1024 * 1024,
52    },
53    RlimitConfig {
54        resource: Resource::RLIMIT_AS,
55        soft_limit: 1024 * 1024 * 1024 * 1024,
56        hard_limit: 1024 * 1024 * 1024 * 1024,
57    },
58    RlimitConfig {
59        resource: Resource::RLIMIT_CPU,
60        soft_limit: 60,
61        hard_limit: 90,
62    },
63    RlimitConfig {
64        resource: Resource::RLIMIT_NPROC,
65        soft_limit: 1,
66        hard_limit: 1,
67    },
68    RlimitConfig {
69        resource: Resource::RLIMIT_FSIZE,
70        soft_limit: 1024,
71        hard_limit: 1024,
72    },
73];
74
75const DEFAULT_SCMP_BLACK_LIST: [&str; 4] = ["clone", "clone3", "fork", "vfork"];
76
77pub const CPP_RUNNER: Runner = Runner {
78    extension: "cpp",
79    compile_command: Some(Command {
80        binary: "g++",
81        args: &["-o", MAIN_FILE_NAME, formatcp!("{MAIN_FILE_NAME}.cpp")],
82    }),
83    run_command: RawCommand {
84        //WARN: hard coded binary name
85        binary: c"./Main",
86        args: &[],
87    },
88    time_out: 2,
89    rlimit_config: &DEFAULT_RLIMIT_CONFIGS,
90    scmp_black_list: &DEFAULT_SCMP_BLACK_LIST,
91
92    #[cfg(test)]
93    infinite_loop_code: r#"
94int main() {
95    while(1) {}
96}
97    "#,
98    #[cfg(test)]
99    runtime_error_code: r#"
100#include <bits/stdc++.h>
101
102using namespace std;
103
104int main() {
105    vector<int> a;
106    a[1] = 0;
107}
108    "#,
109    #[cfg(test)]
110    compilation_error_code: "0eae2a2a-8f74-43f6-b486-646ce38f6d21",
111};
112
113pub const JAVA_RUNNER: Runner = Runner {
114    extension: "java",
115    compile_command: Some(Command {
116        binary: "javac",
117        args: &[formatcp!("{MAIN_FILE_NAME}.java")],
118    }),
119    run_command: RawCommand {
120        binary: c"java",
121        //WARN: hard coded entry file name
122        args: &[c"java", c"Main"],
123    },
124    time_out: 4,
125    // remove nproc limit since JVM will need to create thread to start
126    rlimit_config: &[
127        RlimitConfig {
128            resource: Resource::RLIMIT_STACK,
129            soft_limit: 1024 * 1024 * 1024 * 1024,
130            hard_limit: 1024 * 1024 * 1024 * 1024,
131        },
132        RlimitConfig {
133            resource: Resource::RLIMIT_AS,
134            soft_limit: 1024 * 1024 * 1024 * 1024,
135            hard_limit: 1024 * 1024 * 1024 * 1024,
136        },
137        RlimitConfig {
138            resource: Resource::RLIMIT_CPU,
139            soft_limit: 60,
140            hard_limit: 90,
141        },
142        RlimitConfig {
143            resource: Resource::RLIMIT_FSIZE,
144            soft_limit: 1024,
145            hard_limit: 1024,
146        },
147    ],
148    scmp_black_list: &["fork", "vfork"],
149
150    #[cfg(test)]
151    infinite_loop_code: r#"
152class Main {
153    public static void main(String[] args) {
154        while(true) {}
155    }
156}
157    "#,
158    #[cfg(test)]
159    runtime_error_code: r#"
160class Main {
161    public static void main(String[] args) {
162        throw new RuntimeException("Test runtime exception");
163    }
164}
165    "#,
166    #[cfg(test)]
167    compilation_error_code: "0eae2a2a-8f74-43f6-b486-646ce38f6d21",
168};
169
170pub const PYTHON_RUNNER: Runner = Runner {
171    extension: "py",
172    compile_command: None,
173    run_command: RawCommand {
174        binary: c"python",
175        //WARN: hard coded entry file name
176        args: &[c"python", c"Main.py"],
177    },
178    time_out: 10,
179    rlimit_config: &DEFAULT_RLIMIT_CONFIGS,
180    scmp_black_list: &DEFAULT_SCMP_BLACK_LIST,
181
182    #[cfg(test)]
183    infinite_loop_code: r#"
184i = 0
185while True:
186    i = 1
187    "#,
188    #[cfg(test)]
189    runtime_error_code: "3ff926be-8e1c-4637-a248-58405ccf04e0",
190    #[cfg(test)]
191    compilation_error_code: "",
192};
193
194#[cfg(test)]
195mod tests {
196    use std::assert_matches::assert_matches;
197    use std::fs;
198    use std::path::{Path, PathBuf};
199
200    use rstest::rstest;
201
202    use super::*;
203
204    impl TestCase {
205        pub fn null() -> Self {
206            TestCase {
207                input_path: Path::new("/dev/null").to_path_buf(),
208                output_path: Path::new("/dev/null").to_path_buf(),
209            }
210        }
211    }
212
213    fn read_code(problem_path: PathBuf, extension: &str) -> String {
214        let mut code_path = problem_path.clone();
215        code_path.push("code");
216        code_path.set_extension(extension);
217
218        String::from_utf8(fs::read(code_path).unwrap()).unwrap()
219    }
220
221    fn read_test_cases(problem_path: PathBuf) -> Vec<TestCase> {
222        let test_cases_dir = problem_path.join("test_cases");
223        fs::read_dir(test_cases_dir)
224            .unwrap()
225            .flatten()
226            .map(|test_case_dir| test_case_dir.path())
227            .map(|test_case_dir| TestCase {
228                input_path: test_case_dir.join("in.txt"),
229                output_path: test_case_dir.join("out.txt"),
230            })
231            .collect()
232    }
233
234    #[rstest]
235    #[trace]
236    #[tokio::test]
237    async fn test_should_pass_all(
238        #[values(CPP_RUNNER, JAVA_RUNNER, PYTHON_RUNNER)] runner: Runner,
239        #[files("test/data/backend/**")]
240        #[exclude("test_cases")]
241        problem_path: PathBuf,
242    ) {
243        let code = read_code(problem_path.clone(), runner.extension);
244        let test_cases = read_test_cases(problem_path);
245
246        let details = runner.run(code.as_str(), test_cases).await.unwrap();
247
248        let is_all_passed = details
249            .iter()
250            .all(|detail| detail.verdict == JudgeVerdict::Accepted);
251        assert!(is_all_passed)
252    }
253
254    #[rstest]
255    #[trace]
256    #[tokio::test]
257    async fn test_should_fail_all(
258        #[values(CPP_RUNNER, JAVA_RUNNER, PYTHON_RUNNER)] runner: Runner,
259        #[files("test/data/backend/**")]
260        #[exclude("test_cases")]
261        problem_path: PathBuf,
262    ) {
263        let code = read_code(problem_path.clone(), runner.extension);
264        let test_cases = read_test_cases(problem_path)
265            .into_iter()
266            .map(|mut test_case| {
267                test_case.output_path = Path::new("/dev/null").to_path_buf();
268                test_case
269            })
270            .collect();
271
272        let details = runner.run(code.as_str(), test_cases).await.unwrap();
273
274        for detail in details {
275            assert_matches!(detail.verdict, JudgeVerdict::WrongAnswer { diff: _ });
276        }
277    }
278
279    #[rstest]
280    #[trace]
281    #[tokio::test]
282    async fn test_should_timed_out(
283        #[values(CPP_RUNNER, JAVA_RUNNER, PYTHON_RUNNER)] runner: Runner,
284    ) {
285        let code = runner.infinite_loop_code;
286        let test_cases = vec![TestCase::null()];
287
288        let details = runner.run(code, test_cases).await.unwrap();
289
290        let is_all_timed_out = details
291            .iter()
292            .all(|detail| detail.verdict == JudgeVerdict::TimeLimitExceeded);
293        assert!(is_all_timed_out)
294    }
295
296    #[rstest]
297    #[trace]
298    #[tokio::test]
299    async fn test_should_compile_fail(#[values(CPP_RUNNER, JAVA_RUNNER)] runner: Runner) {
300        let code = &runner.compilation_error_code;
301        let test_cases = vec![];
302
303        assert_matches!(
304            runner.run(code, test_cases).await,
305            Err(Error::Compilation { message: _ })
306        );
307    }
308
309    #[rstest]
310    #[trace]
311    #[tokio::test]
312    async fn test_should_error_at_runtime(
313        #[values(CPP_RUNNER, JAVA_RUNNER, PYTHON_RUNNER)] runner: Runner,
314    ) {
315        let code = runner.runtime_error_code;
316        let test_cases = vec![TestCase::null()];
317
318        let details = runner.run(code, test_cases).await.unwrap();
319
320        let is_all_runtime_error = details
321            .iter()
322            .all(|detail| detail.verdict == JudgeVerdict::RuntimeError);
323        assert!(is_all_runtime_error)
324    }
325}