Local runner for GitHub autograder
0
fork

Configure Feed

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

at main 163 lines 5.1 kB view raw
1mod cli; 2mod grader; 3mod meta; 4mod runner; 5 6use std::time::Duration; 7 8use anyhow::Result; 9use clap::Parser; 10 11use cli::Cli; 12use grader::AutoGraderData; 13use indicatif::ProgressBar; 14 15use crate::{grader::ComparisonType, runner::TestResult}; 16 17struct CmdOutput(String, String, Option<String>); 18 19impl CmdOutput { 20 fn format_output(self, name: &str, comp: &ComparisonType) -> String { 21 let CmdOutput(stdout, stderr, expected) = self; 22 23 let heading = format!("┏━┻ Stdout of {name}"); 24 let stdout = stdout.trim().replace("\n", "\n"); 25 let mid = format!("┣━━ Stderr of {name}"); 26 let stderr = stderr.trim().replace("\n", "\n"); 27 let (mid2, expected) = if let Some(expected) = expected { 28 ( 29 format!("┣━━ Stdout of {name} must {comp}"), 30 expected.trim().replace("\n", "\n"), 31 ) 32 } else { 33 (format!("┣━━ {name} has no output check"), String::new()) 34 }; 35 let foot = "┗━━ End of output".to_string(); 36 37 format!("{heading}\n{stdout}\n{mid}\n{stderr}\n{mid2}\n{expected}\n{foot}") 38 } 39} 40 41#[derive(Debug)] 42enum TestOutput { 43 Passed, 44 LogicError, 45 NonZeroExit(Option<i32>), 46 RunnerError(anyhow::Error), 47 Skipped, 48} 49 50impl From<Result<(bool, TestResult)>> for TestOutput { 51 fn from(res: Result<(bool, TestResult)>) -> Self { 52 match res { 53 Ok((matched, TestResult { status: Ok(0), .. })) => { 54 if matched { 55 Self::Passed 56 } else { 57 Self::LogicError 58 } 59 } 60 Ok((_, TestResult { status, .. })) => Self::NonZeroExit(status.ok()), 61 Err(why) => Self::RunnerError(why), 62 } 63 } 64} 65 66impl TestOutput { 67 fn output(&self) -> (&str, String) { 68 match self { 69 TestOutput::Passed => ("", "Passed".to_string()), 70 TestOutput::LogicError => ("", "Did not match expected output".to_string()), 71 TestOutput::NonZeroExit(code) => ( 72 "💥", 73 format!("Exited with code {}", code.unwrap_or(i32::MAX)), 74 ), 75 TestOutput::RunnerError(error) => ("🙈", format!("Wasn't able to run: {error:?}")), 76 TestOutput::Skipped => ("〰️", "Was skipped".to_string()), 77 } 78 } 79} 80 81fn main() -> Result<()> { 82 let cli = Cli::parse(); 83 84 if cli.man_gen { 85 println!("{}", meta::gen_man_page()); 86 return Ok(()); 87 } else if let Some(shell) = cli.completions { 88 println!("{}", meta::gen_completions(shell.parse().unwrap())); 89 return Ok(()); 90 } 91 92 let target_test = cli.test.as_ref().map(|s| s.to_lowercase()); 93 94 let grader_data = AutoGraderData::get(cli.file)?; 95 96 let outputs = grader_data 97 .tests 98 .into_iter() 99 .enumerate() 100 .map(|(i, test)| { 101 // In verbose mode make the output a bit easier to follow 102 if cli.verbose { 103 println!(); 104 } 105 let bar = ProgressBar::new_spinner(); 106 bar.set_message(format!("Running {}", test.name)); 107 bar.enable_steady_tick(Duration::from_millis(100)); 108 109 let skip_number = cli.skip.map(|skip| i < skip).unwrap_or(false); 110 111 let skip_non_target = target_test 112 .as_ref() 113 .is_some_and(|s| *s != test.name.to_lowercase()); 114 115 let (output, verbose_output) = if skip_number || skip_non_target { 116 (TestOutput::Skipped, None) 117 } else { 118 let result = test.run(); 119 let verbose_output = 120 result 121 .as_ref() 122 .ok() 123 .map(|(_, TestResult { stdout, stderr, .. })| { 124 CmdOutput(stdout.clone(), stderr.clone(), test.output.clone()) 125 }); 126 (result.into(), verbose_output) 127 }; 128 129 let (emoji, msg) = output.output(); 130 131 bar.finish_with_message(format!("{emoji} {} - {msg}", test.name)); 132 133 if let (Some(verbose_output), true) = (verbose_output, cli.verbose) { 134 println!( 135 "{}", 136 verbose_output.format_output(&test.name, &test.comparison) 137 ); 138 } 139 140 output 141 }) 142 .collect::<Vec<_>>(); 143 144 let total_tests = outputs.len(); 145 let total_not_skipped = outputs 146 .iter() 147 .filter(|o| !matches!(o, TestOutput::Skipped)) 148 .count(); 149 let total_passed = outputs 150 .iter() 151 .filter(|o| matches!(o, TestOutput::Passed)) 152 .count(); 153 154 let percent_passed = total_passed as f32 / total_not_skipped as f32 * 100_f32; 155 156 println!("\n== TEST SUMMARY =="); 157 println!("{total_passed} / {total_not_skipped} Tests Passed ({percent_passed:.2}%)"); 158 if total_tests != total_not_skipped { 159 println!("({} Tests Were Skipped)", total_tests - total_not_skipped); 160 } 161 162 Ok(()) 163}