1use std::str::{from_utf8, Utf8Error};
2
3use log::debug;
4use miette::{Diagnostic, Report, SourceSpan};
5use serde::Deserialize;
6use similar::{ChangeTag, DiffOp, TextDiff};
7use thiserror::Error;
8use tokio::process::Command;
9
10use crate::config::Config;
11
12#[derive(Deserialize, Debug)]
15pub struct TestUnits {
16 pub tests: Vec<TestUnit>,
17}
18
19#[derive(Deserialize, Debug)]
20pub struct TestUnit {
21 name: String,
22 input: Vec<String>,
23 expected: String,
24 rubric: u64,
25}
26
27impl TestUnit {
28 pub fn interpolate_config(
30 &mut self,
31 config: &Config,
32 executable: &str,
33 ) -> Result<(), UnitError> {
34 const PROJECT_DIR_SUBSTRING: &str = "$project";
35 const DIGITAL_JAR_SUBSTRING: &str = "$digital";
36 self.input
37 .iter_mut()
38 .map(|slice| {
39 if slice.contains(PROJECT_DIR_SUBSTRING) {
40 *slice = slice.replace(PROJECT_DIR_SUBSTRING, executable)
41 } else if slice.contains(DIGITAL_JAR_SUBSTRING) {
42 let digital_path = config.test.clone().unwrap().digital_path();
43 *slice = match digital_path {
44 Some(path) => path,
45 None => {
46 eprintln!("Error: digital_path is not set.");
47 return Err(UnitError::DigitalJarPathNotSpecified);
48 }
49 };
50 }
51 Ok(())
52 })
53 .for_each(drop); Ok(())
55 }
56}
57
58#[derive(Debug)]
59struct UnitOutput {
60 name: String,
62 grade: u64,
63 rubric: u64,
64}
65
66#[derive(Error, Diagnostic, Debug)]
67#[error("One or more tests failed")]
68pub struct UnitErrors {
69 #[source_code]
70 src: String,
71 #[related]
72 errors: Vec<UnitError>,
73}
74
75#[derive(Error, Diagnostic, Debug)]
76pub enum UnitError {
77 #[error("Program crashed")]
80 ProgramCrashed,
81 #[error("Output doesn't match expected result")]
85 IncorrectOutput,
86 #[error("Not UTF8")]
87 NotUtf8(Utf8Error),
88 #[error("Could not run program")]
89 Wrapped(std::io::Error),
90 #[error("Could not interpolate string")]
91 DigitalJarPathNotSpecified,
92}
93
94#[derive(Error, Diagnostic, Debug)]
95#[error("Output doesn't match expected result")]
96#[diagnostic()]
100pub struct IncorrectOutput {
101 #[related]
102 span_list: Vec<IncorrectSpan>,
103}
104
105#[derive(Error, Diagnostic, Debug, Clone)]
106#[error("Want: {expected:?}, got: ")]
107struct IncorrectSpan {
108 expected: Option<String>,
109 #[source_code]
110 got: String,
111 #[label("here")]
112 at: SourceSpan,
113}
114
115impl TestUnits {
129 pub async fn run(self) -> miette::Result<u64> {
130 let mut tasks = Vec::with_capacity(self.tests.len());
131 for unit in self.tests {
132 tasks.push(tokio::spawn(unit.run()))
133 }
134
135 let mut outputs = Vec::with_capacity(tasks.len());
136 for task in tasks {
137 outputs.push(task.await.unwrap());
138 }
139
140 let grade: u64 = outputs
141 .into_iter()
142 .map(|out| match out {
143 Ok(out) => {
144 println!("{}: ({}/{})", out.name, out.grade, out.rubric);
145 out.grade
146 }
147 Err(e) => {
148 let report = Report::new(e);
149 eprintln!("{:?}", report);
150 0
151 }
152 })
153 .sum();
154
155 Ok(grade)
156 }
157}
158
159impl TestUnit {
161 async fn run(self) -> Result<UnitOutput, UnitError> {
162 let output = Command::new(self.input.first().expect("Empty input in tests file!"))
163 .args(
164 self.input
165 .split_first()
166 .expect("Empty input in tests file!")
167 .1,
168 )
169 .output()
170 .await
171 .map_err(UnitError::Wrapped)?;
172
173 let stdout = from_utf8(&output.stdout)
178 .map_err(UnitError::NotUtf8)?
179 .trim();
180
181 let mut errors = vec![];
182 let diff = TextDiff::from_lines(self.expected.trim(), stdout);
183 for op in diff.ops() {
184 for change in diff.iter_changes(op) {
185 if change.tag() == ChangeTag::Equal || change.value() == "\n" {
186 continue;
187 }
188
189 errors.push(IncorrectSpan {
192 expected: Some(self.expected.clone()),
193 got: change.to_string(),
197 at: (op.new_range().into()),
198 })
199 }
200 }
201
202 if errors.is_empty() {
203 Ok(UnitOutput {
205 name: self.name,
206 grade: self.rubric,
207 rubric: self.rubric,
208 })
209 } else {
210 Err(
211 UnitError::IncorrectOutput, )
216 }
217 }
218}
219
220#[allow(clippy::needless_return)]
222#[tokio::test]
223async fn test_unit_run() -> miette::Result<()> {
224 use miette::IntoDiagnostic;
225
226 let test = TestUnit {
227 name: "".into(),
228 input: ["echo", "hello world"]
229 .iter_mut()
230 .map(|s| s.to_owned())
231 .collect(),
232 expected: "hello world".into(),
233 rubric: 100,
234 };
235 test.run().await.into_diagnostic().unwrap();
236
237 let test = TestUnit {
238 name: "".into(),
239 input: ["echo", "howdy y'all"]
240 .iter_mut()
241 .map(|s| s.to_owned())
242 .collect(),
243 expected: "hello world".into(),
244 rubric: 100,
245 };
246 let res = test.run().await;
248 assert!(res.is_err());
249 println!("{:?}", res);
250
251 Ok(())
252}