diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ca6c72f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = 'sudo -E' diff --git a/Cargo.lock b/Cargo.lock index a03ea60..79c044f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,10 +14,13 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.100" +name = "aho-corasick" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] [[package]] name = "arrayvec" @@ -78,7 +81,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -136,7 +139,7 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -211,6 +214,31 @@ dependencies = [ "piper", ] +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + [[package]] name = "borsh" version = "1.6.0" @@ -321,6 +349,41 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -367,7 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -397,6 +460,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "funty" version = "2.0.0" @@ -428,6 +497,43 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -451,6 +557,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.12.3" @@ -478,6 +590,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.12.1" @@ -508,9 +626,13 @@ dependencies = [ name = "judge-runner" version = "0.1.0" dependencies = [ - "anyhow", + "bon", "byte-unit", "cgroups-rs", + "rand 0.9.2", + "rstest", + "state-shift", + "tokio", "uuid", ] @@ -526,6 +648,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -547,6 +678,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "nix" version = "0.25.1" @@ -603,12 +745,41 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.4" @@ -631,7 +802,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -643,6 +814,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -761,6 +942,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -781,6 +971,41 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -819,6 +1044,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.111", + "unicode-ident", +] + [[package]] name = "rust_decimal" version = "1.39.0" @@ -835,6 +1089,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" @@ -845,7 +1108,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -872,12 +1135,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -953,12 +1228,52 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "state-shift" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b84d514d82ae2456c0ecc1fdf84e77368ff158e7a9dcdfc135669ba3ec57fdf" +dependencies = [ + "proc-macro2", + "quote", + "stringcase", + "syn 2.0.111", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringcase" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -997,7 +1312,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1035,6 +1350,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -1226,6 +1569,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1235,6 +1587,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" @@ -1286,7 +1703,7 @@ dependencies = [ "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", diff --git a/Cargo.toml b/Cargo.toml index 7870cf0..d1f27c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,13 @@ A code runner library for online judge system """ [dependencies] -anyhow = "1.0.100" +bon = "3.8.1" byte-unit = "5.2.0" cgroups-rs = "0.5.0" +state-shift = "2.1.1" +tokio = { version = "1.48.0", features = ["full"] } uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } + +[dev-dependencies] +rand = "0.9.2" +rstest = "0.26.1" diff --git a/README.md b/README.md index e69de29..a5acc1c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,88 @@ +# Judge Runner + +A code runner library for online judge system. + +## Supported Languages + +- Rust +- C++ +- Python +- Java +- ... More language can be defined + +## Usage + +```rust +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Code, Judge, Resource, language}; + +#[tokio::main] +async fn main() { + let checker_code = r#" +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} + "#; + let code = r#" +#include + +using namespace std; + +int main() { + string s; + cin >> s; + cout << s << endl; +} +"#; + + let checker = Judge::builder() + .main(Code { + content: checker_code.as_bytes(), + language: language::CPP, + }) + .build() + .await + .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + let checker = checker.read_executable().await.unwrap(); + + let judge = Judge::builder() + .checker(Code { + content: &checker, + language: language::CPP, + }) + .main(Code { + content: code.as_bytes(), + language: language::CPP, + }) + .time_limit(Duration::from_secs(1)) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) + .build() + .await + .unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); + + let input = "4"; + let metrics = judge.run(input.as_bytes()).await.unwrap(); + println!("{:#?}", metrics); +} +``` + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..558551d --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,68 @@ +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Code, Judge, Resource, language}; + +#[tokio::main] +async fn main() { + let checker_code = r#" +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} + "#; + let code = r#" +#include + +using namespace std; + +int main() { + string s; + cin >> s; + cout << s << endl; +} +"#; + + let checker = Judge::builder() + .main(Code { + content: checker_code.as_bytes(), + language: language::CPP, + }) + .build() + .await + .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + let checker = checker.read_executable().await.unwrap(); + + let judge = Judge::builder() + .checker(Code { + content: &checker, + language: language::CPP, + }) + .main(Code { + content: code.as_bytes(), + language: language::CPP, + }) + .time_limit(Duration::from_secs(1)) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) + .build() + .await + .unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); + + let input = "4"; + let metrics = judge.run(input.as_bytes()).await.unwrap(); + println!("{:#?}", metrics); +} diff --git a/src/judge.rs b/src/judge.rs new file mode 100644 index 0000000..64ce80d --- /dev/null +++ b/src/judge.rs @@ -0,0 +1,201 @@ +use std::{env, io, marker::PhantomData, path::PathBuf, process::Stdio, time::Duration}; + +use bon::bon; +use state_shift::{impl_state, type_state}; +use tokio::{ + fs, + io::{AsyncReadExt, AsyncWriteExt}, +}; + +use crate::{Language, Metrics, Resource, Sandbox, Verdict, util}; + +const MAIN: &str = "main"; +const CHECKER: &str = "checker"; +const BUFFER_SIZE: usize = 8 * 1024; + +pub struct Code<'a> { + pub language: Language, + pub content: &'a [u8], +} + +#[type_state( + states = (Created, Compiled), + slots = (Created) +)] +#[derive(Default)] +pub struct Judge { + pub project_path: PathBuf, + pub language: Language, + pub checker_language: Option, + pub is_interactive: bool, + pub resource: Resource, + pub time_limit: Duration, +} + +#[bon] +impl Judge { + #[builder] + pub async fn new<'a>( + main: Code<'a>, + checker: Option>, + #[builder(default = false, name = "interactive")] is_interactive: bool, + #[builder(default)] resource: Resource, + #[builder(default)] time_limit: Duration, + ) -> io::Result> { + let project_path = env::temp_dir().join(util::random(main.content).to_string()); + fs::create_dir(&project_path).await?; + + let main_path = project_path + .join(MAIN) + .with_extension(main.language.extension); + fs::write(&main_path, main.content).await?; + if let Some(checker) = &checker { + let mut checker_path = project_path.join(CHECKER); + if checker.language.is_interpreted() { + checker_path.set_extension(checker.language.extension); + } + let mut checker_file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o755) + .open(&checker_path) + .await?; + checker_file.write_all(checker.content).await?; + checker_file.sync_all().await?; + } + + Ok(Judge { + project_path, + language: main.language, + checker_language: checker.map(|checker| checker.language), + is_interactive, + resource, + time_limit, + _state: PhantomData, + }) + } +} + +#[impl_state] +impl Judge { + #[require(Created)] + #[switch_to(Compiled)] + pub async fn compile(self) -> io::Result, Verdict>> { + if let Some(mut cmd) = self.language.get_compile_command(MAIN) { + let mut process = cmd.current_dir(&self.project_path).spawn()?; + let status = process.wait().await?; + if !status.success() { + return Ok(Err(Verdict::CompilationError)); + } + } + + Ok(Ok(Judge { + project_path: self.project_path, + language: self.language, + checker_language: self.checker_language, + is_interactive: self.is_interactive, + resource: self.resource, + time_limit: self.time_limit, + })) + } + + #[require(Compiled)] + pub async fn read_executable(&self) -> io::Result> { + let mut path = self.project_path.join(MAIN); + if self.language.is_interpreted() { + path.set_extension(self.language.extension); + } + + fs::read(path).await + } + + #[require(Compiled)] + pub async fn run(&self, input: &[u8]) -> io::Result { + let checker_language = self + .checker_language + .ok_or(io::Error::other("Missing checker"))?; + let mut checker = checker_language + .get_run_command(CHECKER) + .current_dir(&self.project_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + let mut cstdin = checker.stdin.take().unwrap(); + let mut cstdout = checker.stdout.take().unwrap(); + cstdin.write_all(input).await?; + cstdin.write_all(b"\n").await?; + cstdin.flush().await?; + + let sandbox = Sandbox::new(self.resource, self.time_limit)?; + let mut cmd = self.language.get_run_command(MAIN); + cmd.current_dir(&self.project_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut main = sandbox.spawn(cmd)?; + let mut stdin = main.stdin.take().unwrap(); + let mut stdout = main.stdout.take().unwrap(); + let mut stderr = main.stderr.take().unwrap(); + + let monitor = tokio::spawn(async move { sandbox.monitor(main).await }); + if !self.is_interactive { + stdin.write_all(input).await?; + stdin.write_all(b"\n").await?; + stdin.flush().await?; + } + let stdin_thread = + tokio::spawn(async move { tokio::io::copy(&mut cstdout, &mut stdin).await }); + let stdout_thread = tokio::spawn(async move { + let mut out = vec![]; + let mut buffer = [0u8; BUFFER_SIZE]; + loop { + let n = stdout.read(&mut buffer).await?; + if n == 0 { + break; + } + if cstdin.write_all(&buffer[..n]).await.is_err() { + break; + } + cstdin.flush().await?; + out.extend_from_slice(&buffer[0..n]); + } + + Ok::<_, io::Error>(out) + }); + + let (verdict, run_time, memory_usage) = monitor.await.unwrap()?; + let checker_status = checker.wait().await?; + drop(checker); + + let _ = stdin_thread.await; + let stdout = stdout_thread.await.unwrap()?; + let mut err = vec![]; + stderr.read_to_end(&mut err).await?; + + if let Some(verdict) = verdict { + return Ok(Metrics { + verdict, + run_time, + stdout, + stderr: err, + memory_usage, + }); + } + + let verdict = if checker_status.success() { + Verdict::Accepted + } else { + Verdict::WrongAnswer + }; + + Ok(Metrics { + verdict, + run_time, + stdout, + stderr: err, + memory_usage, + }) + } +} diff --git a/src/language/cpp.rs b/src/language/cpp.rs index 8f69b4e..c08d72f 100644 --- a/src/language/cpp.rs +++ b/src/language/cpp.rs @@ -1,6 +1,7 @@ -use super::Language; +use crate::Language; pub const CPP: Language = Language { - compile_args: Some(&["g++", "-o", "main", "main.cpp"]), - run_args: &["./main"], + compile_args: Some("g++ -o {main} {main}.cpp"), + run_args: "./{main}", + extension: "cpp", }; diff --git a/src/language/java.rs b/src/language/java.rs index 215024b..6571b43 100644 --- a/src/language/java.rs +++ b/src/language/java.rs @@ -1,6 +1,7 @@ -use super::Language; +use crate::Language; pub const JAVA: Language = Language { - compile_args: Some(&["javac", "Main.java"]), - run_args: &["java", "Main"], + compile_args: Some("javac {main}.java"), + run_args: "java {main}", + extension: "java", }; diff --git a/src/language/javascript.rs b/src/language/javascript.rs deleted file mode 100644 index 995c3e4..0000000 --- a/src/language/javascript.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::Language; - -pub const JAVASCRIPT: Language = Language { - compile_args: None, - run_args: &["bun", "run", "main.js"], -}; diff --git a/src/language/mod.rs b/src/language/mod.rs index 87f8adf..6b9fc04 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -1,18 +1,45 @@ mod cpp; mod java; -mod javascript; mod python; mod rust; -mod typescript; + +use tokio::process::Command; pub use cpp::CPP; pub use java::JAVA; -pub use javascript::JAVASCRIPT; pub use python::PYTHON; pub use rust::RUST; -pub use typescript::TYPESCRIPT; -pub struct Language<'a> { - pub compile_args: Option<&'a [&'a str]>, - pub run_args: &'a [&'a str], +#[derive(Debug, Clone, Copy, Default)] +pub struct Language { + pub compile_args: Option<&'static str>, + pub run_args: &'static str, + pub extension: &'static str, +} + +impl Language { + pub fn get_compile_command(&self, main: &str) -> Option { + let args = self.compile_args?; + let args = args.replace("{main}", main); + let mut args = args.split_whitespace(); + let binary = args.next().unwrap(); + + let mut command = Command::new(binary); + command.args(args); + + Some(command) + } + pub fn get_run_command(&self, main: &str) -> Command { + let args = self.run_args.replace("{main}", main); + let mut args = args.split_whitespace(); + let binary = args.next().unwrap(); + + let mut command = Command::new(binary); + command.args(args); + + command + } + pub fn is_interpreted(&self) -> bool { + self.compile_args.is_none() + } } diff --git a/src/language/python.rs b/src/language/python.rs index 9d9e181..a253437 100644 --- a/src/language/python.rs +++ b/src/language/python.rs @@ -1,6 +1,7 @@ -use super::Language; +use crate::Language; pub const PYTHON: Language = Language { compile_args: None, - run_args: &["python", "main.py"], + run_args: "python {main}.py", + extension: "py", }; diff --git a/src/language/rust.rs b/src/language/rust.rs index 32a20a6..ae8a2f8 100644 --- a/src/language/rust.rs +++ b/src/language/rust.rs @@ -1,6 +1,7 @@ -use super::Language; +use crate::Language; pub const RUST: Language = Language { - compile_args: Some(&["rustc", "-O", "main.rs"]), - run_args: &["./main"], + compile_args: Some("rustc -O {main}.rs"), + run_args: "./{main}", + extension: "rs", }; diff --git a/src/language/typescript.rs b/src/language/typescript.rs deleted file mode 100644 index b2c1fbd..0000000 --- a/src/language/typescript.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::Language; - -pub const TYPESCRIPT: Language = Language { - compile_args: None, - run_args: &["bun", "run", "main.ts"], -}; diff --git a/src/lib.rs b/src/lib.rs index 7ee60ac..206dde8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,10 @@ +mod judge; pub mod language; +mod metrics; mod sandbox; -mod verdict; +mod util; +pub use judge::*; +pub use language::Language; +pub use metrics::*; pub use sandbox::*; -pub use verdict::*; - -#[cfg(test)] -mod test { - #[test] - fn base() {} -} diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..c7fd0f3 --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,23 @@ +use std::time::Duration; + +use byte_unit::Byte; + +#[derive(Debug, PartialEq, Eq)] +pub enum Verdict { + Accepted, + WrongAnswer, + TimeLimitExceeded, + CompilationError, + MemoryLimitExceeded, + RuntimeError, + IdleTimeLimitExceeded, +} + +#[derive(Debug)] +pub struct Metrics { + pub verdict: Verdict, + pub run_time: Duration, + pub memory_usage: Byte, + pub stdout: Vec, + pub stderr: Vec, +} diff --git a/src/sandbox/cgroup.rs b/src/sandbox/cgroup.rs index 3fa334d..afc2200 100644 --- a/src/sandbox/cgroup.rs +++ b/src/sandbox/cgroup.rs @@ -1,34 +1,42 @@ use std::time::Duration; -use cgroups_rs::fs::{Cgroup, cpu::CpuController, memory::MemController}; +use byte_unit::Byte; +use cgroups_rs::fs::{cpu::CpuController, memory::MemController}; const CPU_USAGE_PREFIX: &str = "usage_usec "; -pub trait CgroupExt { - fn get_cpu_time(&self) -> Duration; - fn is_out_of_memory(&self) -> bool; +pub trait CpuControllerExt { + fn usage(&self) -> Duration; } -impl CgroupExt for Cgroup { - fn get_cpu_time(&self) -> Duration { - let cpu_controller: &CpuController = self.controller_of().unwrap(); - let stats = cpu_controller.cpu().stat; +impl CpuControllerExt for CpuController { + fn usage(&self) -> Duration { + let stats = self.cpu().stat; - // SAFETY: there must be cpu usage for valid cgroup let usage = stats .lines() .find_map(|line| line.strip_prefix(CPU_USAGE_PREFIX)) .unwrap(); - // SAFETY: cpu usage must be duration in microsecond let usage = usage.parse().unwrap(); Duration::from_micros(usage) } +} + +pub trait MemControllerExt { + fn usage(&self) -> Byte; + fn limit(&self) -> Byte; +} + +impl MemControllerExt for MemController { + fn usage(&self) -> Byte { + let stats = self.memory_stat(); + + Byte::from_u64(stats.usage_in_bytes) + } - fn is_out_of_memory(&self) -> bool { - // SAFETY: there must be memory controller for cgroup v2 - let memory_controller: &MemController = self.controller_of().unwrap(); - let stats = memory_controller.memory_stat(); + fn limit(&self) -> Byte { + let stats = self.memory_stat(); - stats.oom_control.oom_kill > 0 && stats.usage_in_bytes as i64 > stats.limit_in_bytes + Byte::from_u64(stats.limit_in_bytes.max(0) as u64) } } diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index a88b305..0faae07 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -1,53 +1,97 @@ mod cgroup; mod resource; -use std::{ - process::Child, - thread::sleep, - time::{Duration, Instant}, +use std::{io, os::unix::process::ExitStatusExt, process, time::Duration}; + +use tokio::{ + process::{Child, Command}, + time::{Instant, interval}, }; -use anyhow::Result; -use cgroups_rs::{CgroupPid, fs::Cgroup}; +use byte_unit::Byte; +use cgroups_rs::{ + CgroupPid, + fs::{Cgroup, cpu::CpuController, memory::MemController}, +}; pub use resource::Resource; -use crate::{Verdict, sandbox::cgroup::CgroupExt}; +use crate::{ + Verdict, + sandbox::cgroup::{CpuControllerExt, MemControllerExt}, +}; // TODO: need further tuning const POLL: Duration = Duration::from_millis(10); -const MIN_CPU_TIME_PER_POLL: Duration = Duration::from_millis(1); +const MIN_CPU_USAGE_PER_POLL: Duration = Duration::from_millis(1); const IDLE_TIME_LIMIT: Duration = Duration::from_millis(100); pub struct Sandbox { pub cgroup: Cgroup, - pub cpu_time_limit: Duration, + pub cpu_usage_limit: Duration, pub wall_time_limit: Duration, } impl Sandbox { - pub fn new(resource: Resource, time_limit: Duration) -> Result { + pub fn new(resource: Resource, time_limit: Duration) -> io::Result { Ok(Sandbox { cgroup: resource.try_into()?, - cpu_time_limit: time_limit, + cpu_usage_limit: time_limit, wall_time_limit: Duration::max(time_limit * 2, time_limit + Duration::from_secs(2)), }) } - pub fn monitor(self, mut child: Child) -> Result { + pub fn spawn(&self, mut command: Command) -> io::Result { + let cgroup = self.cgroup.clone(); + + unsafe { + command + .pre_exec(move || { + let id = process::id(); + + cgroup + .add_task_by_tgid(CgroupPid::from(id as u64)) + .map_err(io::Error::other) + }) + .spawn() + } + } + + pub async fn monitor(&self, mut child: Child) -> io::Result<(Option, Duration, Byte)> { + let Some(id) = child.id() else { + return Err(io::Error::other("Child exited")); + }; self.cgroup - .add_task_by_tgid(CgroupPid::from(child.id() as u64))?; + .add_task_by_tgid(CgroupPid::from(id as u64)) + .map_err(io::Error::other)?; + let cpu: &CpuController = self + .cgroup + .controller_of() + .ok_or(io::Error::other("Missing cpu controller"))?; + let memory: &MemController = self + .cgroup + .controller_of() + .ok_or(io::Error::other("Missing memory controller"))?; let start = Instant::now(); - let mut prev_cpu_time = self.cgroup.get_cpu_time(); + let mut memory_usage = Byte::default(); + let mut prev_cpu_usage = cpu.usage(); let mut idle_start: Option = None; + + let mut interval = interval(POLL); + while child.try_wait()?.is_none() { - let cpu_time = self.cgroup.get_cpu_time(); + let cpu_usage = cpu.usage(); + memory_usage = memory_usage.max(memory.usage()); - if cpu_time.abs_diff(prev_cpu_time) <= MIN_CPU_TIME_PER_POLL { + if cpu_usage.abs_diff(prev_cpu_usage) <= MIN_CPU_USAGE_PER_POLL { match idle_start { Some(idle_start) => { if idle_start.elapsed() >= IDLE_TIME_LIMIT { - return Ok(Verdict::IdleTimeLimitExceeded); + return Ok(( + Some(Verdict::IdleTimeLimitExceeded), + cpu_usage, + memory_usage, + )); } } None => idle_start = Some(Instant::now()), @@ -56,34 +100,38 @@ impl Sandbox { idle_start = None; } - if cpu_time >= self.cpu_time_limit || start.elapsed() >= self.wall_time_limit { - return Ok(Verdict::TimeLimitExceeded); + if cpu_usage >= self.cpu_usage_limit || start.elapsed() >= self.wall_time_limit { + return Ok(( + Some(Verdict::TimeLimitExceeded), + self.cpu_usage_limit, + memory_usage, + )); } - prev_cpu_time = cpu_time; + prev_cpu_usage = cpu_usage; - sleep(POLL); + interval.tick().await; } - // SAFETY: child must be finished at this point to exit the previous loop let status = child.try_wait()?.unwrap(); if status.success() { - // temporarily return AC - return Ok(Verdict::Accepted); + return Ok((None, prev_cpu_usage, memory_usage)); } - if self.cgroup.is_out_of_memory() { - return Ok(Verdict::MemoryLimitExceeded); + match status.signal() { + // SIGKILL + Some(9) => Ok(( + Some(Verdict::MemoryLimitExceeded), + prev_cpu_usage, + memory.limit(), + )), + _ => Ok((Some(Verdict::RuntimeError), prev_cpu_usage, memory_usage)), } - Ok(Verdict::RuntimeError) } } impl Drop for Sandbox { fn drop(&mut self) { - // SAFETY: always be used with stable version of linux kernel - self.cgroup.kill().unwrap(); - - // SAFETY: no descendant is created previously by judge - self.cgroup.delete().unwrap(); + let _ = self.cgroup.kill(); + let _ = self.cgroup.delete(); } } diff --git a/src/sandbox/resource.rs b/src/sandbox/resource.rs index 2543e86..80dbc05 100644 --- a/src/sandbox/resource.rs +++ b/src/sandbox/resource.rs @@ -1,12 +1,13 @@ -use std::time::Duration; +use std::{io, time::Duration}; use byte_unit::Byte; use cgroups_rs::fs::{Cgroup, cgroup_builder::CgroupBuilder, hierarchies}; -use uuid::Uuid; + +use crate::util; const PREFIX: &str = "judge"; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Hash)] pub struct Resource { pub memory: Byte, pub cpu_quota: Duration, @@ -24,10 +25,10 @@ impl Default for Resource { } impl TryFrom for Cgroup { - type Error = anyhow::Error; + type Error = io::Error; fn try_from(resource: Resource) -> Result { - let builder = CgroupBuilder::new(&format!("{}/{}", PREFIX, Uuid::new_v4())); + let builder = CgroupBuilder::new(&format!("{}/{}", PREFIX, util::random(resource))); let memory = resource.memory.as_u64() as i64; let builder = builder @@ -41,7 +42,9 @@ impl TryFrom for Cgroup { let period = resource.cpu_period.as_micros() as u64; let builder = builder.cpu().quota(quota).period(period).done(); - let cgroup = builder.build(hierarchies::auto())?; + let cgroup = builder + .build(hierarchies::auto()) + .map_err(io::Error::other)?; Ok(cgroup) } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..fae314f --- /dev/null +++ b/src/util.rs @@ -0,0 +1,11 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use uuid::Uuid; + +pub fn random(data: T) -> u64 { + let mut hasher = DefaultHasher::default(); + data.hash(&mut hasher); + Uuid::new_v4().hash(&mut hasher); + + hasher.finish() +} diff --git a/src/verdict.rs b/src/verdict.rs deleted file mode 100644 index ed1fa29..0000000 --- a/src/verdict.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub enum Verdict { - Accepted, - WrongAnswer, - TimeLimitExceeded, - CompilationError, - MemoryLimitExceeded, - RuntimeError, - IdleTimeLimitExceeded, -} diff --git a/tests/problem/easy/hello-world/checker/main.cpp b/tests/problem/easy/hello-world/checker/main.cpp new file mode 100644 index 0000000..467fc7f --- /dev/null +++ b/tests/problem/easy/hello-world/checker/main.cpp @@ -0,0 +1,13 @@ +#include + +using namespace std; + +int main() { + string s, res; + cin >> s >> res; + if (s == res) { + return 0; + } else { + return 1; + } +} diff --git a/tests/problem/easy/hello-world/input/1.txt b/tests/problem/easy/hello-world/input/1.txt new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/tests/problem/easy/hello-world/input/1.txt @@ -0,0 +1 @@ +Hello diff --git a/tests/problem/easy/hello-world/input/2.txt b/tests/problem/easy/hello-world/input/2.txt new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/tests/problem/easy/hello-world/input/2.txt @@ -0,0 +1 @@ +4 diff --git a/tests/problem/easy/hello-world/solution/main.cpp b/tests/problem/easy/hello-world/solution/main.cpp new file mode 100644 index 0000000..b05c2e7 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + string s; + cin >> s; + cout << s << endl; +} diff --git a/tests/problem/easy/hello-world/solution/main.java b/tests/problem/easy/hello-world/solution/main.java new file mode 100644 index 0000000..fc414ab --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.java @@ -0,0 +1,8 @@ +import java.util.Scanner; +class main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + System.out.println(s); + } +} diff --git a/tests/problem/easy/hello-world/solution/main.py b/tests/problem/easy/hello-world/solution/main.py new file mode 100644 index 0000000..5ee4533 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.py @@ -0,0 +1,2 @@ +x = input() +print(x) diff --git a/tests/problem/easy/hello-world/solution/main.rs b/tests/problem/easy/hello-world/solution/main.rs new file mode 100644 index 0000000..53f0a20 --- /dev/null +++ b/tests/problem/easy/hello-world/solution/main.rs @@ -0,0 +1,6 @@ +use std::io::stdin; + +fn main() { + let s = stdin().lines().next().unwrap().unwrap(); + println!("{s}"); +} diff --git a/tests/should_return_accepted.rs b/tests/should_return_accepted.rs new file mode 100644 index 0000000..e276bce --- /dev/null +++ b/tests/should_return_accepted.rs @@ -0,0 +1,50 @@ +mod util; + +use std::path::Path; +use std::time::Duration; + +use byte_unit::Byte; +use judge_runner::{Code, Judge, Language, Resource, Verdict, language::*}; +use rstest::rstest; + +#[rstest] +#[tokio::test(flavor = "multi_thread")] +pub async fn should_return_accepted( + #[rustfmt::skip] + #[values(RUST, CPP, PYTHON, JAVA)] + language: Language, + + #[dirs] + #[files("tests/problem/easy/*")] + #[exclude("wrong-answer")] + #[by_ref] + problem: &Path, +) { + let inputs = util::read_inputs(problem); + let checker = util::read_checker(problem, CPP).await; + let solution = util::read_solution(problem, language); + + let judge = Judge::builder() + .checker(Code { + content: &checker, + language: CPP, + }) + .main(Code { + content: &solution, + language, + }) + .resource(Resource { + memory: Byte::GIGABYTE, + ..Default::default() + }) + .time_limit(Duration::from_secs(1)) + .build() + .await + .unwrap(); + let judge = judge.compile().await.unwrap().unwrap(); + + for input in inputs { + let metrics = judge.run(input.as_bytes()).await.unwrap(); + assert_eq!(metrics.verdict, Verdict::Accepted); + } +} diff --git a/tests/util.rs b/tests/util.rs new file mode 100644 index 0000000..3f39e37 --- /dev/null +++ b/tests/util.rs @@ -0,0 +1,49 @@ +use std::{fs, path::Path}; + +use judge_runner::{Code, Judge, Language}; + +const INPUT: &str = "input"; +const SOLUTION: &str = "solution"; +const CHECKER: &str = "checker"; +const MAIN: &str = "main"; + +pub fn read_inputs(problem: &Path) -> Vec { + problem + .join(INPUT) + .read_dir() + .unwrap() + .flatten() + .map(|x| fs::read_to_string(x.path()).unwrap()) + .collect() +} + +pub async fn read_checker(problem: &Path, language: Language) -> Vec { + let checker = fs::read( + problem + .join(CHECKER) + .join(MAIN) + .with_extension(language.extension), + ) + .unwrap(); + + let checker = Judge::builder() + .main(Code { + content: &checker, + language, + }) + .build() + .await + .unwrap(); + let checker = checker.compile().await.unwrap().unwrap(); + checker.read_executable().await.unwrap() +} + +pub fn read_solution(problem: &Path, language: Language) -> Vec { + fs::read( + problem + .join(SOLUTION) + .join(MAIN) + .with_extension(language.extension), + ) + .unwrap() +}