use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use protocol_droid_schema::generator::{
emit, emit::Generated, schema_model, schema_model::SchemaFile,
};
const REVISIONS: &[(&str, usize)] = &[
("2024-11-05", 79),
("2025-03-26", 83),
("2025-06-18", 91),
("2025-11-25", 145),
("2026-07-28", 155),
("draft", 155),
];
fn spec_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.map_or_else(PathBuf::new, |root| {
root.join("crates/protocol-droid-schema/spec")
})
}
fn load(revision: &str) -> String {
let path = spec_dir().join(revision).join("schema.json");
std::fs::read_to_string(&path).unwrap_or_default()
}
fn generate(revision: &str) -> Option<(SchemaFile, Generated)> {
let file = schema_model::parse(revision, &load(revision)).ok()?;
let generated = emit::emit(&file);
Some((file, generated))
}
#[track_caller]
fn generated(revision: &str) -> (SchemaFile, Generated) {
generate(revision).unwrap_or_else(|| fail(&format!("{revision}: could not be parsed or read")))
}
fn fail(message: &str) -> ! {
eprintln!("test failed: {message}");
std::process::exit(1)
}
#[test]
fn every_revision_parses_and_emits() {
for (revision, expected) in REVISIONS {
let (file, generated) = generated(revision);
assert_eq!(
file.defs.len(),
*expected,
"{revision}: definition count changed; the vendored schema moved"
);
assert_eq!(
generated.type_names.len(),
*expected,
"{revision}: emitter dropped or invented a definition"
);
assert!(
generated.dangling_refs.is_empty(),
"{revision}: $ref targets that the schema does not define: {:?}",
generated.dangling_refs
);
}
}
#[test]
fn dialects_split_as_documented() {
for (revision, _) in REVISIONS {
let (file, _) = generated(revision);
let is_2020 = file.dialect.contains("2020-12");
assert_eq!(
is_2020,
*revision == "2025-11-25" || *revision == "2026-07-28" || *revision == "draft",
"{revision}: unexpected dialect {dialect}",
dialect = file.dialect
);
}
}
#[test]
fn every_struct_carries_the_flattened_catch_all() {
for (revision, _) in REVISIONS {
let (_, generated) = generated(revision);
let structs = generated
.source
.match_indices("pub struct ")
.filter(|(at, _)| {
let rest = &generated.source[at + "pub struct ".len()..];
!rest.starts_with("UntaggedEnum")
})
.count();
let catches = generated
.source
.matches("#[serde(flatten)]\n pub extra:")
.count();
assert_eq!(
catches, structs,
"{revision}: {structs} schema structs but {catches} flattened \
catch-alls — unknown members would be dropped"
);
assert!(
structs > 0,
"{revision}: no structs generated at all, which means the test below \
would pass vacuously"
);
}
}
#[test]
fn no_type_rejects_unknown_fields() {
for (revision, _) in REVISIONS {
let (_, generated) = generated(revision);
assert!(
!generated.source.contains("deny_unknown_fields"),
"{revision}: a generated type rejects unknown fields"
);
}
}
#[test]
fn shared_type_names_exist_in_every_revision() {
let mut per_revision = Vec::new();
for (revision, _) in REVISIONS {
let (file, _) = generated(revision);
per_revision.push((revision, file.defs.keys().cloned().collect::<BTreeSet<_>>()));
}
let Some((_, first)) = per_revision.first() else {
fail("no revisions were generated, so there is nothing to intersect");
};
let shared: BTreeSet<&String> = first
.iter()
.filter(|name| per_revision.iter().all(|(_, names)| names.contains(*name)))
.collect();
assert!(
shared.len() > 50,
"only {} type names are common to all six revisions: {shared:?}",
shared.len()
);
}
#[test]
fn shared_names_have_a_stable_shape() {
for (revision, _) in REVISIONS {
let (file, _) = generated(revision);
for name in ["Request", "Result", "Content", "Role", "Error"] {
let Some(schema) = file.defs.get(name) else {
continue;
};
assert!(
!schema.is_permissive() || schema.properties.is_empty(),
"{revision}: `{name}` is unconstrained, which usually means the \
model mis-parsed it"
);
}
}
}
#[test]
fn emission_is_deterministic() {
for (revision, _) in REVISIONS {
let (_, first) = generated(revision);
let (_, second) = generated(revision);
assert_eq!(
first.source, second.source,
"{revision}: two runs over identical input produced different source"
);
}
}
#[test]
fn generated_source_is_syntactically_plausible() {
for (revision, _) in REVISIONS {
let (_, generated) = generated(revision);
let opens = generated.source.matches('{').count();
let closes = generated.source.matches('}').count();
assert_eq!(
opens, closes,
"{revision}: unbalanced braces — {opens} open, {closes} close"
);
assert!(
generated.source.starts_with("//!"),
"{revision}: generated file must open with its provenance header"
);
assert!(
generated.source.contains("GENERATED — do not edit"),
"{revision}: generated file must say it is generated, so a hand-edit \
is obvious in review"
);
}
}