tests/the_root_lock_is_the_crates_io_lock.rs
//! **The root lock is the crates.io lock** (HHUNT-2, 2026-10-28): a lone clone of holger-ui
//! (no sibling beside it, no `.cargo/siblings.toml`) builds with `--locked`.
//!
//! `.cargo/config.toml` includes the uncommitted `siblings.toml` when it is there, so a lock
//! written in an estate checkout can name a crate from a sibling PATH: such an entry has no
//! `source` line, and a lone clone's `cargo build --locked` then refuses ("cannot update the
//! lock file"). Measured on main 3eda458: nordisk-estate-svg 0.1.0, nornir-git 0.2.2 and
//! nornir-xtask-support 0.1.5 were path entries (the A34-38 merges). The only entries with
//! no source are this repository's own crates.
//!
//! And the ROOT takes no sibling patch (holger's B7, the owner's publish of 2026-10-26): the
//! CI runner and `lockcheck::siblings_on` write `.cargo/siblings.toml` in an estate checkout,
//! so a root that included it would read a different graph there than in a lone clone, and
//! one committed lock could not be `--locked` in both. The estate's patches belong to the
//! detached `browser-tests/` workspace, which carries them in its own manifest.
use std::path::Path;
/// The package names of every manifest in this repository (not `target/`, not the detached
/// `browser-tests/` workspace, which has a lock of its own).
fn own_crates(dir: &Path, out: &mut Vec<String>) {
for e in std::fs::read_dir(dir).unwrap().flatten() {
let p = e.path();
let name = e.file_name().to_string_lossy().to_string();
if p.is_dir() {
if !matches!(name.as_str(), "target" | "browser-tests" | ".git" | "node_modules" | "dist") {
own_crates(&p, out);
}
} else if name == "Cargo.toml" {
let text = std::fs::read_to_string(&p).unwrap();
let mut in_package = false;
for line in text.lines() {
let l = line.trim();
if l.starts_with('[') {
in_package = l == "[package]";
} else if in_package {
if let Some(v) = l.strip_prefix("name").map(str::trim).and_then(|r| r.strip_prefix('=')) {
out.push(v.trim().trim_matches('"').to_string());
break;
}
}
}
}
}
}
/// Pure: the `(name, version)` of every lock entry with no `source` line.
fn sourceless(lock: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
for block in lock.split("[[package]]").skip(1) {
let field = |k: &str| block.lines().find_map(|l| l.strip_prefix(&format!("{k} = ")).map(|v| v.trim_matches('"').to_string()));
if field("source").is_none() {
out.push((field("name").unwrap_or_default(), field("version").unwrap_or_default()));
}
}
out
}
#[test]
fn every_lock_entry_but_this_repositorys_own_crates_comes_from_a_registry() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut own = Vec::new();
own_crates(root, &mut own);
assert!(own.iter().any(|n| n == "holger-ui-web") && own.iter().any(|n| n == "holger-ui-server"), "{own:?}");
let lock = std::fs::read_to_string(root.join("Cargo.lock")).unwrap();
let strays: Vec<String> = sourceless(&lock).into_iter().filter(|(n, _)| !own.contains(n)).map(|(n, v)| format!("{n} {v}")).collect();
assert!(strays.is_empty(), "the root lock names sibling PATH crates a lone clone cannot resolve with --locked (relock with no .cargo/siblings.toml): {strays:?}");
}
#[test]
fn a_path_entry_is_told_from_a_registry_entry() {
let lock = "[[package]]\nname = \"a\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n\n[[package]]\nname = \"b\"\nversion = \"0.2.2\"\ndependencies = [\n \"a\",\n]\n";
assert_eq!(sourceless(lock), vec![("b".to_string(), "0.2.2".to_string())]);
}
#[test]
fn the_root_takes_no_sibling_patch() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let config = std::fs::read_to_string(root.join(".cargo/config.toml")).unwrap();
assert!(
!config.lines().map(str::trim).any(|l| l.starts_with("include") && l.contains("siblings")),
".cargo/config.toml must not include the sibling patches: the root builds from crates.io in every checkout"
);
let ignore = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert!(ignore.lines().any(|l| l.trim() == "/.cargo/siblings.toml"), ".cargo/siblings.toml stays ignored");
}