use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const ALLOWLIST: &[&str] = &[
"STO-1", "STO-3", "STO-10", "STO-11", "STO-12", "STO-20", "STO-21", "STO-22", "STO-23",
"STO-30", "STO-31",
"LIFE-3", "LIFE-6", "LIFE-15", "LIFE-21",
"NS-3", "NS-10", "DSC-13",
"INIT-8",
"CLI-142", "CLI-143",
"TUI-1",
"HOOK-71",
"HOOK-73",
];
#[test]
fn every_spec_id_is_cited_or_allowlisted() {
let defined = defined_ids();
assert!(
defined.len() > 50,
"found only {} spec IDs; the parser or spec layout likely changed",
defined.len()
);
let cited = cited_ids();
let allow: BTreeSet<String> = ALLOWLIST.iter().map(|s| s.to_string()).collect();
let undefined: Vec<_> = cited.difference(&defined).cloned().collect();
assert!(
undefined.is_empty(),
"tests cite spec IDs not defined in spec/ (document them): {undefined:?}"
);
let stale: Vec<_> = allow.difference(&defined).cloned().collect();
assert!(
stale.is_empty(),
"ALLOWLIST references unknown spec IDs: {stale:?}"
);
let redundant: Vec<_> = allow.intersection(&cited).cloned().collect();
assert!(
redundant.is_empty(),
"these IDs are now cited by tests; remove them from ALLOWLIST: {redundant:?}"
);
let uncovered: Vec<_> = defined
.iter()
.filter(|id| !cited.contains(*id) && !allow.contains(*id))
.cloned()
.collect();
assert!(
uncovered.is_empty(),
"spec IDs with no test citation (add a test that cites them, or ALLOWLIST them): {uncovered:?}"
);
}
fn root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn is_id(tok: &str) -> bool {
match tok.split_once('-') {
Some((alpha, num)) => {
(2..=4).contains(&alpha.len())
&& alpha.bytes().all(|b| b.is_ascii_uppercase())
&& !num.is_empty()
&& num.bytes().all(|b| b.is_ascii_digit())
}
None => false,
}
}
fn defined_ids() -> BTreeSet<String> {
let mut out = BTreeSet::new();
for md in files_with_ext(&root().join("spec"), "md") {
let text = std::fs::read_to_string(&md).unwrap();
for line in text.lines() {
if let Some(rest) = line.trim_start().strip_prefix("- `")
&& let Some(end) = rest.find('`')
{
let tok = &rest[..end];
if is_id(tok) {
out.insert(tok.to_string());
}
}
}
}
out
}
fn cited_ids() -> BTreeSet<String> {
const MARKER: &str = "// spec:";
let mut out = BTreeSet::new();
let mut sources = files_with_ext(&root().join("src"), "rs");
sources.extend(files_with_ext(&root().join("tests"), "rs"));
for f in sources {
if f.file_name().is_some_and(|n| n == "spec_coverage.rs") {
continue; }
let text = std::fs::read_to_string(&f).unwrap();
for line in text.lines() {
if let Some(idx) = line.find(MARKER) {
for tok in id_tokens(&line[idx + MARKER.len()..]) {
out.insert(tok);
}
}
}
}
out
}
fn id_tokens(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
for c in text.chars() {
if c.is_ascii_alphanumeric() || c == '-' {
cur.push(c);
} else {
if is_id(&cur) {
out.push(cur.clone());
}
cur.clear();
}
}
if is_id(&cur) {
out.push(cur);
}
out
}
fn files_with_ext(dir: &Path, ext: &str) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(dir) else {
return out;
};
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
out.extend(files_with_ext(&path, ext));
} else if path.extension().is_some_and(|e| e == ext) {
out.push(path);
}
}
out
}