Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

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");
}