xtask/tests/lock_current.rs
//! **RED-when-broken: every `Cargo.lock` in this repository is what cargo would
//! resolve today.**
//!
//! ## The defect this exists for
//!
//! Measured three times on the night of 2026-09-17. Every `cargo xtask reimage`
//! run came back with `deploy/upcloud/Cargo.lock` modified in the working tree —
//! `nornir-build-thing` moved `0.1.16 -> 0.1.18` with nobody asking — and the
//! NEXT re-image refused:
//!
//! ```text
//! xtask: REFUSED [gunnar-dirty] /home/rickard/git/gunnar has 1 uncommitted
//! change(s); the banner must name a commit that describes the ISO
//! ```
//!
//! That refusal is correct and must stay: a medium whose banner names a commit
//! that does not describe it is the exact confusion this repository has spent
//! whole sessions untangling. The defect was upstream of it.
//!
//! ## The cause, which is not "cargo is greedy"
//!
//! `efb6c29` took the build system to `0.1.17` across the tree. It committed the
//! root `Cargo.lock` and `installtest/Cargo.lock`. It did not commit
//! `deploy/upcloud/Cargo.lock`, because that is a DETACHED workspace — invisible
//! to `cargo metadata` at the root, so nothing that walks the crate graph could
//! notice it had been left behind.
//!
//! The result was a lock cargo could not obey: the entry said `0.1.16`, the
//! requirement beside it said `^0.1.17`, and `0.1.16` does not satisfy it. Cargo
//! was not choosing to upgrade — it had no legal lock to read, so it re-resolved
//! from scratch on every invocation and a free resolve takes the newest
//! compatible release. Reverting the file by hand restored the unsatisfiable
//! state, which is why one revert bought exactly one run and the drift came back
//! three times.
//!
//! An unsatisfiable lock is silent. A lock is only consulted; nothing shouts
//! when it is discarded. That is what makes this worth a test rather than a
//! comment: the symptom surfaced two verbs away from the cause, as a dirty-tree
//! refusal on a DIFFERENT run.
//!
//! ## Why the gate asks cargo instead of reading the file
//!
//! Every cheaper check is a re-implementation of semver resolution, and a
//! resolver written to guard a resolver agrees with it right up to the case that
//! matters. `xtask/src/vendor_patch.rs` already settled this argument here — its
//! own comment records that its file-reading arms "was green from 2026-08-23 to
//! 2026-08-25" over two locks that were fossils, and the arm that finally could
//! not be lied to was `cargo metadata --locked`. This gate is that arm, widened
//! from the two patched roots to every lockfile in the tree.
//!
//! ## The red half
//!
//! A gate over "did anything fail" goes green for two very different reasons:
//! every lock is current, or the walk found no locks at all. Both halves are
//! asserted by name:
//!
//! * [`the_walk_actually_sees_the_detached_workspaces`] — the population is
//! real and specifically contains `deploy/upcloud`, the one that drifted.
//! * [`a_lock_behind_its_manifest_is_caught`] — a workspace built on disk with
//! exactly the 2026-09-17 shape (a lock naming a version the manifest no
//! longer accepts) is REPORTED, so "all current" is a verdict and not a
//! silence. It uses a `path` dependency, so it proves the mechanism without
//! the network.
use std::path::{Path, PathBuf};
use xtask::one_clone::lockfiles;
/// `CARGO_MANIFEST_DIR` is `<root>/xtask`.
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent")
.to_path_buf()
}
fn cargo() -> String {
std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())
}
/// Lockfiles under `xtask/tests/fixtures/` are TEST DATA, not workspaces this
/// repository builds: `cve_vulnerable` names a knowingly vulnerable release and
/// `gix_porcelain_in_server` resolves `gix-features` from the registry on
/// purpose. Holding them to "what cargo would resolve today" would make them
/// stop being fixtures.
const FIXTURES: &str = "xtask/tests/fixtures/";
/// Every workspace root in this repository that owns a `Cargo.lock`, relative to
/// the repository root, `""` for the root workspace itself.
fn locked_roots(root: &Path) -> Vec<String> {
lockfiles(root)
.into_iter()
.filter(|rel| !rel.starts_with(FIXTURES))
.map(|rel| {
rel.trim_end_matches("Cargo.lock")
.trim_end_matches('/')
.to_string()
})
.collect()
}
/// **THE GATE.** `cargo metadata --locked`, in every workspace root that has a
/// lock.
///
/// `--locked` makes cargo refuse to write the lock rather than silently
/// updating it, so a non-zero exit here means precisely one thing: this lock is
/// not what cargo would resolve, and the next command run in that directory
/// will rewrite it.
#[test]
fn every_lock_in_the_tree_is_what_cargo_would_resolve() {
let root = root();
let cargo = cargo();
let mut stale: Vec<String> = Vec::new();
for rel in locked_roots(&root) {
let dir = if rel.is_empty() {
root.clone()
} else {
root.join(&rel)
};
let where_ = if rel.is_empty() {
"the repository root"
} else {
&rel
};
let out = std::process::Command::new(&cargo)
.current_dir(&dir)
.args(["metadata", "--format-version", "1", "--locked"])
.output()
.unwrap_or_else(|e| panic!("{where_}: running `{cargo} metadata`: {e}"));
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
let first = err
.lines()
.find(|l| l.trim_start().starts_with("error"))
.unwrap_or_else(|| err.lines().next().unwrap_or("(no stderr)"));
stale.push(format!(" {where_}/Cargo.lock\n {}", first.trim()));
}
}
assert!(
stale.is_empty(),
"these Cargo.lock files are NOT what cargo would resolve today:\n{}\n\n\
Each one is rewritten by the next cargo command that touches its \
workspace, and for `deploy/upcloud` that command is `cargo xtask \
reimage` — which then leaves the tree dirty and makes the NEXT \
re-image refuse with `[gunnar-dirty]`. Reverting the file restores the \
same stale lock and buys exactly one run.\n\n\
Fix: refresh the lock deliberately and commit it WITH the manifest \
change that moved it — `cargo update -p <crate>@<old> --precise <new>` \
in that directory for a single pin, and diff the package list before \
and after so a refresh cannot smuggle in a new third-party crate. A \
detached workspace does not follow the root's `cargo update`; it has to \
be named.",
stale.join("\n")
);
}
/// ★ g-reds 1, G-B7: **every lock records its own workspace's patches as they are declared now.**
///
/// cargo writes each patch a workspace did not use into its lock as `[[patch.unused]]`. Until
/// G-B7 the estate's sibling table reached every workspace in the tree through the root's
/// `.cargo/config.toml`, fixtures included; it is gone, and each estate workspace names the
/// sibling patches it takes in its own manifest. A lock that still records a patch its manifest
/// does not declare (b4c335c2 `plugin-door-wire`, or the old include's 26) is rewritten by the
/// next build in its workspace: [`every_lock_in_the_tree_is_what_cargo_would_resolve`] cannot see
/// it (`--locked` accepts it), so the lists are compared here.
#[test]
fn every_lock_records_its_own_patches_as_they_are() {
let root = root();
let none = std::collections::BTreeSet::new();
let mut drift = Vec::new();
for rel in lockfiles(&root) {
let lock = std::fs::read_to_string(root.join(&rel)).expect("a lock");
let manifest = root
.join(rel.trim_end_matches("Cargo.lock"))
.join("Cargo.toml");
let own =
xtask::siblings::patch_names(&std::fs::read_to_string(&manifest).unwrap_or_default());
for d in xtask::siblings::lock_drift(&lock, &none, &own) {
drift.push(format!(" {rel} {d}"));
}
}
assert!(
drift.is_empty(),
"these locks record patches their own manifest does not declare, so the next build in their \
workspace rewrites them:\n{}\n\nFix: run `cargo metadata` in that workspace and commit the lock \
with the manifest's change.",
drift.join("\n")
);
}
/// **The population is real**, and contains the workspace that drifted.
///
/// Without this, a walk that skipped `target/` a little too eagerly — or one
/// that could not read the tree at all — would satisfy the gate above forever by
/// checking nothing.
#[test]
fn the_walk_actually_sees_the_detached_workspaces() {
let roots = locked_roots(&root());
for want in ["", "deploy/upcloud", "installtest", "bench"] {
assert!(
roots.iter().any(|r| r == want),
"the lockfile walk did not find `{}`, so the gate is checking a \
population that is missing the very workspaces detachment hides. \
Found: {roots:?}",
if want.is_empty() {
"the repository root"
} else {
want
}
);
}
assert!(
roots.len() >= 6,
"only {} locked roots found; the detached workspaces are where the \
stragglers hide and they are not optional. Found: {roots:?}",
roots.len()
);
}
/// **The gate fires on the real shape.** A workspace whose lock names a version
/// its manifest no longer accepts.
///
/// Built on disk out of two `path` crates so the proof needs no registry and no
/// network: the leaf is `0.2.0`, the lock says the root resolved `0.1.0`, and
/// `^0.2` cannot be satisfied by `0.1.0`. That is the 2026-09-17 shape exactly —
/// requirement moved, lock left behind — and cargo must answer by refusing to
/// proceed rather than by rewriting the file.
#[test]
fn a_lock_behind_its_manifest_is_caught() {
let dir = std::env::temp_dir().join(format!("gunnar-lock-current-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let leaf = dir.join("leaf");
let subject = dir.join("subject");
std::fs::create_dir_all(leaf.join("src")).expect("temp leaf");
std::fs::create_dir_all(subject.join("src")).expect("temp subject");
std::fs::write(
leaf.join("Cargo.toml"),
"[package]\nname = \"lock-current-leaf\"\nversion = \"0.2.0\"\nedition = \"2021\"\n\
publish = false\n\n[workspace]\n",
)
.expect("leaf manifest");
std::fs::write(leaf.join("src/lib.rs"), "").expect("leaf lib");
std::fs::write(
subject.join("Cargo.toml"),
"[package]\nname = \"lock-current-subject\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\
publish = false\n\n[workspace]\n\n[dependencies]\n\
lock-current-leaf = { path = \"../leaf\", version = \"0.2\" }\n",
)
.expect("subject manifest");
std::fs::write(subject.join("src/lib.rs"), "").expect("subject lib");
// The lock a commit like efb6c29 leaves behind: the requirement moved to
// `0.2`, this file still records the `0.1.0` resolve.
std::fs::write(
subject.join("Cargo.lock"),
"# This file is automatically @generated by Cargo.\n\
# It is not intended for manual editing.\nversion = 4\n\n\
[[package]]\nname = \"lock-current-leaf\"\nversion = \"0.1.0\"\n\n\
[[package]]\nname = \"lock-current-subject\"\nversion = \"0.0.0\"\n\
dependencies = [\n \"lock-current-leaf\",\n]\n",
)
.expect("stale lock");
let cargo = cargo();
let out = std::process::Command::new(&cargo)
.current_dir(&subject)
.args(["metadata", "--format-version", "1", "--locked", "--offline"])
.output()
.expect("running cargo metadata on the fixture");
let status = out.status;
let err = String::from_utf8_lossy(&out.stderr).into_owned();
let _ = std::fs::remove_dir_all(&dir);
assert!(
!status.success(),
"`cargo metadata --locked` ACCEPTED a lock naming `lock-current-leaf \
0.1.0` under a `^0.2` requirement. If cargo ever stops treating an \
unsatisfiable lock as an error, `--locked` in \
`xtask/src/reinstall.rs` stops protecting the re-image and the gate \
above is measuring nothing.\n{err}"
);
assert!(
err.contains("--locked") || err.contains("lock file"),
"cargo refused, but not for the lock: the fixture is proving something \
other than what it claims.\n{err}"
);
}