use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use super::run;
use crate::speculate_queue::{enqueue, queue};
fn git(repo: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(repo)
.args(["-c", "user.name=t", "-c", "user.email=t@t"])
.args(args)
.output()
.unwrap();
assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr));
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
struct Fx {
_tmp: TempDir,
root: PathBuf,
scratch: PathBuf,
territory: PathBuf,
gate: PathBuf,
log: PathBuf,
}
fn fx(gate_exit: &str) -> Fx {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("repo");
fs::create_dir_all(&root).unwrap();
git(&root, &["init", "-q", "-b", "main"]);
fs::write(root.join("shared"), "line\n").unwrap();
git(&root, &["add", "-A"]);
git(&root, &["commit", "-q", "-m", "base"]);
let base = git(&root, &["rev-parse", "HEAD"]);
for (branch, file) in [("work/a", "fa"), ("work/b", "fb")] {
git(&root, &["checkout", "-q", "-b", branch, &base]);
fs::write(root.join(file), branch).unwrap();
git(&root, &["add", "-A"]);
git(&root, &["commit", "-q", "-m", branch]);
}
git(&root, &["checkout", "-q", "-b", "work/hostile", &base]);
fs::write(root.join("shared"), "hostile\n").unwrap();
git(&root, &["add", "-A"]);
git(&root, &["commit", "-q", "-m", "hostile"]);
git(&root, &["checkout", "-q", "main"]);
fs::write(root.join("shared"), "moved\n").unwrap();
git(&root, &["add", "-A"]);
git(&root, &["commit", "-q", "-m", "moved"]);
let f = Fx {
scratch: tmp.path().join("scratch"),
territory: tmp.path().join("territory"),
gate: tmp.path().join("gate.sh"),
log: tmp.path().join("gate.log"),
_tmp: tmp,
root,
};
set_gate(&f, gate_exit);
f
}
fn set_gate(f: &Fx, code: &str) {
fs::write(&f.gate, format!("#!/bin/sh\npwd >> {}\nexit {code}\n", f.log.display())).unwrap();
fs::set_permissions(&f.gate, fs::Permissions::from_mode(0o755)).unwrap();
}
fn verdicts(f: &Fx) -> usize {
fs::read_dir(f.territory.join("verdicts")).map_or(0, Iterator::count)
}
fn pass(f: &Fx, builds: usize) -> Vec<String> {
run(
&f.root,
&f.scratch,
&f.territory,
"rustc test",
"main",
f.gate.to_str().unwrap(),
builds,
)
.unwrap()
}
fn gate_runs(f: &Fx) -> usize {
fs::read_to_string(&f.log).map_or(0, |s| s.lines().count())
}
#[test]
fn builds_head_first_then_hits_and_the_second_pass_spends_nothing() {
let f = fx("0");
enqueue(&f.root, "a", Some("2026-01-01T10:00:00Z")).unwrap();
enqueue(&f.root, "b", Some("2026-01-01T11:00:00Z")).unwrap();
let report = pass(&f, 10);
assert_eq!(report.len(), 3, "{report:?}");
assert!(report[0].starts_with("built a ") && report[0].ends_with(" pass"), "{report:?}");
assert!(report[1].starts_with("built b "), "{report:?}");
assert!(report[2].starts_with("adopted hostile "), "the stray tip is adopted: {report:?}");
assert_eq!(gate_runs(&f), 2);
let again = pass(&f, 10);
assert!(again[0].starts_with("hit a ") && again[1].starts_with("hit b "), "{again:?}");
assert_eq!(gate_runs(&f), 2, "hits spend no gates");
let listing = git(&f.root, &["worktree", "list", "--porcelain"]);
assert_eq!(listing.matches("worktree ").count(), 1, "no build worktrees survive");
}
#[test]
fn budget_defers_and_a_failing_gate_stops_the_chain() {
let f = fx("1");
enqueue(&f.root, "a", Some("2026-01-01T10:00:00Z")).unwrap();
enqueue(&f.root, "b", Some("2026-01-01T11:00:00Z")).unwrap();
let report = pass(&f, 0);
assert_eq!(report[0], "deferred a (builds spent)", "builds=0 is plan-only: {report:?}");
assert!(report[1].starts_with("adopted hostile "), "{report:?}");
let report = pass(&f, 10);
assert!(report[0].starts_with("built a ") && report[0].contains("FAIL"), "{report:?}");
assert_eq!(report.len(), 1, "a failing prefix ends the pass, nothing left to adopt: {report:?}");
let report = pass(&f, 10);
assert!(report[0].starts_with("fail a "), "the recorded FAIL stops later passes too");
assert_eq!(gate_runs(&f), 1, "the fail was built once, never again");
}
#[test]
fn a_tempfail_gate_records_nothing_and_the_next_pass_rebuilds() {
let f = fx("75");
enqueue(&f.root, "a", Some("2026-01-01T10:00:00Z")).unwrap();
enqueue(&f.root, "b", Some("2026-01-01T11:00:00Z")).unwrap();
let report = pass(&f, 10);
assert!(report[0].starts_with("no verdict a "), "{report:?}");
assert!(report[0].ends_with("(gate exit 75) — unbuilt, pass ends"), "{report:?}");
assert!(report[1].starts_with("adopted hostile "), "nothing builds past the unbuilt: {report:?}");
assert_eq!(report.len(), 2, "{report:?}");
assert_eq!(gate_runs(&f), 1);
assert_eq!(verdicts(&f), 0, "a no-verdict writes no verdict file");
set_gate(&f, "0");
let report = pass(&f, 10);
assert!(report[0].starts_with("built a ") && report[0].ends_with(" pass"), "{report:?}");
assert!(report[1].starts_with("built b "), "the chain resumes past the rebuilt head: {report:?}");
assert_eq!(gate_runs(&f), 3, "the unbuilt head was rebuilt, not remembered");
assert_eq!(verdicts(&f), 2);
}
#[test]
fn conflicts_stop_the_chain_and_the_unsealed_are_swept() {
let f = fx("0");
enqueue(&f.root, "hostile", Some("2026-01-01T10:00:00Z")).unwrap();
enqueue(&f.root, "a", Some("2026-01-01T11:00:00Z")).unwrap();
let report = pass(&f, 10);
assert!(report[0].starts_with("conflict hostile"), "{report:?}");
assert!(report[1].starts_with("adopted b "), "nothing BUILDS past a conflict: {report:?}");
assert_eq!(report.len(), 2, "{report:?}");
git(&f.root, &["branch", "-D", "work/hostile"]);
let report = pass(&f, 10);
assert_eq!(report[0], "swept hostile (unsealed)", "{report:?}");
assert!(report[1].starts_with("built a "), "the queue heals past the swept: {report:?}");
assert!(report[2].starts_with("built b "), "the adopted entry builds in turn: {report:?}");
assert_eq!(queue(&f.root).unwrap().len(), 2, "the stale tag is gone");
}
#[test]
fn adoption_is_the_paved_path_and_is_idempotent() {
let f = fx("0");
let report = pass(&f, 10);
assert_eq!(report.len(), 3, "every stray tip is adopted, none built yet: {report:?}");
for (line, id) in report.iter().zip(["a", "b", "hostile"]) {
let tip = git(&f.root, &["rev-parse", &format!("refs/heads/work/{id}")]);
assert_eq!(line, &format!("adopted {id} {tip}"), "{report:?}");
}
assert_eq!(gate_runs(&f), 0, "a fresh seal must survive one full pass before it builds");
let report = pass(&f, 10);
assert!(report[0].starts_with("built a ") && report[1].starts_with("built b "), "{report:?}");
assert!(report[2].starts_with("conflict hostile"), "{report:?}");
assert_eq!(report.len(), 3, "standing seals are not re-adopted: {report:?}");
}
#[test]
fn a_moved_tip_requeues_at_the_bottom_via_sweep_plus_adopt() {
let f = fx("0");
enqueue(&f.root, "a", Some("2026-01-01T10:00:00Z")).unwrap();
enqueue(&f.root, "b", Some("2026-01-01T11:00:00Z")).unwrap();
git(&f.root, &["checkout", "-q", "work/a"]);
fs::write(f.root.join("fa"), "moved").unwrap();
git(&f.root, &["add", "-A"]);
git(&f.root, &["commit", "-q", "-m", "moved"]);
git(&f.root, &["checkout", "-q", "main"]);
let report = pass(&f, 10);
assert_eq!(report[0], "swept a (unsealed)", "{report:?}");
assert!(report[1].starts_with("built b "), "{report:?}");
assert!(report[2].starts_with("adopted a "), "the moved tip re-seals at the bottom: {report:?}");
let report = pass(&f, 10);
assert!(report[0].starts_with("hit b ") && report[1].starts_with("built a "), "{report:?}");
}
#[test]
fn a_missing_landing_branch_is_loud() {
let f = fx("0");
let err = run(&f.root, &f.scratch, &f.territory, "rustc test", "ghost", "true", 1).unwrap_err();
assert!(err.to_string().contains("rev-parse ghost"), "{err}");
}