use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Deserialize)]
struct Case {
input: String,
output: BTreeMap<String, serde_json::Value>,
#[serde(default)]
skip: Option<String>,
}
type ElementsByKind = BTreeMap<String, Vec<String>>;
fn normalize(value: &serde_json::Value) -> Vec<String> {
match value {
serde_json::Value::Array(items) => items
.iter()
.map(|v| {
v.as_str()
.expect("fixture array element must be a string")
.to_string()
})
.collect(),
serde_json::Value::String(s) => vec![s.clone()],
other => panic!("unexpected fixture value: {other:?}"),
}
}
fn parse_case(case: &Case) -> (ElementsByKind, ElementsByKind) {
let elements = anitomy_ng::parse(&case.input, anitomy_ng::Options::default());
let mut actual: ElementsByKind = BTreeMap::new();
for element in &elements {
actual
.entry(element.kind.as_str().to_string())
.or_default()
.push(element.value.clone());
}
let expected: ElementsByKind = case
.output
.iter()
.map(|(k, v)| (k.clone(), normalize(v)))
.collect();
(expected, actual)
}
fn load_cases(name: &str, data: &str) -> Vec<Case> {
assert!(!data.trim().is_empty(), "fixtures/{name}.json is empty");
serde_json::from_str(data).unwrap_or_else(|e| panic!("{name} fixtures must parse: {e}"))
}
fn failing_inputs(cases: &[Case]) -> BTreeSet<String> {
cases
.iter()
.filter(|c| c.skip.is_none())
.filter_map(|c| {
let (expected, actual) = parse_case(c);
(actual != expected).then(|| c.input.clone())
})
.collect()
}
fn manifest_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("known_failures")
.join(format!("{name}.txt"))
}
fn load_known_failures(name: &str) -> BTreeSet<String> {
match std::fs::read_to_string(manifest_path(name)) {
Ok(s) => s
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(String::from)
.collect(),
Err(_) => BTreeSet::new(),
}
}
fn write_known_failures(name: &str, failing: &BTreeSet<String>) {
let path = manifest_path(name);
std::fs::create_dir_all(path.parent().unwrap()).expect("create known_failures dir");
let body: String = failing.iter().map(|s| format!("{s}\n")).collect();
std::fs::write(&path, body).expect("write known_failures manifest");
}
fn updating() -> bool {
std::env::var_os("UPDATE_KNOWN_FAILURES").is_some()
}
fn print_diff(cases: &[Case], input: &str) {
let case = cases.iter().find(|c| c.input == input).unwrap();
let (expected, actual) = parse_case(case);
eprintln!(" input: {input}\n expected: {expected:?}\n actual: {actual:?}");
}
fn run_manifest_suite(name: &str, data: &str) {
let cases = load_cases(name, data);
let tested = cases.iter().filter(|c| c.skip.is_none()).count();
let failing = failing_inputs(&cases);
if updating() {
write_known_failures(name, &failing);
println!(
"{name}: wrote {} known failures ({}/{tested} passing)",
failing.len(),
tested - failing.len(),
);
return;
}
let known = load_known_failures(name);
let regressions: Vec<String> = failing.difference(&known).cloned().collect();
let fixed: Vec<String> = known.difference(&failing).cloned().collect();
println!(
"{name}: {}/{tested} passing ({} known failures)",
tested - failing.len(),
known.len(),
);
if regressions.is_empty() && fixed.is_empty() {
return;
}
if !regressions.is_empty() {
eprintln!(
"[{name}] {} newly-failing case(s) (regression):",
regressions.len()
);
for input in ®ressions {
print_diff(&cases, input);
}
}
if !fixed.is_empty() {
eprintln!(
"[{name}] {} case(s) now pass — remove them from tests/known_failures/{name}.txt:",
fixed.len(),
);
for input in &fixed {
eprintln!(" {input}");
}
}
panic!(
"[{name}] conformance set changed; after reviewing, regenerate with \
`UPDATE_KNOWN_FAILURES=1 cargo test -p anitomy-ng --test conformance`"
);
}
#[test]
fn anitomy_develop() {
run_manifest_suite(
"anitomy_develop",
include_str!("fixtures/anitomy_develop.json"),
);
}
#[test]
fn anitomy_master() {
run_manifest_suite(
"anitomy_master",
include_str!("fixtures/anitomy_master.json"),
);
}
#[test]
fn anitopy() {
run_manifest_suite("anitopy", include_str!("fixtures/anitopy.json"));
}
#[test]
fn anitomy_ng() {
run_manifest_suite("anitomy_ng", include_str!("fixtures/anitomy_ng.json"));
}