use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;
fn options_offered(exe: &str) -> BTreeSet<String> {
let out = Command::new(exe)
.arg("--help")
.output()
.unwrap_or_else(|e| panic!("cannot run {exe}: {e}"));
let mut help = String::from_utf8_lossy(&out.stdout).to_string();
help.push_str(&String::from_utf8_lossy(&out.stderr));
long_options(&help)
}
fn long_options(text: &str) -> BTreeSet<String> {
let mut found = BTreeSet::new();
let bytes: Vec<char> = text.chars().collect();
let mut i = 0;
while i + 2 < bytes.len() {
if bytes[i] == '-' && bytes[i + 1] == '-' && bytes[i + 2].is_ascii_alphabetic() {
let mut j = i + 2;
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == '-') {
j += 1;
}
found.insert(bytes[i..j].iter().collect::<String>());
i = j;
} else {
i += 1;
}
}
found
}
fn document(name: &str) -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(name);
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()))
}
fn check(exe: &str, tool: &str, doc_name: &str, listed: &BTreeSet<String>) {
let offered = options_offered(exe);
let undocumented: Vec<&String> = offered.difference(listed).collect();
assert!(
undocumented.is_empty(),
"{tool} offers {undocumented:?}, which {doc_name} does not mention"
);
}
#[test]
fn the_readme_lists_every_option_tcslog_dump_offers() {
let doc = document("README.md");
check(
env!("CARGO_BIN_EXE_tcslog-dump"),
"tcslog-dump",
"README.md",
&long_options(&doc),
);
}
#[test]
fn the_manual_lists_every_option_tcslog_dump_offers() {
let doc = document("docs/tcslog-tools.rst");
check(
env!("CARGO_BIN_EXE_tcslog-dump"),
"tcslog-dump",
"docs/tcslog-tools.rst",
&long_options(&doc),
);
}
#[test]
fn the_readme_lists_every_option_tcslog_dumphdr_offers() {
let doc = document("README.md");
check(
env!("CARGO_BIN_EXE_tcslog-dumphdr"),
"tcslog-dumphdr",
"README.md",
&long_options(&doc),
);
}
#[test]
fn the_manual_lists_every_option_tcslog_dumphdr_offers() {
let doc = document("docs/tcslog-tools.rst");
check(
env!("CARGO_BIN_EXE_tcslog-dumphdr"),
"tcslog-dumphdr",
"docs/tcslog-tools.rst",
&long_options(&doc),
);
}
#[test]
fn neither_document_lists_an_option_no_tool_offers() {
let mut offered = options_offered(env!("CARGO_BIN_EXE_tcslog-dump"));
offered.extend(options_offered(env!("CARGO_BIN_EXE_tcslog-dumphdr")));
let cargos: BTreeSet<String> = ["--example"].iter().map(|s| (*s).to_string()).collect();
for name in ["README.md", "docs/tcslog-tools.rst"] {
let doc = document(name);
let stale: Vec<String> = long_options(&doc)
.difference(&offered)
.filter(|o| !cargos.contains(*o))
.cloned()
.collect();
assert!(
stale.is_empty(),
"{name} describes {stale:?}, which no tool offers"
);
}
}