use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};
#[test]
fn markdown_links_point_to_existing_files() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let files = markdown_files(root);
let mut failures = Vec::new();
for file in &files {
let content = fs::read_to_string(file).expect("read markdown");
for link in markdown_links(&content) {
if should_skip_link(&link) {
continue;
}
let target = file
.parent()
.unwrap_or(root)
.join(link.split('#').next().unwrap_or_default());
if !target.exists() {
failures.push(format!(
"{} -> {}",
file.strip_prefix(root).unwrap().display(),
link
));
}
}
}
assert!(
failures.is_empty(),
"broken markdown links:\n{}",
failures.join("\n")
);
}
#[test]
fn docs_do_not_contain_local_paths_or_secret_patterns() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let files = markdown_files(root);
let sensitive_patterns = [
"/Users/",
"password=",
"secret=",
"token=",
"AKIA",
"BEGIN PRIVATE KEY",
];
let mut failures = Vec::new();
for file in &files {
let content = fs::read_to_string(file).expect("read markdown");
for pattern in sensitive_patterns {
if content.contains(pattern) {
failures.push(format!(
"{} contains {}",
file.strip_prefix(root).unwrap().display(),
pattern
));
}
}
}
assert!(
failures.is_empty(),
"sensitive documentation patterns:\n{}",
failures.join("\n")
);
}
fn markdown_files(root: &Path) -> Vec<PathBuf> {
let mut files = vec![root.join("README.md")];
collect_markdown(&root.join("docs"), &mut files);
files
}
fn collect_markdown(dir: &Path, files: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).expect("read docs dir") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
collect_markdown(&path, files);
} else if path.extension().is_some_and(|ext| ext == "md") {
files.push(path);
}
}
}
fn markdown_links(content: &str) -> HashSet<String> {
let mut links = HashSet::new();
let bytes = content.as_bytes();
let mut index = 0;
while index < bytes.len() {
let Some(open_text) = content[index..].find('[').map(|offset| index + offset) else {
break;
};
let Some(close_text) = content[open_text..]
.find(']')
.map(|offset| open_text + offset)
else {
break;
};
let after_text = close_text + 1;
if content[after_text..].starts_with('(')
&& let Some(close_link) = content[after_text + 1..]
.find(')')
.map(|offset| after_text + 1 + offset)
{
links.insert(content[after_text + 1..close_link].trim().to_string());
index = close_link + 1;
} else {
index = after_text;
}
}
links
}
fn should_skip_link(link: &str) -> bool {
link.is_empty()
|| link.starts_with('#')
|| link.starts_with("http://")
|| link.starts_with("https://")
|| link.starts_with("mailto:")
|| link.starts_with("rustdoc:")
}