Local runner for GitHub autograder
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}