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 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 args: &[c"java", c"Main"],
123 },
124 time_out: 4,
125 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 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}