use std::path::{Path, PathBuf};
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/frink-cli has two ancestors")
.to_path_buf()
}
fn walk(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
out.extend(walk(&p));
} else if p.extension().is_some_and(|x| x == "rs") {
out.push(p);
}
}
out
}
fn read(rel: &str) -> String {
let p = repo_root().join(rel);
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("reading {}: {e}", p.display()))
}
fn long_flags(source: &str) -> Vec<String> {
let mut out = Vec::new();
for (i, _) in source.match_indices("long = \"") {
let rest = &source[i + "long = \"".len()..];
if let Some(end) = rest.find('"') {
let f = rest[..end].to_string();
if !out.contains(&f) {
out.push(f);
}
}
}
out
}
#[test]
fn every_run_flag_is_documented() {
let flags = long_flags(&read("crates/frink-cli/src/run.rs"));
assert!(flags.len() > 30, "only {} flags parsed", flags.len());
let docs = read("docs/CLI.md");
let missing: Vec<&String> = flags.iter().filter(|f| !documents(&docs, f)).collect();
assert!(
missing.is_empty(),
"these `frink run` flags are accepted and undocumented in docs/CLI.md: {missing:?}"
);
}
#[test]
fn every_server_flag_is_documented() {
let flags = long_flags(&read("crates/frink-server/src/cli.rs"));
assert!(flags.len() > 20, "only {} flags parsed", flags.len());
let docs = ["docs/CLI.md", "docs/API.md", "docs/CONFIG.md", "README.md"]
.iter()
.map(|p| read(p))
.collect::<Vec<_>>()
.join("\n");
let missing: Vec<&String> = flags.iter().filter(|f| !documents(&docs, f)).collect();
assert!(
missing.is_empty(),
"these `frink-server` flags are accepted and undocumented: {missing:?}"
);
}
const NAMED_AS_ABSENT: &[&str] = &[
"in-file",
"ppl-stride",
"tokenize",
"release",
"split-",
];
#[test]
fn every_documented_flag_is_one_the_cli_accepts() {
let mut accepted: Vec<String> = Vec::new();
let mut sources = 0usize;
for dir in ["crates/frink-cli/src", "crates/frink-server/src"] {
for entry in walk(&repo_root().join(dir)) {
let text = std::fs::read_to_string(&entry).expect("read a source file");
sources += 1;
accepted.extend(long_flags(&text));
accepted.extend(field_flags(&text));
for key in [
"alias = \"",
"visible_alias = \"",
"aliases = [",
"visible_aliases = [",
] {
for (i, _) in text.match_indices(key) {
let rest = &text[i + key.len()..];
let group = &rest[..rest
.find(']')
.unwrap_or(0)
.max(rest.find('"').map_or(0, |q| q + 1))];
for (j, _) in group.match_indices('"') {
let tail = &group[j + 1..];
if let Some(end) = tail.find('"') {
accepted.push(tail[..end].to_string());
}
}
if key.ends_with('"') {
if let Some(end) = rest.find('"') {
accepted.push(rest[..end].to_string());
}
}
}
}
}
}
assert!(sources > 10, "only {sources} source files walked");
assert!(
accepted.len() > 50,
"only {} flags parsed, so this check would pass vacuously",
accepted.len()
);
let docs = read("docs/CLI.md");
let mut unknown: Vec<String> = Vec::new();
let mut rest = docs.as_str();
while let Some(at) = rest.find("`--") {
rest = &rest[at + 1..];
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_')
.unwrap_or(rest.len());
let spelled = rest[2..end].to_string();
if spelled.is_empty() {
continue;
}
if accepted.contains(&spelled) || NAMED_AS_ABSENT.contains(&spelled.as_str()) {
continue;
}
if !unknown.contains(&spelled) {
unknown.push(spelled);
}
}
assert!(
unknown.is_empty(),
"docs/CLI.md spells these flags and the CLI does not accept them: {unknown:?}. \
Either add the spelling as a clap `alias`, or -- if the page names it to say frink \
does NOT have it -- add it to NAMED_AS_ABSENT with the reason."
);
}
fn field_flags(source: &str) -> Vec<String> {
let mut out = Vec::new();
for line in source.lines() {
let t = line.trim();
let rest = t.strip_prefix("pub ").unwrap_or(t);
let Some(name) = rest.split(':').next() else {
continue;
};
let name = name.trim();
if name.is_empty() || !name.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
continue;
}
out.push(name.replace('_', "-"));
}
out
}
fn documents(docs: &str, flag: &str) -> bool {
let needle = format!("--{flag}");
docs.match_indices(&needle).any(|(i, _)| {
docs[i + needle.len()..]
.chars()
.next()
.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '-' && c != '_')
})
}
#[cfg(test)]
mod tests {
use super::documents;
#[test]
fn a_longer_flag_does_not_document_a_shorter_one() {
assert!(documents("| `--draft-max N` | ...", "draft-max"));
assert!(
!documents("| `--draft-max N` | ...", "draft"),
"--draft-max must not stand in for --draft"
);
assert!(
!documents("`--draft-p-minSABOTAGE`", "draft-p-min"),
"a mangled entry must not match the name it mangled"
);
assert!(documents("`--ctk` selects", "ctk"));
assert!(documents("use --jinja.", "jinja"));
}
}