Local runner for GitHub autograder
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial Commit

Ben C 252fe53e

+722
+1
.gitignore
··· 1 + /target
+3
.vscode/settings.json
··· 1 + { 2 + "rust-analyzer.showUnlinkedFileNotification": false 3 + }
+409
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 3 4 + 5 + [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 15 + name = "anstream" 16 + version = "0.6.11" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 19 + dependencies = [ 20 + "anstyle", 21 + "anstyle-parse", 22 + "anstyle-query", 23 + "anstyle-wincon", 24 + "colorchoice", 25 + "utf8parse", 26 + ] 27 + 28 + [[package]] 29 + name = "anstyle" 30 + version = "1.0.5" 31 + source = "registry+https://github.com/rust-lang/crates.io-index" 32 + checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" 33 + 34 + [[package]] 35 + name = "anstyle-parse" 36 + version = "0.2.3" 37 + source = "registry+https://github.com/rust-lang/crates.io-index" 38 + checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 + dependencies = [ 40 + "utf8parse", 41 + ] 42 + 43 + [[package]] 44 + name = "anstyle-query" 45 + version = "1.0.2" 46 + source = "registry+https://github.com/rust-lang/crates.io-index" 47 + checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 + dependencies = [ 49 + "windows-sys", 50 + ] 51 + 52 + [[package]] 53 + name = "anstyle-wincon" 54 + version = "3.0.2" 55 + source = "registry+https://github.com/rust-lang/crates.io-index" 56 + checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 + dependencies = [ 58 + "anstyle", 59 + "windows-sys", 60 + ] 61 + 62 + [[package]] 63 + name = "anyhow" 64 + version = "1.0.79" 65 + source = "registry+https://github.com/rust-lang/crates.io-index" 66 + checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 67 + 68 + [[package]] 69 + name = "cfg-if" 70 + version = "1.0.0" 71 + source = "registry+https://github.com/rust-lang/crates.io-index" 72 + checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 + 74 + [[package]] 75 + name = "clap" 76 + version = "4.4.18" 77 + source = "registry+https://github.com/rust-lang/crates.io-index" 78 + checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" 79 + dependencies = [ 80 + "clap_builder", 81 + "clap_derive", 82 + ] 83 + 84 + [[package]] 85 + name = "clap_builder" 86 + version = "4.4.18" 87 + source = "registry+https://github.com/rust-lang/crates.io-index" 88 + checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" 89 + dependencies = [ 90 + "anstream", 91 + "anstyle", 92 + "clap_lex", 93 + "strsim", 94 + ] 95 + 96 + [[package]] 97 + name = "clap_derive" 98 + version = "4.4.7" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 101 + dependencies = [ 102 + "heck", 103 + "proc-macro2", 104 + "quote", 105 + "syn", 106 + ] 107 + 108 + [[package]] 109 + name = "clap_lex" 110 + version = "0.6.0" 111 + source = "registry+https://github.com/rust-lang/crates.io-index" 112 + checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 113 + 114 + [[package]] 115 + name = "colorchoice" 116 + version = "1.0.0" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 119 + 120 + [[package]] 121 + name = "console" 122 + version = "0.15.8" 123 + source = "registry+https://github.com/rust-lang/crates.io-index" 124 + checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 125 + dependencies = [ 126 + "encode_unicode", 127 + "lazy_static", 128 + "libc", 129 + "unicode-width", 130 + "windows-sys", 131 + ] 132 + 133 + [[package]] 134 + name = "encode_unicode" 135 + version = "0.3.6" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 138 + 139 + [[package]] 140 + name = "gh-grader-preview" 141 + version = "0.1.0" 142 + dependencies = [ 143 + "anyhow", 144 + "clap", 145 + "clap_derive", 146 + "indicatif", 147 + "regex", 148 + "serde", 149 + "serde_json", 150 + "wait-timeout", 151 + ] 152 + 153 + [[package]] 154 + name = "heck" 155 + version = "0.4.1" 156 + source = "registry+https://github.com/rust-lang/crates.io-index" 157 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 158 + 159 + [[package]] 160 + name = "indicatif" 161 + version = "0.17.7" 162 + source = "registry+https://github.com/rust-lang/crates.io-index" 163 + checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" 164 + dependencies = [ 165 + "console", 166 + "instant", 167 + "number_prefix", 168 + "portable-atomic", 169 + "unicode-width", 170 + ] 171 + 172 + [[package]] 173 + name = "instant" 174 + version = "0.1.12" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 177 + dependencies = [ 178 + "cfg-if", 179 + ] 180 + 181 + [[package]] 182 + name = "itoa" 183 + version = "1.0.10" 184 + source = "registry+https://github.com/rust-lang/crates.io-index" 185 + checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 186 + 187 + [[package]] 188 + name = "lazy_static" 189 + version = "1.4.0" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 192 + 193 + [[package]] 194 + name = "libc" 195 + version = "0.2.152" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 198 + 199 + [[package]] 200 + name = "memchr" 201 + version = "2.7.1" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 204 + 205 + [[package]] 206 + name = "number_prefix" 207 + version = "0.4.0" 208 + source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 210 + 211 + [[package]] 212 + name = "portable-atomic" 213 + version = "1.6.0" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 216 + 217 + [[package]] 218 + name = "proc-macro2" 219 + version = "1.0.78" 220 + source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 222 + dependencies = [ 223 + "unicode-ident", 224 + ] 225 + 226 + [[package]] 227 + name = "quote" 228 + version = "1.0.35" 229 + source = "registry+https://github.com/rust-lang/crates.io-index" 230 + checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 231 + dependencies = [ 232 + "proc-macro2", 233 + ] 234 + 235 + [[package]] 236 + name = "regex" 237 + version = "1.10.3" 238 + source = "registry+https://github.com/rust-lang/crates.io-index" 239 + checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 240 + dependencies = [ 241 + "aho-corasick", 242 + "memchr", 243 + "regex-automata", 244 + "regex-syntax", 245 + ] 246 + 247 + [[package]] 248 + name = "regex-automata" 249 + version = "0.4.5" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 252 + dependencies = [ 253 + "aho-corasick", 254 + "memchr", 255 + "regex-syntax", 256 + ] 257 + 258 + [[package]] 259 + name = "regex-syntax" 260 + version = "0.8.2" 261 + source = "registry+https://github.com/rust-lang/crates.io-index" 262 + checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 263 + 264 + [[package]] 265 + name = "ryu" 266 + version = "1.0.16" 267 + source = "registry+https://github.com/rust-lang/crates.io-index" 268 + checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 269 + 270 + [[package]] 271 + name = "serde" 272 + version = "1.0.196" 273 + source = "registry+https://github.com/rust-lang/crates.io-index" 274 + checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 275 + dependencies = [ 276 + "serde_derive", 277 + ] 278 + 279 + [[package]] 280 + name = "serde_derive" 281 + version = "1.0.196" 282 + source = "registry+https://github.com/rust-lang/crates.io-index" 283 + checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 284 + dependencies = [ 285 + "proc-macro2", 286 + "quote", 287 + "syn", 288 + ] 289 + 290 + [[package]] 291 + name = "serde_json" 292 + version = "1.0.113" 293 + source = "registry+https://github.com/rust-lang/crates.io-index" 294 + checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" 295 + dependencies = [ 296 + "itoa", 297 + "ryu", 298 + "serde", 299 + ] 300 + 301 + [[package]] 302 + name = "strsim" 303 + version = "0.10.0" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 306 + 307 + [[package]] 308 + name = "syn" 309 + version = "2.0.48" 310 + source = "registry+https://github.com/rust-lang/crates.io-index" 311 + checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 312 + dependencies = [ 313 + "proc-macro2", 314 + "quote", 315 + "unicode-ident", 316 + ] 317 + 318 + [[package]] 319 + name = "unicode-ident" 320 + version = "1.0.12" 321 + source = "registry+https://github.com/rust-lang/crates.io-index" 322 + checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 323 + 324 + [[package]] 325 + name = "unicode-width" 326 + version = "0.1.11" 327 + source = "registry+https://github.com/rust-lang/crates.io-index" 328 + checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 329 + 330 + [[package]] 331 + name = "utf8parse" 332 + version = "0.2.1" 333 + source = "registry+https://github.com/rust-lang/crates.io-index" 334 + checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 335 + 336 + [[package]] 337 + name = "wait-timeout" 338 + version = "0.2.0" 339 + source = "registry+https://github.com/rust-lang/crates.io-index" 340 + checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 341 + dependencies = [ 342 + "libc", 343 + ] 344 + 345 + [[package]] 346 + name = "windows-sys" 347 + version = "0.52.0" 348 + source = "registry+https://github.com/rust-lang/crates.io-index" 349 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 350 + dependencies = [ 351 + "windows-targets", 352 + ] 353 + 354 + [[package]] 355 + name = "windows-targets" 356 + version = "0.52.0" 357 + source = "registry+https://github.com/rust-lang/crates.io-index" 358 + checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 359 + dependencies = [ 360 + "windows_aarch64_gnullvm", 361 + "windows_aarch64_msvc", 362 + "windows_i686_gnu", 363 + "windows_i686_msvc", 364 + "windows_x86_64_gnu", 365 + "windows_x86_64_gnullvm", 366 + "windows_x86_64_msvc", 367 + ] 368 + 369 + [[package]] 370 + name = "windows_aarch64_gnullvm" 371 + version = "0.52.0" 372 + source = "registry+https://github.com/rust-lang/crates.io-index" 373 + checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 374 + 375 + [[package]] 376 + name = "windows_aarch64_msvc" 377 + version = "0.52.0" 378 + source = "registry+https://github.com/rust-lang/crates.io-index" 379 + checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 380 + 381 + [[package]] 382 + name = "windows_i686_gnu" 383 + version = "0.52.0" 384 + source = "registry+https://github.com/rust-lang/crates.io-index" 385 + checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 386 + 387 + [[package]] 388 + name = "windows_i686_msvc" 389 + version = "0.52.0" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 392 + 393 + [[package]] 394 + name = "windows_x86_64_gnu" 395 + version = "0.52.0" 396 + source = "registry+https://github.com/rust-lang/crates.io-index" 397 + checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 398 + 399 + [[package]] 400 + name = "windows_x86_64_gnullvm" 401 + version = "0.52.0" 402 + source = "registry+https://github.com/rust-lang/crates.io-index" 403 + checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 404 + 405 + [[package]] 406 + name = "windows_x86_64_msvc" 407 + version = "0.52.0" 408 + source = "registry+https://github.com/rust-lang/crates.io-index" 409 + checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+16
Cargo.toml
··· 1 + [package] 2 + name = "gh-grader-preview" 3 + version = "0.1.0" 4 + edition = "2021" 5 + 6 + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 + 8 + [dependencies] 9 + anyhow = "1.0.79" 10 + clap = { version = "4.4.18", features = ["derive"] } 11 + clap_derive = "4.4.7" 12 + indicatif = "0.17.7" 13 + regex = "1.10.3" 14 + serde = { version = "1.0.196", features = ["derive"] } 15 + serde_json = "1.0.113" 16 + wait-timeout = "0.2.0"
+25
README.md
··· 1 + # GitHub Grader Preview 2 + 3 + Simple program that runs test cases specified in an autograding.json file and reports results. Useful for previewing how GitHub will run your autograder and project. 4 + 5 + Currently this relies on you setting `SHELL` in your environment meaning Windows won't work. 6 + 7 + ## Usage 8 + 9 + ```sh 10 + gh-grader-preview 11 + ``` 12 + 13 + Will auto-find the `autograding.json` file located in `.github/classroom` relative to the current dir. 14 + 15 + You can specify `-f` to choose a different file. 16 + 17 + ```sh 18 + gh-grader-preview -f some/other/dir/autograding.json 19 + ``` 20 + 21 + For more information, run `gh-grader-preview -h`. 22 + 23 + ## Building 24 + 25 + `cargo build --release`
+30
src/cli.rs
··· 1 + use clap::{command, Parser}; 2 + 3 + #[derive(Parser)] 4 + #[command(name="gh-grader-preview", author, version, about, long_about = None)] 5 + pub struct Cli { 6 + #[arg( 7 + short = 'f', 8 + long = "file", 9 + help = "Override the autograder.json file to use, by default we look in `.github/classroom/autograder.json`" 10 + )] 11 + pub file: Option<String>, 12 + #[arg( 13 + short = 'v', 14 + long = "verbose", 15 + help = "Show stdout and stderr of tests" 16 + )] 17 + pub verbose: bool, 18 + #[arg( 19 + short = 't', 20 + long = "test", 21 + help = "Run only the test specified (must match `name` case-insensitively)" 22 + )] 23 + pub test: Option<String>, 24 + #[arg( 25 + short = 'x', 26 + long = "skip", 27 + help = "Skip the first X tests, useful if you have tests that are purely informational" 28 + )] 29 + pub skip: Option<usize>, 30 + }
+71
src/grader.rs
··· 1 + use anyhow::Result; 2 + use serde::Deserialize; 3 + 4 + use crate::runner::TestResult; 5 + 6 + use super::runner; 7 + 8 + const AUTO_GRADER_DEFAULT_PATH: &str = ".github/classroom/autograding.json"; 9 + 10 + #[derive(Deserialize)] 11 + #[serde(rename_all = "snake_case")] 12 + pub enum ComparisonType { 13 + Included, 14 + Excluded, 15 + Regex, 16 + } 17 + 18 + #[derive(Deserialize)] 19 + pub struct TestCase { 20 + pub name: String, 21 + pub setup: String, 22 + pub run: String, 23 + pub input: String, 24 + pub output: String, 25 + pub comparison: ComparisonType, 26 + pub timeout: u32, 27 + pub points: Option<u32>, 28 + } 29 + 30 + #[derive(Deserialize)] 31 + pub struct AutoGraderData { 32 + pub tests: Vec<TestCase>, 33 + } 34 + 35 + impl AutoGraderData { 36 + pub fn parse(data: &str) -> Result<Self> { 37 + let data: AutoGraderData = serde_json::from_str(data)?; 38 + Ok(data) 39 + } 40 + 41 + pub fn read_file(path: &str) -> Result<Self> { 42 + println!("Reading auto-grader JSON from `{path}`..."); 43 + let data = std::fs::read_to_string(path)?; 44 + Self::parse(&data) 45 + } 46 + 47 + pub fn get(path: Option<String>) -> Result<Self> { 48 + let path = path.unwrap_or(AUTO_GRADER_DEFAULT_PATH.to_string()); 49 + Self::read_file(&path) 50 + } 51 + } 52 + 53 + impl TestCase { 54 + pub fn check_output(&self, output: String) -> Result<bool> { 55 + match self.comparison { 56 + ComparisonType::Included => Ok(output.contains(&self.output)), 57 + ComparisonType::Excluded => Ok(!output.contains(&self.output)), 58 + ComparisonType::Regex => { 59 + let re = regex::Regex::new(&self.output)?; 60 + Ok(re.is_match(&output)) 61 + } 62 + } 63 + } 64 + 65 + pub fn run(&self) -> Result<(bool, TestResult)> { 66 + runner::setup_phase(&self.setup)?; 67 + let res = runner::run_phase(&self.run, &self.input, self.timeout as u64)?; 68 + let matches = self.check_output(res.stdout.clone())?; 69 + Ok((matches, res)) 70 + } 71 + }
+89
src/main.rs
··· 1 + mod cli; 2 + mod grader; 3 + mod runner; 4 + 5 + use std::time::Duration; 6 + 7 + use anyhow::Result; 8 + use clap::Parser; 9 + 10 + use cli::Cli; 11 + use grader::AutoGraderData; 12 + use indicatif::ProgressBar; 13 + 14 + fn main() -> Result<()> { 15 + let cli = Cli::parse(); 16 + 17 + let grader_data = AutoGraderData::get(cli.file)?; 18 + 19 + let test_len = grader_data.tests.len(); 20 + 21 + let amount_passed = grader_data 22 + .tests 23 + .into_iter() 24 + .enumerate() 25 + .filter(|(i, test)| { 26 + let bar = ProgressBar::new_spinner(); 27 + bar.set_message(format!("Running {}", test.name)); 28 + bar.enable_steady_tick(Duration::from_millis(100)); 29 + if cli.skip.map(|skip| i < &skip).unwrap_or(false) 30 + || cli 31 + .test 32 + .as_ref() 33 + .map(|target| target.to_lowercase() != test.name.to_lowercase()) 34 + .unwrap_or(false) 35 + { 36 + bar.finish_with_message(format!("〰️ Skipped {}", test.name)); 37 + return false; 38 + } 39 + match test.run() { 40 + Ok((matched, result)) => { 41 + if cli.verbose { 42 + println!( 43 + "Stdout of {}:\n{}\n\nStderr of {}:\n{}", 44 + test.name, result.stdout, test.name, result.stderr 45 + ); 46 + } 47 + match result.status { 48 + Ok(code) => { 49 + if code != 0 { 50 + bar.finish_with_message(format!( 51 + "⚠️ {} Exited with code {code}", 52 + test.name 53 + )) 54 + } 55 + if matched { 56 + bar.finish_with_message(format!("✅ {} passed!", test.name)); 57 + true 58 + } else { 59 + bar.finish_with_message(format!( 60 + "❌ {} did not give the correct output", 61 + test.name 62 + )); 63 + false 64 + } 65 + } 66 + Err(why) => { 67 + bar.finish_with_message(format!( 68 + "❌ Failed to run {}: {why:?}", 69 + test.name 70 + )); 71 + false 72 + } 73 + } 74 + } 75 + Err(why) => { 76 + bar.finish_with_message(format!("❌ Failed to run {}: {why:?}", test.name)); 77 + false 78 + } 79 + } 80 + }) 81 + .count(); 82 + 83 + println!("Passed {amount_passed}/{test_len} tests"); 84 + if cli.skip.is_some() || cli.test.is_some() { 85 + println!("(Some were skipped)"); 86 + } 87 + 88 + Ok(()) 89 + }
+78
src/runner.rs
··· 1 + use std::{ 2 + env, 3 + io::{Read, Write}, 4 + process::{Command, Stdio}, 5 + time::Duration, 6 + }; 7 + 8 + use anyhow::{anyhow, Result}; 9 + use wait_timeout::ChildExt; 10 + 11 + pub fn setup_phase(cmd: &str) -> Result<()> { 12 + let shell = env::var("SHELL").map_err(|_| anyhow!("SHELL environment variable not set"))?; 13 + let mut child = Command::new(shell) 14 + .arg("-c") 15 + .arg(cmd) 16 + .spawn() 17 + .map_err(|e| anyhow!("Failed to spawn shell: {e:?}"))?; 18 + 19 + child 20 + .wait() 21 + .map_err(|e| anyhow!("Failed to execute \"{cmd}\": {e:?}"))?; 22 + Ok(()) 23 + } 24 + 25 + pub struct TestResult { 26 + pub stdout: String, 27 + pub stderr: String, 28 + pub status: Result<i32>, 29 + } 30 + 31 + fn read_stream<T>(mut stream: T) -> Option<String> 32 + where 33 + T: Read, 34 + { 35 + let mut buf = String::new(); 36 + stream 37 + .read_to_string(&mut buf) 38 + .map_err(|e| anyhow!("Failed to read stream: {e:?}")) 39 + .ok()?; 40 + Some(buf) 41 + } 42 + 43 + pub fn run_phase(cmd: &str, input: &str, timeout: u64) -> Result<TestResult> { 44 + let shell = env::var("SHELL").map_err(|_| anyhow!("SHELL environment variable not set"))?; 45 + let mut child = Command::new(shell) 46 + .arg("-c") 47 + .arg(cmd) 48 + .stdin(Stdio::piped()) 49 + .stdout(Stdio::piped()) 50 + .stderr(Stdio::piped()) 51 + .spawn() 52 + .map_err(|e| anyhow!("Failed to spawn shell: {e:?}"))?; 53 + 54 + let mut stdin = child 55 + .stdin 56 + .as_ref() 57 + .ok_or_else(|| anyhow!("Failed to get stdin"))?; 58 + 59 + write!(stdin, "{input}").map_err(|e| anyhow!("Failed to write to stdin: {e:?}"))?; 60 + 61 + let duration = Duration::from_secs(timeout); 62 + 63 + let res = child.wait_timeout(duration); 64 + 65 + let nested_res = res 66 + .map_err(|e| anyhow!("Failed to execute: {e:?}")) 67 + .map(|opt| opt.ok_or_else(|| anyhow!("Program Timed Out"))); 68 + 69 + let err = nested_res 70 + .and_then(|r| r) 71 + .map(|status| status.code().unwrap_or(0)); 72 + 73 + Ok(TestResult { 74 + stdout: child.stdout.and_then(read_stream).unwrap_or_default(), 75 + stderr: child.stderr.and_then(read_stream).unwrap_or_default(), 76 + status: err, 77 + }) 78 + }