xtask/tests/repository_home.rs
//! **Every crate here names this repository's real home** (A28 L17).
//!
//! The old forge is gone. A `repository =` that still points at it, or at
//! another of our own old addresses, sends a reader of crates.io or a
//! `cargo metadata` consumer to a dead or wrong place. This test walks every
//! `Cargo.toml` in the repository (detached workspaces and fixtures too) and
//! every `THIRD_PARTY_LICENSES*.md`, and fails on the first such line.
use std::path::{Path, PathBuf};
/// Where this repository lives.
const HOME: &str = "https://git.gunnar.rs/nordisk/holger";
/// The old forge's host, spelled so a search for it finds only real uses.
const OLD_FORGE: &str = concat!("code", "berg", ".org");
/// Our own hosts and organisations: a `repository =` on one of these must be [`HOME`].
const OURS: &[&str] = &[
"https://gunnar.rs/",
"https://git.gunnar.rs/",
"https://github.com/vetra-automation/",
"https://github.com/nordisk/",
"https://github.com/mikronova/",
];
/// The repository root: the first ancestor holding `.git`, else the top-most
/// ancestor that still holds a `Cargo.toml`.
fn repo_root() -> PathBuf {
let start = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut top = start.clone();
for dir in start.ancestors() {
if dir.join(".git").exists() {
return dir.to_path_buf();
}
if dir.join("Cargo.toml").is_file() {
top = dir.to_path_buf();
}
}
top
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
let Ok(meta) = std::fs::symlink_metadata(&path) else {
continue;
};
if meta.is_dir() {
if name == "target" || name == "node_modules" || name.starts_with('.') {
continue;
}
walk(&path, out);
} else if meta.is_file()
&& (name == "Cargo.toml"
|| (name.starts_with("THIRD_PARTY_LICENSES") && name.ends_with(".md")))
{
out.push(path);
}
}
}
/// The quoted value of a `repository = "…"` line, if this is one.
fn repository_value(line: &str) -> Option<&str> {
let rest = line.trim_start().strip_prefix("repository")?;
let rest = rest
.trim_start()
.strip_prefix('=')?
.trim_start()
.strip_prefix('"')?;
rest.split('"').next()
}
fn offences(path: &Path, text: &str) -> Vec<String> {
let mut found = Vec::new();
let manifest = path.file_name().is_some_and(|n| n == "Cargo.toml");
for (i, line) in text.lines().enumerate() {
let at = format!("{}:{}", path.display(), i + 1);
if line.contains(OLD_FORGE) {
found.push(format!("{at}: names the old forge"));
continue;
}
if manifest {
if let Some(v) = repository_value(line) {
if OURS.iter().any(|o| v.starts_with(o)) && v != HOME {
found.push(format!("{at}: repository is {v}, not {HOME}"));
}
}
}
}
found
}
#[test]
fn every_manifest_names_this_repositorys_real_home() {
let root = repo_root();
let mut files = Vec::new();
walk(&root, &mut files);
files.sort();
assert!(
files.iter().any(|p| p.ends_with("Cargo.toml")),
"no Cargo.toml found under {}",
root.display()
);
let mut all = Vec::new();
for f in &files {
let text = std::fs::read_to_string(f).unwrap_or_default();
all.extend(offences(f, &text));
}
assert!(
all.is_empty(),
"{} line(s) name an old home:\n{}",
all.len(),
all.join("\n")
);
}
#[test]
fn the_guard_reads_a_repository_line_and_spots_the_old_forge() {
assert_eq!(
repository_value(r#"repository = "https://x/y""#),
Some("https://x/y")
);
assert_eq!(repository_value(r#"repository = "a""#), Some("a"));
assert_eq!(repository_value("repository.workspace = true"), None);
let p = Path::new("Cargo.toml");
let old = format!("repository = \"https://{OLD_FORGE}/nordisk/x\"");
assert_eq!(offences(p, &old).len(), 1);
assert_eq!(
offences(
p,
"repository = \"https://github.com/vetra-automation/elsewhere\""
)
.len(),
1
);
assert!(offences(p, &format!("repository = \"{HOME}\"")).is_empty());
assert!(offences(p, "repository = \"https://github.com/apache/iceberg-rust\"").is_empty());
}
// ── N7-left (A28 L17): our own repositories are never addressed on the old forge ──
/// Our organisations on the old forge, in each spelling a link, a clone URL or a provenance
/// check uses. A forge KIND of that name (a mirror source a user picks) is not a home.
const OLD_HOMES: &[&str] = &[
concat!("code", "berg", ".org/nordisk"),
concat!("code", "berg", ".org:nordisk"),
concat!("code", "berg", ".org/mikronova"),
concat!("code", "berg", ".org:mikronova"),
];
/// The text files that can name a home: code, protos, manifests, configs and docs. The
/// dated notes under `.nornir/` (and every other dot directory) are history: `walk_text`
/// does not enter them.
fn is_text(name: &str) -> bool {
let ext = name.rsplit('.').next().unwrap_or("");
name != "Cargo.lock"
&& [
"rs", "proto", "toml", "yaml", "yml", "md", "json", "go", "py", "sh", "ts", "html",
"txt",
]
.contains(&ext)
}
fn walk_text(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
let Ok(meta) = std::fs::symlink_metadata(&path) else {
continue;
};
if meta.is_dir() {
if name == "target" || name == "node_modules" || name.starts_with('.') {
continue;
}
walk_text(&path, out);
} else if meta.is_file() && is_text(&name) {
out.push(path);
}
}
}
/// Container IMAGE names on the old forge's registry, not repository addresses. They stay
/// until the owner names the images' new registry (A28 N7-left, recorded in the handback).
const OLD_IMAGES: &[&str] = &[concat!("code", "berg", ".org/nordisk/holger-server")];
fn old_home_lines(path: &Path, text: &str) -> Vec<String> {
text.lines()
.enumerate()
.filter(|(_, l)| {
OLD_HOMES.iter().any(|o| l.contains(o)) && !OLD_IMAGES.iter().any(|i| l.contains(i))
})
.map(|(i, _)| {
format!(
"{}:{}: addresses one of our repositories on the old forge",
path.display(),
i + 1
)
})
.collect()
}
#[test]
fn no_file_addresses_our_repositories_on_the_old_forge() {
let root = repo_root();
let mut files = Vec::new();
walk_text(&root, &mut files);
files.sort();
let mut all = Vec::new();
for f in &files {
let text = std::fs::read_to_string(f).unwrap_or_default();
all.extend(old_home_lines(f, &text));
}
assert!(
all.is_empty(),
"{} line(s) address an old home:\n{}",
all.len(),
all.join("\n")
);
}
#[test]
fn the_guard_tells_an_old_home_from_a_forge_kind() {
let p = Path::new("x.rs");
assert_eq!(
old_home_lines(p, &format!("a link to https://{}/x", OLD_HOMES[0])).len(),
1
);
assert_eq!(
old_home_lines(p, &format!("git@{}/x.git", OLD_HOMES[3])).len(),
1
);
assert!(old_home_lines(p, "a mirror from GitHub, Codeberg or GitLab").is_empty());
assert!(
old_home_lines(p, &format!("image: {}:0.6.4", OLD_IMAGES[0])).is_empty(),
"an image name is not a home"
);
assert!(
is_text("lib.rs")
&& is_text("README.md")
&& !is_text("Cargo.lock")
&& !is_text("manual.pdf")
);
}
/// ★ **The current guides name no old forge either** (A28 L17 / A29-23): the dated notes under
/// `.nornir/` are history, but these are how-tos a reader follows today: the guide (its
/// container image is holger.rs's own, owner decision 4 of 2026-10-26), the design's links and
/// the mini-Holger walkthrough's clones. A forge KIND of that name ("a mirror from Codeberg")
/// is not a home.
#[test]
fn the_current_guides_name_no_old_forge() {
let root = repo_root();
let mut all = Vec::new();
for f in [
".nornir/guide.md",
".nornir/design.md",
".nornir/mini-holger-for-henrik.md",
".nornir/holger-evolution.md",
] {
let text = std::fs::read_to_string(root.join(f)).unwrap_or_else(|e| panic!("{f}: {e}"));
for (i, l) in text.lines().enumerate() {
let l = l.to_lowercase();
if OLD_HOMES.iter().chain(OLD_IMAGES).any(|o| l.contains(o))
|| l.contains(concat!("code", "berg", ".page"))
|| l.contains(concat!("git@", "code", "berg"))
{
all.push(format!("{f}:{}", i + 1));
}
}
}
assert!(
all.is_empty(),
"{} guide line(s) still name the old forge:\n{}",
all.len(),
all.join("\n")
);
}