use std::collections::BTreeSet;
use std::path::Path;
fn declared_names(cargo_toml: &str, section: &str) -> BTreeSet<String> {
let mut names = BTreeSet::new();
let mut in_section = false;
for line in cargo_toml.lines() {
let trimmed = line.trim();
if trimmed.starts_with("[[") {
in_section = trimmed == section;
continue;
}
if in_section {
if let Some((key, value)) = trimmed.split_once('=') {
if key.trim() == "name" {
names.insert(value.trim().trim_matches('"').to_owned());
}
}
}
}
names
}
fn rs_stems(dir: &Path) -> BTreeSet<String> {
let mut stems = BTreeSet::new();
for entry in std::fs::read_dir(dir)
.expect("read target directory")
.flatten()
{
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
stems.insert(stem.to_owned());
}
}
}
stems
}
fn drift(files: &BTreeSet<String>, declared: &BTreeSet<String>) -> (Vec<String>, Vec<String>) {
let unregistered = files.difference(declared).cloned().collect();
let orphan = declared.difference(files).cloned().collect();
(unregistered, orphan)
}
fn cargo_toml() -> String {
std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"))
.expect("read Cargo.toml")
}
#[test]
fn every_test_file_is_registered() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let (unregistered, orphan) = drift(
&rs_stems(&root.join("tests")),
&declared_names(&cargo_toml(), "[[test]]"),
);
assert!(
unregistered.is_empty() && orphan.is_empty(),
"tests/ registration drift:\n \
unregistered files (add a [[test]] entry with required-features): {unregistered:?}\n \
orphan [[test]] entries (no matching tests/*.rs file): {orphan:?}"
);
}
#[test]
fn every_example_file_is_registered() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let (unregistered, orphan) = drift(
&rs_stems(&root.join("examples")),
&declared_names(&cargo_toml(), "[[example]]"),
);
assert!(
unregistered.is_empty() && orphan.is_empty(),
"examples/ registration drift:\n \
unregistered files (add an [[example]] entry with required-features): {unregistered:?}\n \
orphan [[example]] entries (no matching examples/*.rs file): {orphan:?}"
);
}
#[test]
fn drift_detects_both_directions() {
let files: BTreeSet<String> = ["a", "b", "new_unregistered"]
.iter()
.copied()
.map(String::from)
.collect();
let declared: BTreeSet<String> = ["a", "b", "orphan_entry"]
.iter()
.copied()
.map(String::from)
.collect();
let (unregistered, orphan) = drift(&files, &declared);
assert_eq!(unregistered, vec![String::from("new_unregistered")]);
assert_eq!(orphan, vec![String::from("orphan_entry")]);
}