#![allow(dead_code, reason = "each suite uses the part of this it needs")]
use std::path::{Path, PathBuf};
pub fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("crates/en16931 is two levels below the workspace root")
.to_path_buf()
}
pub fn documentation() -> Vec<(String, String)> {
let root = workspace_root();
let mut out = Vec::new();
let mut add = |rel: &str| {
let p = root.join(rel);
if let Ok(text) = std::fs::read_to_string(&p) {
out.push((rel.to_owned(), text));
}
};
add("README.md");
for crate_dir in ["en16931", "en16931-formats", "en16931-cli"] {
add(&format!("crates/{crate_dir}/README.md"));
}
let mut stack = vec![root.join("site/content")];
for crate_dir in ["en16931", "en16931-formats", "en16931-cli"] {
stack.push(root.join(format!("crates/{crate_dir}/src")));
}
stack.push(root.join("xtask/src"));
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "md" || e == "rs") {
let rel = path
.strip_prefix(&root)
.unwrap_or(&path)
.display()
.to_string();
if let Ok(text) = std::fs::read_to_string(&path) {
out.push((rel, text));
}
}
}
}
assert!(
out.len() > 10,
"expected the READMEs and the site; found {} file(s)",
out.len()
);
out
}
pub struct Claim {
pub what: &'static str,
pub pattern: &'static str,
pub expected: usize,
}
const SEPARATORS: [char; 4] = [' ', '_', '\u{a0}', '\u{202f}'];
fn parse(found: &str) -> Option<usize> {
found.replace(SEPARATORS, "").parse().ok()
}
pub fn digit_runs(text: &str) -> Vec<(usize, usize, usize)> {
let mut runs = Vec::new();
let mut chars = text.char_indices().peekable();
while let Some((start, c)) = chars.next() {
if !c.is_ascii_digit() {
continue;
}
let mut end = start + c.len_utf8();
while let Some(&(i, c)) = chars.peek() {
if c.is_ascii_digit() {
end = i + c.len_utf8();
} else if !SEPARATORS.contains(&c) {
break;
}
chars.next();
}
if let Some(n) = parse(&text[start..end]) {
runs.push((n, start, end));
}
}
runs
}
pub fn find_all(pattern: &str, text: &str) -> Vec<usize> {
let parts: Vec<&str> = pattern.split("<N>").collect();
assert_eq!(parts.len(), 2, "a claim has exactly one <N>: {pattern}");
let (before, after) = (parts[0], parts[1]);
let needs_boundary = after
.chars()
.last()
.is_none_or(|c| c.is_ascii_alphanumeric());
digit_runs(text)
.into_iter()
.filter(|&(_, start, end)| {
text[..start].ends_with(before)
&& text[end..].starts_with(after)
&& !text[..start].ends_with("EN ")
&& (!needs_boundary
|| text[end + after.len()..]
.chars()
.next()
.is_none_or(|c| !c.is_ascii_alphanumeric()))
})
.map(|(n, _, _)| n)
.collect()
}
pub const HISTORICAL_OPEN: &str = "<!-- doc-numbers: historical -->";
pub const HISTORICAL_CLOSE: &str = "<!-- /doc-numbers -->";
fn strip_historical(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(i) = rest.find(HISTORICAL_OPEN) {
out.push_str(&rest[..i]);
let after = &rest[i..];
let end = after.find(HISTORICAL_CLOSE).unwrap_or_else(|| {
panic!("a `{HISTORICAL_OPEN}` region is never closed, which would turn off every check below it")
}) + HISTORICAL_CLOSE.len();
out.extend(std::iter::repeat_n(' ', end));
rest = &after[end..];
}
out.push_str(rest);
out
}
pub fn check(claims: &[Claim]) {
let docs: Vec<(String, String)> = documentation()
.into_iter()
.map(|(f, t)| (f, strip_historical(&t)))
.collect();
let mut wrong = Vec::new();
let mut never_matched = Vec::new();
for claim in claims {
let mut matched = 0usize;
for (file, text) in &docs {
for got in find_all(claim.pattern, text) {
matched += 1;
if got != claim.expected {
wrong.push(format!(
" {file}: {} — documented as {got}, measured {}",
claim.what, claim.expected
));
}
}
}
if matched == 0 {
never_matched.push(format!(
" {:?} — for {}; measured {}",
claim.pattern, claim.what, claim.expected
));
}
}
assert!(
never_matched.is_empty(),
"{} claim pattern(s) matched nothing anywhere, which means the prose was \
reworded and they are now checking nothing. Update the pattern rather \
than deleting it:\n{}",
never_matched.len(),
never_matched.join("\n")
);
assert!(
wrong.is_empty(),
"{} documented number(s) no longer match the code:\n{}",
wrong.len(),
wrong.join("\n")
);
}