use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("workspace root")
}
fn read(rel: &str) -> String {
let p = workspace_root().join(rel);
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
fn serde_renames(source_rel: &str, enum_name: &str) -> BTreeSet<String> {
serde_renames_in(&read(source_rel), enum_name)
}
fn serde_renames_in(src: &str, enum_name: &str) -> BTreeSet<String> {
let start = src
.find(&format!("pub enum {enum_name}"))
.unwrap_or_else(|| panic!("{enum_name} not found in the source read for it"));
let body = &src[start..];
let end = body.find("\n}").expect("enum body is unterminated");
let body = &body[..end];
let explicit: BTreeSet<String> = body
.match_indices("#[serde(rename = \"")
.map(|(i, m)| {
let rest = &body[i + m.len()..];
rest[..rest.find('"').expect("unterminated serde rename")].to_string()
})
.collect();
let derived: BTreeSet<String> = body
.lines()
.filter_map(|l| {
let t = l.trim();
let ident: String = t
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect();
if ident.is_empty() || !ident.starts_with(|c: char| c.is_ascii_uppercase()) {
return None;
}
let rest = t[ident.len()..].trim_start();
let looks_like_variant = rest.is_empty()
|| rest.starts_with('{')
|| rest.starts_with('(')
|| rest.starts_with(',')
|| rest.starts_with('=');
looks_like_variant.then(|| snake_case(&ident))
})
.collect();
explicit.union(&derived).cloned().collect()
}
fn snake_case(ident: &str) -> String {
let mut out = String::new();
for (i, c) in ident.chars().enumerate() {
if c.is_ascii_uppercase() {
if i != 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
out
}
fn documented_types(doc_rel: &str, heading: &str) -> BTreeSet<String> {
documented_types_in(&read(doc_rel), heading)
}
fn documented_types_in(md: &str, heading: &str) -> BTreeSet<String> {
md.lines()
.filter_map(|l| l.strip_prefix(heading))
.filter_map(|rest| {
let rest = rest.trim_start();
let inner = rest.strip_prefix('`')?;
let name = &inner[..inner.find('`')?];
(!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
.then(|| name.to_string())
})
.collect()
}
fn assert_parity(
what: &str,
doc_rel: &str,
documented: &BTreeSet<String>,
real: &BTreeSet<String>,
) {
let phantom: Vec<_> = documented.difference(real).cloned().collect();
let undocumented: Vec<_> = real.difference(documented).cloned().collect();
assert!(
phantom.is_empty(),
"{doc_rel} documents {what} the parser rejects: {phantom:?}\n\
A reader who follows the reference writes one of these and gets a parse error, with the \
reference being the thing they would consult to find out why. Remove the section, or add \
the variant."
);
assert!(
undocumented.is_empty(),
"{doc_rel} has no section for {what} a user can write: {undocumented:?}\n\
Each of these carries a serde rename on a tagged enum, so it parses today."
);
}
#[test]
fn sequence_rule_types_and_their_reference_agree() {
let real = serde_renames("crates/assay-core/src/model/types.rs", "SequenceRule");
assert!(
!real.is_empty(),
"extracted no variant names from SequenceRule, so every assertion below would be vacuous"
);
let documented = documented_types("docs/reference/config/sequences.md", "### ");
assert_parity(
"sequence rule types",
"docs/reference/config/sequences.md",
&documented,
&real,
);
}
#[test]
fn trace_assertion_types_and_their_reference_agree() {
let real = serde_renames(
"crates/assay-core/src/agent_assertions/model.rs",
"TraceAssertion",
);
assert!(
!real.is_empty(),
"extracted no variant names from TraceAssertion, so every assertion below would be vacuous"
);
let documented = documented_types("docs/reference/config/eval-yaml.md", "#### ");
assert_parity(
"assertion types",
"docs/reference/config/eval-yaml.md",
&documented,
&real,
);
}
#[cfg(test)]
mod extractor {
use super::serde_renames_in;
#[test]
fn a_variant_without_an_explicit_rename_is_still_found() {
let src = r#"
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Thing {
#[serde(rename = "already_named")]
AlreadyNamed { a: String },
NewCheck { thing: String },
}
"#;
let got = serde_renames_in(src, "Thing");
assert!(got.contains("already_named"), "got {got:?}");
assert!(
got.contains("new_check"),
"the unrenamed variant was dropped: {got:?}"
);
}
#[test]
fn every_variant_shape_is_found() {
let src = r#"
#[serde(rename_all = "snake_case")]
pub enum Thing {
Structy { a: String },
Tuply(String),
Unity,
Trailing,
Discriminated = 1,
}
"#;
let got = serde_renames_in(src, "Thing");
for want in ["structy", "tuply", "unity", "trailing", "discriminated"] {
assert!(got.contains(want), "{want} missing from {got:?}");
}
}
#[test]
fn a_documented_type_with_a_digit_is_read() {
let md = "### `sequence_v2` — Something\n### `plain` — Other\n";
let got = super::documented_types_in(md, "### ");
assert!(
got.contains("sequence_v2"),
"digit-bearing name dropped: {got:?}"
);
assert!(got.contains("plain"), "got {got:?}");
}
#[test]
fn prose_headings_are_not_types() {
let md = "### 1. Start Simple\n### E-commerce: Payment Flow\n### `real_one`\n";
let got = super::documented_types_in(md, "### ");
assert_eq!(got.len(), 1, "got {got:?}");
assert!(got.contains("real_one"));
}
#[test]
fn non_variants_are_not_mistaken_for_variants() {
let src = r#"
pub enum Thing {
/// Doc comment mentioning Something.
#[serde(default)]
Real { field_name: String, Another: u8 },
}
"#;
let got = serde_renames_in(src, "Thing");
assert!(got.contains("real"), "got {got:?}");
assert!(
!got.contains("doc"),
"doc comment read as a variant: {got:?}"
);
}
}