Local runner for GitHub autograder
at main 103 lines 3.0 kB view raw
1use std::fmt::Display; 2 3use anyhow::Result; 4use serde::Deserialize; 5 6use crate::runner::TestResult; 7 8use super::runner; 9 10const AUTO_GRADER_DEFAULT_PATH: &str = ".github/classroom/autograding.json"; 11 12#[derive(Deserialize, Default)] 13#[serde(rename_all = "snake_case")] 14pub enum ComparisonType { 15 #[default] 16 Pass, 17 Included, 18 Excluded, 19 Exact, 20 Regex, 21} 22 23impl Display for ComparisonType { 24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 let s = match self { 26 ComparisonType::Included => "include this string", 27 ComparisonType::Excluded => "not include this string", 28 ComparisonType::Exact => "exactly match this string", 29 ComparisonType::Regex => "match against this regular expression", 30 ComparisonType::Pass => "can be anything", 31 }; 32 f.write_str(s) 33 } 34} 35 36#[derive(Deserialize)] 37pub struct TestCase { 38 pub name: String, 39 pub setup: Option<String>, 40 pub run: String, 41 #[serde(default)] 42 pub input: String, 43 pub output: Option<String>, 44 #[serde(default)] 45 pub comparison: ComparisonType, 46 pub timeout: u32, 47 #[allow(unused)] 48 pub points: Option<u32>, 49} 50 51#[derive(Deserialize)] 52pub struct AutoGraderData { 53 pub tests: Vec<TestCase>, 54} 55 56impl AutoGraderData { 57 pub fn parse(data: &str) -> Result<Self> { 58 let data: AutoGraderData = serde_json::from_str(data)?; 59 Ok(data) 60 } 61 62 pub fn read_file(path: &str) -> Result<Self> { 63 println!("Reading auto-grader JSON from `{path}`..."); 64 let data = std::fs::read_to_string(path)?; 65 Self::parse(&data) 66 } 67 68 pub fn get(path: Option<String>) -> Result<Self> { 69 let path = path.unwrap_or(AUTO_GRADER_DEFAULT_PATH.to_string()); 70 Self::read_file(&path) 71 } 72} 73 74impl TestCase { 75 fn check_output(&self, output: String) -> Result<bool> { 76 if let Some(ref expected_output) = self.output { 77 let expected_output = expected_output.replace("\r\n", "\n"); 78 79 match self.comparison { 80 ComparisonType::Included => Ok(output.contains(&expected_output)), 81 ComparisonType::Excluded => Ok(!output.contains(&expected_output)), 82 ComparisonType::Exact => Ok(output.trim() == expected_output.trim()), 83 ComparisonType::Regex => { 84 let re = regex::Regex::new(&expected_output)?; 85 Ok(re.is_match(&output)) 86 } 87 ComparisonType::Pass => Ok(true), 88 } 89 } else { 90 Ok(true) 91 } 92 } 93 94 pub fn run(&self) -> Result<(bool, TestResult)> { 95 if let Some(setup) = self.setup.as_ref() { 96 runner::setup_phase(setup)?; 97 } 98 let res = runner::run_phase(&self.run, &self.input, self.timeout as u64)?; 99 let output = res.stdout.replace("\r\n", "\n"); 100 let matches = self.check_output(output)?; 101 Ok((matches, res)) 102 } 103}