From 5867664299189524d24c781d0826eaf7e932debd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= <46275354+fu050409@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:44:06 +0800 Subject: [PATCH] feat: support nodejs (#9) * feat: support nodejs * chore: remove useless code --- .changes/nodejs.md | 12 ++++ .cspell.json | 3 + Cargo.lock | 125 +++++++++++++++++++++++++++------------ Cargo.toml | 4 +- src/case.rs | 21 +++++-- src/compile.rs | 36 +++++------ src/config.rs | 30 +++++++++- src/exec.rs | 79 +++++++++++++++++++++---- src/judge.rs | 100 ++++++++++++++++++++++--------- src/utils.rs | 21 ------- tests/.gitignore | 1 + tests/1.out | 3 +- tests/2.out | 1 + tests/any.in | 0 tests/any.out | 0 tests/fail_to_write.mjs | 3 + tests/fail_to_write.py | 13 ++++ tests/test.cpp | 4 +- tests/test.mjs | 12 ++++ tests/test.py | 3 + tests/test.rs | 13 ++++ tests/test_fs.rs | 74 +++++++++++++++++++++++ tests/test_judge.rs | 128 ++++++++++++++++++++++++++++++++++++++-- 23 files changed, 555 insertions(+), 131 deletions(-) create mode 100644 .changes/nodejs.md create mode 100644 tests/.gitignore create mode 100644 tests/any.in create mode 100644 tests/any.out create mode 100644 tests/fail_to_write.mjs create mode 100644 tests/fail_to_write.py create mode 100644 tests/test.mjs create mode 100644 tests/test.py create mode 100644 tests/test.rs create mode 100644 tests/test_fs.rs diff --git a/.changes/nodejs.md b/.changes/nodejs.md new file mode 100644 index 0000000..b1db179 --- /dev/null +++ b/.changes/nodejs.md @@ -0,0 +1,12 @@ +--- +"eval-stack": minor:feat +--- + +Add support for Deno.js, and other changes: + +- Use deno instead of node for executing JavaScript files, deny all permissions by default. +- Allow stderr to be piped since we can capture it for error messages. +- Disable core dumps by default. +- Set CPU time limits using `libc`. +- Use seccomp to restrict sys calls and fs operations. +- Prevent process to create subprocesses. diff --git a/.cspell.json b/.cspell.json index b3a38f6..998ea55 100644 --- a/.cspell.json +++ b/.cspell.json @@ -10,10 +10,13 @@ "NEWNS", "prctl", "PRIVS", + "pycache", "rlim", "rlimit", "rustc", "rustup", + "seccomp", + "seccompiler", "serde", "setrlimit", "SIGBUS", diff --git a/Cargo.lock b/Cargo.lock index f1bfce1..3aa1f8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "autocfg" @@ -52,9 +52,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cfg-if" @@ -63,20 +63,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cfg_aliases" -version = "0.2.1" +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "errno" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] [[package]] name = "eval-stack" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "libc", - "nix", + "seccompiler", "serde", "tokio", + "which", ] [[package]] @@ -86,16 +97,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] [[package]] name = "libc" -version = "0.2.161" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -124,28 +144,15 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "object" version = "0.36.5" @@ -186,9 +193,9 @@ checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -217,12 +224,34 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seccompiler" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5" +dependencies = [ + "libc", +] + [[package]] name = "serde" version = "1.0.215" @@ -260,9 +289,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys", @@ -270,9 +299,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -281,9 +310,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -310,9 +339,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "wasi" @@ -320,6 +349,18 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "which" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -392,3 +433,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" diff --git a/Cargo.toml b/Cargo.toml index 5a2193c..fe9aa8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ anyhow = "1.0.92" libc = "0.2.161" tokio = { version = "1.41.0", features = ["full"] } -nix = { version = "0.29.0", features = ["user"], optional = true } serde = { version = "1.0.215", features = ["derive"], optional = true } +seccompiler = "0.4.0" +which = "7.0.0" [features] default = ["serde"] -rerun = ["nix"] serde = ["dep:serde"] diff --git a/src/case.rs b/src/case.rs index 10d069b..baaaf22 100644 --- a/src/case.rs +++ b/src/case.rs @@ -1,6 +1,7 @@ use std::{fs::create_dir_all, path::PathBuf, time::Duration}; use anyhow::Result; +use which::which; use crate::{ compile::{compile, Language}, @@ -31,7 +32,8 @@ where .to_string_lossy() .to_string(); let exec_path = match &language { - Language::Python => PathBuf::from("python"), + Language::Python => which("python")?, + Language::NodeJs => which("deno")?, _ => workspace.join("out"), }; @@ -52,9 +54,20 @@ where }]); }; - let args = vec![source_file_path.as_str()]; + let py_args = vec![source_file_path.as_str()]; + let deno_args = vec![ + "run", + format!("--v8-flags=--max-old-space-size={}", options.memory_limit).leak(), + "--deny-read=*", + "--deny-write=*", + "--deny-env=*", + "--deny-run=*", + "--deny-ffi=*", + source_file_path.as_str(), + ]; let args = match language { - Language::Python => Some(&args), + Language::Python => Some(&py_args), + Language::NodeJs => Some(&deno_args), _ => None, } .map(|v| &**v); @@ -73,7 +86,7 @@ where workspace.join("test.out"), ) .await?; - if options.fast_fail && !matches!(result.status, JudgeStatus::Accepted) { + if options.fail_fast && !matches!(result.status, JudgeStatus::Accepted) { results.push(result); break; } diff --git a/src/compile.rs b/src/compile.rs index 7c1bbca..f2fee82 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -9,12 +9,13 @@ use tokio::process::Command; #[derive(Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Language { C, CPP, Rust, Python, - Go, + NodeJs, } pub async fn compile, E: AsRef, O: AsRef>( @@ -29,7 +30,7 @@ pub async fn compile, E: AsRef, O: AsRef>( let output_path = base_path.join(output_file.as_ref()); let output_path_str = output_path.to_string_lossy(); - let mut command = match language { + let command = match language { Language::C => { let mut command = Command::new("gcc"); command.args([ @@ -42,7 +43,7 @@ pub async fn compile, E: AsRef, O: AsRef>( "-o", output_path_str.as_ref(), ]); - command + Some(command) } Language::CPP => { let mut command = Command::new("g++"); @@ -56,7 +57,7 @@ pub async fn compile, E: AsRef, O: AsRef>( "-o", output_path_str.as_ref(), ]); - command + Some(command) } Language::Rust => { let rustc_path = Path::new(&env::var("HOME")?) @@ -75,27 +76,28 @@ pub async fn compile, E: AsRef, O: AsRef>( "-o", output_file.as_ref(), ]); - command + Some(command) } Language::Python => { let mut command = Command::new("python3"); command.args(["-m", "py_compile", source_path_str.as_ref()]); - command + Some(command) } - Language::Go => todo!(), + Language::NodeJs => None, }; - command.kill_on_drop(true).stdout(Stdio::piped()).spawn()?; + if let Some(mut command) = command { + command.kill_on_drop(true).stdout(Stdio::piped()).spawn()?; - let output = command.output().await?; + let output = command.output().await?; - if !output.status.success() { - let error_message = String::from_utf8_lossy(&output.stderr).to_string(); - Err(anyhow::anyhow!( - "Failed to compile source code: {}", - error_message - )) - } else { - Ok(()) + if !output.status.success() { + let error_message = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(anyhow::anyhow!( + "Failed to compile source code: {}", + error_message + )); + } } + Ok(()) } diff --git a/src/config.rs b/src/config.rs index e565975..72c9b79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,35 @@ use std::{path::PathBuf, time::Duration}; pub struct JudgeOptions { pub time_limit: Duration, pub memory_limit: u64, - pub fast_fail: bool, + pub fail_fast: bool, + pub no_startup_limits: bool, +} + +impl Default for JudgeOptions { + fn default() -> Self { + Self { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: false, + no_startup_limits: false, + } + } +} + +impl JudgeOptions { + pub fn fail_fast(mut self, fail_fast: bool) -> Self { + self.fail_fast = fail_fast; + self + } + + pub fn no_fail_fast(self) -> Self { + self.fail_fast(false) + } + + pub fn no_startup_limits(mut self, no_startup_limits: bool) -> Self { + self.no_startup_limits = no_startup_limits; + self + } } pub struct TestCase diff --git a/src/exec.rs b/src/exec.rs index 9d83b43..a775d49 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -7,12 +7,33 @@ use std::{ }; use anyhow::Result; +use seccompiler::{ + BpfProgram, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter, SeccompRule, +}; use crate::{ config::{JudgeOptions, TestCase}, judge::{Judge, JudgeResult}, }; +pub fn seccomp_filter() -> anyhow::Result { + Ok(SeccompFilter::new( + vec![( + libc::SYS_write, + vec![SeccompRule::new(vec![ + SeccompCondition::new(0, SeccompCmpArgLen::Dword, SeccompCmpOp::Ne, 1)?, + SeccompCondition::new(0, SeccompCmpArgLen::Dword, SeccompCmpOp::Ne, 2)?, + ])?], + )] + .into_iter() + .collect(), + seccompiler::SeccompAction::Allow, + seccompiler::SeccompAction::KillProcess, + seccompiler::TargetArch::x86_64, + )? + .try_into()?) +} + pub async fn execute<'a, B, E, I, O>( base: B, exec_path: E, @@ -41,37 +62,73 @@ where .current_dir(base_path) .stdin(Stdio::from(fs::File::open(&input_file)?)) .stdout(Stdio::from(fs::File::create(&output_file)?)) - .stderr(Stdio::null()); + .stderr(Stdio::piped()); + let no_sys_as_limits = options.no_startup_limits; let memory_limit = options.memory_limit; + let time_limit = options.time_limit.as_secs(); unsafe { command.pre_exec(move || { - use libc::{rlimit, setrlimit, RLIMIT_AS}; - + use libc::{rlimit, setrlimit}; + // Close all file descriptors except for stdin, stdout, and stderr for fd in 3..1024 { libc::close(fd); } + // Prevent child from gaining new privileges if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 { panic!( "Failed to disable grant of additional privileges: {}", std::io::Error::last_os_error() ) } + // Unshare the mount namespace to prevent child from gaining new mounts if libc::unshare(libc::CLONE_NEWNS) != 0 { panic!( "Failed to unshare namespace: {}", std::io::Error::last_os_error() ) } - let limit = rlimit { - rlim_cur: memory_limit, - rlim_max: memory_limit, + // Set memory limit + if !no_sys_as_limits { + let limit = rlimit { + rlim_cur: memory_limit, + rlim_max: memory_limit, + }; + if setrlimit(libc::RLIMIT_AS, &limit) != 0 { + panic!( + "Failed to set memory limit: {}", + std::io::Error::last_os_error() + ) + } + let filter = seccomp_filter().unwrap(); + seccompiler::apply_filter(&filter).unwrap(); + } + // Set process limit + let proc_limit = rlimit { + rlim_cur: 0, + rlim_max: 0, }; - if setrlimit(RLIMIT_AS, &limit) != 0 { - panic!( - "Failed to set memory limit: {}", - std::io::Error::last_os_error() - ) + if setrlimit(libc::RLIMIT_NPROC, &proc_limit) != 0 { + return Err(std::io::Error::last_os_error()); + } + // Set CPU time limit + let cpu_limit = rlimit { + rlim_cur: time_limit, + rlim_max: time_limit, + }; + if setrlimit(libc::RLIMIT_CPU, &cpu_limit) != 0 { + return Err(std::io::Error::last_os_error()); + } + // Disable core dumps + if setrlimit( + libc::RLIMIT_CORE, + &rlimit { + rlim_cur: 0, + rlim_max: 0, + }, + ) != 0 + { + return Err(std::io::Error::last_os_error()); } Ok(()) }) diff --git a/src/judge.rs b/src/judge.rs index d4f86ba..e8d1ec8 100644 --- a/src/judge.rs +++ b/src/judge.rs @@ -1,7 +1,7 @@ use std::{ fs, future::Future, - io::{BufRead, BufReader}, + io::{BufRead, BufReader, Read}, os::unix::process::ExitStatusExt, path::PathBuf, task::Poll, @@ -20,10 +20,22 @@ pub enum JudgeStatus { WrongAnswer, TimeLimitExceeded, MemoryLimitExceeded, - RuntimeError, - CompileError { message: String }, - SystemError { code: i32 }, - SegmentFault, + RuntimeError { + code: i32, + stderr: String, + }, + CompileError { + message: String, + }, + SystemError { + code: i32, + stderr: String, + signal: i32, + }, + SegmentFault { + code: i32, + stderr: String, + }, } #[derive(Debug)] @@ -35,6 +47,16 @@ pub struct JudgeResult { pub memory_used: u64, } +impl JudgeResult { + pub fn is_accepted(&self) -> bool { + matches!(self.status, JudgeStatus::Accepted) + } + + pub fn is_wrong_answer(&self) -> bool { + !self.is_accepted() + } +} + pub struct Judge { pub child: std::process::Child, pub id: u32, @@ -65,27 +87,37 @@ impl Future for Judge { let mut stdout_lines = stdout.lines(); let mut expected_out_lines = expected_out.lines(); - let mut matched = true; - while let (Some(output), Some(expected_output)) = - (stdout_lines.next(), expected_out_lines.next()) - { - if output?.trim_end_matches(|c: char| c.is_whitespace() || c == '\n') - != expected_output? - .trim_end_matches(|c: char| c.is_whitespace() || c == '\n') - { - matched = false; - break; - } - } - for extra_line in stdout_lines { - if extra_line?.trim_end_matches(|c: char| c.is_whitespace() || c == '\n') - != "" - { - matched = false; - break; + let matched = loop { + match (stdout_lines.next(), expected_out_lines.next()) { + (None, None) => break true, + (Some(output), None) => { + if output? + .trim_end_matches(|c: char| c.is_whitespace() || c == '\n') + != "" + { + break false; + } + } + (None, Some(expected_output)) => { + if expected_output? + .trim_end_matches(|c: char| c.is_whitespace() || c == '\n') + != "" + { + break false; + } + } + (Some(output), Some(expected_output)) => { + if output? + .trim_end_matches(|c: char| c.is_whitespace() || c == '\n') + != expected_output? + .trim_end_matches(|c: char| c.is_whitespace() || c == '\n') + { + break false; + } + } } - } + }; if matched { Poll::Ready(Ok(JudgeResult { @@ -101,21 +133,33 @@ impl Future for Judge { })) } } else { + let mut stderr = String::new(); + let _ = self + .child + .stderr + .take() + .unwrap() + .read_to_string(&mut stderr); + let code = status.code().unwrap_or(-1); match status.signal() { Some(libc::SIGSEGV) | Some(libc::SIGBUS) | Some(libc::SIGILL) => { Poll::Ready(Ok(JudgeResult { - status: JudgeStatus::SegmentFault, + status: JudgeStatus::SegmentFault { code, stderr }, time_used: self.time_used, memory_used: self.memory_used, })) } - Some(code) => Poll::Ready(Ok(JudgeResult { - status: JudgeStatus::SystemError { code }, + Some(signal) => Poll::Ready(Ok(JudgeResult { + status: JudgeStatus::SystemError { + code, + signal, + stderr, + }, time_used: self.time_used, memory_used: self.memory_used, })), None => Poll::Ready(Ok(JudgeResult { - status: JudgeStatus::RuntimeError, + status: JudgeStatus::RuntimeError { code, stderr }, time_used: self.time_used, memory_used: self.memory_used, })), diff --git a/src/utils.rs b/src/utils.rs index c893b61..59664f9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,24 +10,3 @@ pub fn get_memory_usage(pid: u32) -> Option { } None } - -#[cfg(feature = "rerun")] -pub fn rerun_if_not_root() -> anyhow::Result<()> { - use std::{ - env, - process::{self, Command}, - }; - #[cfg(target_os = "linux")] - if !nix::unistd::getuid().is_root() { - let args: Vec = env::args().collect(); - let mut command = Command::new("sudo"); - command.args(&args); - let status = command.status().expect("failed to execute process"); - if status.success() { - process::exit(0); - } else { - process::exit(1); - } - }; - Ok(()) -} diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..1d7901e --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +/__pycache__ \ No newline at end of file diff --git a/tests/1.out b/tests/1.out index e440e5c..a5c8806 100644 --- a/tests/1.out +++ b/tests/1.out @@ -1 +1,2 @@ -3 \ No newline at end of file +3 +3 diff --git a/tests/2.out b/tests/2.out index c793025..68618ea 100644 --- a/tests/2.out +++ b/tests/2.out @@ -1 +1,2 @@ +7 7 \ No newline at end of file diff --git a/tests/any.in b/tests/any.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/any.out b/tests/any.out new file mode 100644 index 0000000..e69de29 diff --git a/tests/fail_to_write.mjs b/tests/fail_to_write.mjs new file mode 100644 index 0000000..bae2454 --- /dev/null +++ b/tests/fail_to_write.mjs @@ -0,0 +1,3 @@ +import fs from "node:fs"; + +fs.writeFileSync("test.txt", "Hello, world!"); diff --git a/tests/fail_to_write.py b/tests/fail_to_write.py new file mode 100644 index 0000000..667037c --- /dev/null +++ b/tests/fail_to_write.py @@ -0,0 +1,13 @@ +from pathlib import Path + +Path.cwd().joinpath("test.txt").touch() +assert Path.cwd().joinpath("test.txt").exists() +Path.cwd().joinpath("test.txt").write_text("This is a test") +Path.cwd().joinpath("test.txt").write_bytes(b"This is a test") +assert Path.cwd().joinpath("test.txt").read_bytes() == b"This is a test" +assert Path.cwd().joinpath("test.txt").read_text() == "This is a test" +print(Path.cwd().joinpath("test.txt")) + +a, b = map(int, input().split()) +print(a + b) +print(a + b) diff --git a/tests/test.cpp b/tests/test.cpp index d8e34e9..69ca294 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -8,6 +8,8 @@ int main() { i64 a, b; cin >> a >> b; - cout << a + b << endl << endl << endl; + cout << a + b << endl; + cout << a + b << endl + << endl; return 0; } diff --git a/tests/test.mjs b/tests/test.mjs new file mode 100644 index 0000000..1e1cf76 --- /dev/null +++ b/tests/test.mjs @@ -0,0 +1,12 @@ +let input = ''; + +process.stdin.on('data', (data) => { + input += data; +}); + +process.stdin.on('end', () => { + const numbers = input.split('\n')[0].split(" ").map((num) => parseInt(num)); + const result = numbers.reduce((acc, num) => acc + num, 0); + console.log(result); + console.log(result); +}); diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..1c9264f --- /dev/null +++ b/tests/test.py @@ -0,0 +1,3 @@ +a, b = map(int, input().split()) +print(a + b) +print(a + b) diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..606d1aa --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,13 @@ +use std::io::Read; + +fn main() -> Result<(), Box> { + let mut buf = String::new(); + std::io::stdin().read_to_string(&mut buf)?; + let numbers: Vec = buf + .split_whitespace() + .map(|n| n.parse::().unwrap()) + .collect(); + println!("{:?}", numbers.iter().sum::()); + println!("{:?}", numbers.iter().sum::()); + Ok(()) +} diff --git a/tests/test_fs.rs b/tests/test_fs.rs new file mode 100644 index 0000000..aa7f437 --- /dev/null +++ b/tests/test_fs.rs @@ -0,0 +1,74 @@ +use std::time::Duration; + +use anyhow::Result; +use eval_stack::{case::run_test_cases, compile::Language, config::JudgeOptions}; + +#[tokio::test] +async fn test_fs() -> Result<()> { + let current_dir = std::env::current_dir()?; + let workspace_path = current_dir.join("workspace"); + let tests_path = current_dir.join("tests"); + + let results = run_test_cases( + Language::Python, + &workspace_path, + &tests_path.join("fail_to_write.py"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: false, + }, + vec![ + (tests_path.join("1.in"), tests_path.join("1.out")), + (tests_path.join("2.in"), tests_path.join("2.out")), + ], + true, + ) + .await?; + + for result in results { + println!("{:?}", result); + assert!(!result.is_accepted()); + assert!(matches!( + result.status, + eval_stack::judge::JudgeStatus::SystemError { + code: -1, + stderr: _stderr, + signal: 31 + } + )) + } + + let results = run_test_cases( + Language::NodeJs, + &workspace_path, + &tests_path.join("fail_to_write.mjs"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: true, + }, + vec![ + (tests_path.join("1.in"), tests_path.join("1.out")), + (tests_path.join("2.in"), tests_path.join("2.out")), + ], + true, + ) + .await?; + + for result in results { + println!("{:?}", result); + assert!(!result.is_accepted()); + assert!(matches!( + result.status, + eval_stack::judge::JudgeStatus::RuntimeError { + code: 1, + stderr: _stderr + } + )) + } + + Ok(()) +} diff --git a/tests/test_judge.rs b/tests/test_judge.rs index 96c942a..654372b 100644 --- a/tests/test_judge.rs +++ b/tests/test_judge.rs @@ -1,15 +1,65 @@ use std::time::Duration; use anyhow::Result; -#[cfg(feature = "rerun")] -use eval_stack::utils::rerun_if_not_root; use eval_stack::{case::run_test_cases, compile::Language, config::JudgeOptions}; #[tokio::test] -async fn main() -> Result<()> { - #[cfg(feature = "rerun")] - rerun_if_not_root()?; +async fn test_rust_judge() -> Result<()> { + let current_dir = std::env::current_dir()?; + let workspace_path = current_dir.join("workspace"); + let tests_path = current_dir.join("tests"); + + let results = run_test_cases( + Language::Rust, + &workspace_path, + &tests_path.join("test.rs"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: false, + }, + vec![ + (tests_path.join("1.in"), tests_path.join("1.out")), + (tests_path.join("2.in"), tests_path.join("2.out")), + ], + true, + ) + .await?; + + for result in results { + println!("{:?}", result); + assert!(result.is_accepted()) + } + + let results = run_test_cases( + Language::Rust, + &workspace_path, + &tests_path.join("test.rs"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: false, + }, + vec![ + (tests_path.join("any.in"), tests_path.join("any.out")), + (tests_path.join("1.out"), tests_path.join("1.in")), + ], + true, + ) + .await?; + for result in results { + println!("{:?}", result); + assert!(!result.is_accepted()) + } + + Ok(()) +} + +#[tokio::test] +async fn test_cpp_judge() -> Result<()> { let current_dir = std::env::current_dir()?; let workspace_path = current_dir.join("workspace"); let tests_path = current_dir.join("tests"); @@ -21,7 +71,40 @@ async fn main() -> Result<()> { JudgeOptions { time_limit: Duration::from_secs(1), memory_limit: 128 * 1024 * 1024, - fast_fail: true, + fail_fast: true, + no_startup_limits: false, + }, + vec![ + (tests_path.join("1.in"), tests_path.join("1.out")), + (tests_path.join("2.in"), tests_path.join("2.out")), + ], + true, + ) + .await?; + + for result in results { + println!("{:?}", result); + assert!(result.is_accepted()) + } + + Ok(()) +} + +#[tokio::test] +async fn test_python_judge() -> Result<()> { + let current_dir = std::env::current_dir()?; + let workspace_path = current_dir.join("workspace"); + let tests_path = current_dir.join("tests"); + + let results = run_test_cases( + Language::Python, + &workspace_path, + &tests_path.join("test.py"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: false, }, vec![ (tests_path.join("1.in"), tests_path.join("1.out")), @@ -33,6 +116,39 @@ async fn main() -> Result<()> { for result in results { println!("{:?}", result); + assert!(result.is_accepted()) + } + + Ok(()) +} + +#[tokio::test] +async fn test_nodejs_judge() -> Result<()> { + let current_dir = std::env::current_dir()?; + let workspace_path = current_dir.join("workspace"); + let tests_path = current_dir.join("tests"); + + let results = run_test_cases( + Language::NodeJs, + &workspace_path, + &tests_path.join("test.mjs"), + JudgeOptions { + time_limit: Duration::from_secs(1), + memory_limit: 128 * 1024 * 1024, + fail_fast: true, + no_startup_limits: true, + }, + vec![ + (tests_path.join("1.in"), tests_path.join("1.out")), + (tests_path.join("2.in"), tests_path.join("2.out")), + ], + false, + ) + .await?; + + for result in results { + println!("{:?}", result); + assert!(result.is_accepted()) } Ok(())