use serde::Deserialize;
use serde::de::DeserializeOwned;
use crate::error::EvalError;
#[derive(Debug, Clone, Deserialize)]
pub struct EvalCase<Setup, Expect> {
pub name: String,
pub instruction: String,
#[serde(default)]
pub setup: Setup,
pub expect: Vec<Expect>,
}
pub fn parse_cases_from_str<Setup, Expect>(
name: &str,
contents: &str,
) -> Result<Vec<EvalCase<Setup, Expect>>, EvalError>
where
Setup: DeserializeOwned + Default,
Expect: DeserializeOwned,
{
if let Ok(list) = ron::from_str::<Vec<EvalCase<Setup, Expect>>>(contents) {
return Ok(list);
}
match ron::from_str::<EvalCase<Setup, Expect>>(contents) {
Ok(case) => Ok(vec![case]),
Err(source) => Err(EvalError::RonParse {
path: std::path::PathBuf::from(name),
source,
}),
}
}
pub fn load_cases<Setup, Expect>(
dir: &std::path::Path,
) -> Result<Vec<EvalCase<Setup, Expect>>, EvalError>
where
Setup: DeserializeOwned + Default,
Expect: DeserializeOwned,
{
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.map_err(|source| EvalError::Io {
path: Some(dir.to_path_buf()),
source,
})?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("ron")))
.collect();
paths.sort();
let mut cases = Vec::with_capacity(paths.len());
for path in paths {
let text = std::fs::read_to_string(&path).map_err(|source| EvalError::Io {
path: Some(path.clone()),
source,
})?;
let parsed = parse_cases_from_str::<Setup, Expect>(&path.to_string_lossy(), &text)?;
cases.extend(parsed);
}
Ok(cases)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use std::io::Write;
#[derive(Debug, Default, Deserialize, PartialEq)]
struct Setup {
seed: u64,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Expect {
contains: String,
}
#[test]
fn loads_sorted_with_default_setup_and_ignores_non_ron() {
let dir = tempfile::tempdir().unwrap();
let mut a = std::fs::File::create(dir.path().join("a.ron")).unwrap();
write!(
a,
r#"(name: "alpha", instruction: "do A", setup: (seed: 5), expect: [(contains: "x")])"#
)
.unwrap();
let mut b = std::fs::File::create(dir.path().join("b.ron")).unwrap();
write!(
b,
r#"(name: "beta", instruction: "do B", expect: [(contains: "y")])"#
)
.unwrap();
std::fs::write(dir.path().join("notes.txt"), "ignored").unwrap();
let cases: Vec<EvalCase<Setup, Expect>> = load_cases(dir.path()).unwrap();
assert_eq!(cases.len(), 2, "two .ron cases, the .txt ignored");
assert_eq!(cases[0].name, "alpha");
assert_eq!(cases[0].setup, Setup { seed: 5 });
assert_eq!(cases[1].name, "beta");
assert_eq!(cases[1].setup, Setup::default(), "omitted setup defaults");
}
#[test]
fn malformed_ron_is_a_hard_error() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("bad.ron"), "this is not ron").unwrap();
let err = load_cases::<Setup, Expect>(dir.path()).unwrap_err();
assert!(
err.to_string().contains("bad.ron"),
"error names the offending file: {err}"
);
}
#[test]
fn parse_cases_handles_single_and_list_and_names_malformed_file() {
let single: Vec<EvalCase<Setup, Expect>> = parse_cases_from_str(
"single.ron",
r#"(name: "solo", instruction: "do it", expect: [(contains: "z")])"#,
)
.unwrap();
assert_eq!(single.len(), 1);
assert_eq!(single[0].name, "solo");
assert_eq!(single[0].setup, Setup::default());
let many: Vec<EvalCase<Setup, Expect>> = parse_cases_from_str(
"many.ron",
r#"[
(name: "one", instruction: "first", setup: (seed: 1), expect: [(contains: "a")]),
(name: "two", instruction: "second", expect: [(contains: "b")]),
]"#,
)
.unwrap();
assert_eq!(many.len(), 2);
assert_eq!(many[0].name, "one");
assert_eq!(many[0].setup, Setup { seed: 1 });
assert_eq!(many[1].name, "two");
let err = parse_cases_from_str::<Setup, Expect>("oops.ron", "this is not ron").unwrap_err();
assert!(
err.to_string().contains("oops.ron"),
"both-parse-failed error names the file: {err}"
);
}
#[test]
fn load_cases_mixes_single_and_multi_case_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("a_single.ron"),
r#"(name: "solo", instruction: "alone", expect: [(contains: "x")])"#,
)
.unwrap();
std::fs::write(
dir.path().join("b_multi.ron"),
r#"[
(name: "m1", instruction: "first", expect: [(contains: "y")]),
(name: "m2", instruction: "second", expect: [(contains: "z")]),
]"#,
)
.unwrap();
let cases: Vec<EvalCase<Setup, Expect>> = load_cases(dir.path()).unwrap();
assert_eq!(
cases.len(),
3,
"1 from the single file + 2 from the multi file"
);
let names: Vec<&str> = cases.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["solo", "m1", "m2"]);
}
}