xtask/tests/lone_clone.rs
//! **A lone clone of gunnar loads, and `cargo build --release` gives the server**
//! (owner order 2026-10-22).
//!
//! Somebody who clones gunnar ALONE, with no `../edda`, `../Skidbladnir`,
//! `../monetize`, `../znippy`, `../tunnr` or `../gunnar-ui` beside it, must be able
//! to run plain `cargo build --release` at the root and get `target/release/gunnar`.
//! cargo refuses to load a workspace when ANY member's dependency, or any `[patch]`
//! entry, points at a path that is not there, optional and dev edges included. So:
//!
//! * no root-workspace member may have a path dependency that leaves the repo;
//! * the root `[patch]` tables may only point inside the repo;
//! * the root takes no sibling patch at all, not through `.cargo/config.toml` either, and its
//! committed lock is the crates.io lock (G-B7, owner order G-BOTH-1); the estate workspaces
//! (gunnar-front, the guest, the benches, installtest) carry the sibling patches they take
//! in their own manifests;
//! * `default-members` is the `gunnar` crate alone.
//!
//! The end-to-end proof (copy only this repo into an empty directory, `cargo build
//! --release`, boot, `/healthz`) is `xtask/lone-clone-check.sh`; it needs crates.io (and,
//! until the publish script has run, the packaged crates it is pointed at).
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use xtask::json::Json;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent")
.to_path_buf()
}
/// Lexical normalisation (no filesystem): what cargo itself does with a declared path.
fn normalise(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
/// Pure: the `path = "…"` values under every `[patch.*]` table of a manifest's text.
fn patch_paths(manifest: &str) -> Vec<String> {
let mut out = Vec::new();
let mut in_patch = false;
for line in manifest.lines().map(str::trim) {
if line.starts_with('#') || line.is_empty() {
continue;
}
if line.starts_with('[') {
in_patch = line.starts_with("[patch.") || line.starts_with("[patch]");
continue;
}
if in_patch {
if let Some(rest) = line.split("path").nth(1) {
if let Some(v) = rest.split('"').nth(1) {
out.push(v.to_string());
}
}
}
}
out
}
#[test]
fn patch_paths_reads_every_entry() {
let m = "[workspace]\nmembers=[]\n[patch.crates-io]\n# x = { path = \"../no\" }\na = { path = \"../edda/a\" }\nb = { path = \"vendor/b\" }\n[profile.release]\nc = { path = \"../not-a-patch\" }\n";
assert_eq!(patch_paths(m), vec!["../edda/a", "vendor/b"]);
}
#[test]
fn the_root_patch_table_stays_inside_the_repo() {
let root = root();
let text = std::fs::read_to_string(root.join("Cargo.toml")).expect("root Cargo.toml");
let outside: Vec<String> = patch_paths(&text)
.into_iter()
.filter(|p| !normalise(&root.join(p)).starts_with(&root))
.collect();
assert!(
outside.is_empty(),
"the root [patch] points out of the repo at {outside:?}: a lone clone cannot load the \
workspace. Sibling patches belong in the estate workspaces' own manifests \
(xtask::siblings::ESTATE_WORKSPACES), never at the root"
);
}
#[test]
fn no_member_of_the_root_workspace_reaches_a_sibling_checkout() {
let root = root();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let out = Command::new(cargo)
.args(["metadata", "--no-deps", "--format-version", "1"])
.current_dir(&root)
.output()
.expect("run cargo metadata");
assert!(
out.status.success(),
"cargo metadata: {}",
String::from_utf8_lossy(&out.stderr)
);
let json = Json::parse(&String::from_utf8_lossy(&out.stdout)).expect("json");
let mut outside = Vec::new();
for package in json.get("packages").expect("packages").arr() {
let name = package.get("name").and_then(Json::str).unwrap_or("?");
for dep in package.get("dependencies").map(Json::arr).unwrap_or(&[]) {
if let Some(path) = dep.get("path").and_then(Json::str) {
if !normalise(Path::new(path)).starts_with(&root) {
let dep_name = dep.get("name").and_then(Json::str).unwrap_or("?");
outside.push(format!("{name} -> {dep_name} ({path})"));
}
}
}
}
assert!(
outside.is_empty(),
"root-workspace members depend on sibling checkouts by path, so a lone clone cannot \
load the workspace:\n {}",
outside.join("\n ")
);
}
#[test]
fn the_default_member_is_the_gunnar_server_alone() {
let text = std::fs::read_to_string(root().join("Cargo.toml")).expect("root Cargo.toml");
assert!(
text.lines()
.any(|l| l.trim() == "default-members = [\"crates/gunnar\"]"),
"the root workspace must say `default-members = [\"crates/gunnar\"]`, so a plain \
`cargo build --release` builds the server and nothing else"
);
}
/// ★ G-B7 (owner order G-BOTH-1): **the root takes NO sibling patch.** Not in its manifest and
/// not through `.cargo/config.toml` either: a config is found from the directory cargo runs in,
/// so an included sibling table rewrote the ROOT's resolution in every estate checkout, and the
/// committed root lock was the estate's (29 sibling crates by path) instead of the one a lone
/// clone resolves. The estate workspaces carry the sibling patches they take in their OWN
/// manifests ([`xtask::siblings::ESTATE_WORKSPACES`]).
#[test]
fn the_root_cargo_config_includes_no_sibling_table() {
let root = root();
let config =
std::fs::read_to_string(root.join(".cargo/config.toml")).expect(".cargo/config.toml");
let live: Vec<&str> = config
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#'))
.collect();
assert!(
!live
.iter()
.any(|l| l.starts_with("include") || l.contains("siblings.toml")),
".cargo/config.toml still includes a sibling table: every cargo run from the repository \
root (the root workspace's included) builds against the sibling checkouts, so its lock is \
not the lock a lone clone resolves"
);
for gone in [".cargo/estate-siblings.toml", ".cargo/estate-siblings.sh"] {
assert!(
!root.join(gone).exists(),
"{gone} is still here: the estate's sibling patches live in the estate workspaces' own \
manifests now, and lockbot switches a repository's patches on when it finds this file"
);
}
let ignore = std::fs::read_to_string(root.join(".gitignore")).expect(".gitignore");
assert!(
ignore.lines().any(|l| l.trim() == "/.cargo/siblings.toml"),
"a stale .cargo/siblings.toml from an older checkout must stay ignored"
);
}
/// The root workspace's member names and the crates its own `[patch]` supplies from inside the
/// repository (`vendor/gix-parallel`): the path packages a crates.io lock may hold.
fn in_repo_packages(root: &Path) -> std::collections::BTreeSet<String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut own = xtask::both_ways::member_names(root, &cargo);
assert!(
own.contains("gunnar"),
"cargo metadata --no-deps read no members: {own:?}"
);
let text = std::fs::read_to_string(root.join("Cargo.toml")).expect("root Cargo.toml");
own.extend(xtask::siblings::patch_names(&text));
own
}
/// ★ G-B7: **the committed root lock is the crates.io lock.** Every package in it is a member,
/// an in-repo patch, or comes from a registry: a lone clone's `cargo build --locked` takes it
/// as it is. RED on master e95cc71a: 29 sibling crates by path (appliance-pid1 0.1.8,
/// disk-snapshot 0.1.1, draupnir 0.2.0, edda-admit 0.1.0, …).
#[test]
fn the_committed_root_lock_names_no_sibling_by_path() {
let root = root();
let lock = std::fs::read_to_string(root.join("Cargo.lock")).expect("Cargo.lock");
let bad = xtask::both_ways::lock_path_entries(&lock, &in_repo_packages(&root));
assert!(
bad.is_empty(),
"the committed root Cargo.lock names {} crate(s) by path that are neither members nor an \
in-repo patch, so a lone clone's `cargo build --locked` refuses it: {bad:?}. Relock the \
root with no sibling patch (`cargo metadata` at the root) and commit that lock.",
bad.len()
);
}
/// ★ G-B7: **each estate workspace patches the siblings its own lock takes by path**, in its own
/// manifest: with the root include gone, nothing else turns them on, and a lock that names a
/// sibling crate by path its manifest does not patch is refused by `--locked`.
#[test]
fn each_estate_workspace_patches_the_siblings_its_lock_takes() {
let root = root();
let estate: std::collections::BTreeSet<String> = xtask::siblings::ESTATE_PATCHES
.iter()
.map(|s| s.to_string())
.collect();
let mut missing = Vec::new();
for ws in xtask::siblings::ESTATE_WORKSPACES {
let manifest = std::fs::read_to_string(root.join(ws).join("Cargo.toml"))
.unwrap_or_else(|e| panic!("{ws}/Cargo.toml: {e}"));
let own = xtask::siblings::patch_names(&manifest);
let lock = std::fs::read_to_string(root.join(ws).join("Cargo.lock"))
.unwrap_or_else(|e| panic!("{ws}/Cargo.lock: {e}"));
for entry in xtask::both_ways::lock_path_entries(&lock, &Default::default()) {
let name = entry.split(' ').next().unwrap_or("").to_string();
if estate.contains(&name) && !own.contains(&name) {
missing.push(format!("{ws}: {entry}"));
}
}
}
assert!(
missing.is_empty(),
"these estate workspaces' locks take a sibling crate by path that their own manifest does \
not patch (it came from the root's include before G-B7):\n {}",
missing.join("\n ")
);
}
#[test]
fn the_readme_documents_the_lone_clone_build() {
let readme = std::fs::read_to_string(root().join("README.md")).expect("README.md");
for needle in ["cargo build --release", "target/release/gunnar"] {
assert!(
readme.contains(needle),
"README.md does not document `{needle}`"
);
}
}