Files
branchgraph/tests/gate.rs
Jean-Gabriel Gill-Couture e03a92c570 Remove dead code, merge duplicated logic, match docs to the code
- Drop fields and functions nothing reads: job and proc tips, fetch time,
  PR url/author (and their GraphQL fields), spine summaries, diff::commit,
  unread data attributes, unused CSS and tokens.
- One home each for state wording (naming main as the repo does), lane
  lookup, unseen, outdated, main's tip, the stop wait and the token header.
- Narrow visibility; rename render::View to Filter; queue ranking moves
  to the CLI, its only user.
- Token test covers every route; shared checkout snapshot in the fixture.
- Client: errors outside Run/Send are toasts, history only after a good
  load, a drag stays in one file, the agent marker is a ring.
- design.md, the skill and AGENTS.md describe what is built.
2026-10-09 12:24:24 -04:00

289 lines
9.0 KiB
Rust

mod fixture;
use branchgraph::git;
use fixture::{Fixture, git as run, merge_tree_conflicts};
#[test]
fn every_lane_matches_the_git_cli() {
let f = Fixture::new();
let c = f.clone.as_path();
let repo = git::open(c).unwrap();
let graph = git::build(&repo).unwrap();
assert_eq!(graph.main, "main");
assert_eq!(
graph.main_tip.to_string(),
run(c, &["rev-parse", "origin/main"])
);
let names: Vec<&str> = graph.lanes.iter().map(|l| l.name.as_str()).collect();
assert_eq!(
names,
[
"agent/from-main",
"agent/x",
"deps/lock",
"feat/conflict",
"feat/long",
"feat/merged",
"feat/old",
"feat/rename",
"feat/synced",
"feat/unpushed"
]
);
for lane in &graph.lanes {
let tip = lane.tip.to_string();
let n = &lane.name;
assert_eq!(
lane.fork.to_string(),
run(c, &["merge-base", "origin/main", &tip]),
"{n} fork"
);
let counts = run(
c,
&[
"rev-list",
"--left-right",
"--count",
&format!("origin/main...{tip}"),
],
);
let (behind, ahead) = counts.split_once('\t').unwrap();
assert_eq!(lane.behind.to_string(), behind, "{n} behind");
assert_eq!(lane.ahead.to_string(), ahead, "{n} ahead");
let expected = run(
c,
&[
"rev-list",
"-n",
&git::SHOWN.to_string(),
&format!("origin/main..{tip}"),
],
);
let mut expected: Vec<&str> = expected.lines().collect();
expected.reverse();
let got: Vec<String> = lane.commits.iter().map(|c| c.id.to_string()).collect();
assert_eq!(got, expected, "{n} lane commits");
let conflicts = git::conflicts(&repo, graph.main_tip, lane.tip).unwrap();
assert_eq!(
conflicts,
merge_tree_conflicts(c, "origin/main", &tip),
"{n} conflicts"
);
}
}
/// `git diff --numstat -z -M base tip` as (path, renamed from, added, removed).
fn numstat(c: &std::path::Path, base: &str, tip: &str) -> Vec<(String, Option<String>, u32, u32)> {
let out = run(c, &["diff", "--numstat", "-z", "-M", base, tip]);
let mut fields = out.split('\0').filter(|f| !f.is_empty());
let mut rows = Vec::new();
while let Some(head) = fields.next() {
let mut parts = head.splitn(3, '\t');
let added = parts.next().unwrap().parse().unwrap();
let removed = parts.next().unwrap().parse().unwrap();
let rows_path = match parts.next().unwrap() {
"" => {
let from = fields.next().unwrap().to_owned();
(fields.next().unwrap().to_owned(), Some(from))
}
path => (path.to_owned(), None),
};
rows.push((rows_path.0, rows_path.1, added, removed));
}
rows.sort();
rows
}
#[test]
fn branch_stat_matches_git_numstat() {
let f = Fixture::new();
let c = f.clone.as_path();
let repo = git::open(c).unwrap();
let graph = git::build(&repo).unwrap();
for lane in &graph.lanes {
let stat = branchgraph::diff::stat(&repo, lane.fork, lane.tip).unwrap();
let mut got: Vec<_> = stat
.files
.iter()
.map(|f| {
let (a, r) = f.stat.unwrap();
(f.path.clone(), f.from.clone(), a, r)
})
.collect();
got.sort();
assert_eq!(
got,
numstat(c, &lane.fork.to_string(), &lane.tip.to_string()),
"{} stat",
lane.name
);
}
}
#[test]
fn lanes_carry_upstream_worktree_and_conflict_facts() {
let f = Fixture::new();
let repo = git::open(&f.clone).unwrap();
let graph = git::build(&repo).unwrap();
let lane = |n: &str| graph.lane(n).unwrap();
let agent = lane("agent/x");
assert_eq!((agent.remote, agent.unpushed), (None, 2));
assert!(agent.worktree.as_ref().unwrap().path.ends_with("wt-agent"));
let from_main = lane("agent/from-main");
assert_eq!(
(from_main.remote, from_main.unpushed),
(None, 1),
"tracking main is no upstream"
);
let unpushed = lane("feat/unpushed");
assert_eq!((unpushed.unpushed, unpushed.to_pull), (1, 0));
let long = lane("feat/long");
assert_eq!(
(long.commits.len(), long.folded()),
(git::SHOWN, 40 - git::SHOWN)
);
assert!(lane("feat/merged").is_merged());
let main_wt = graph
.lanes
.iter()
.filter_map(|l| l.worktree.as_ref())
.find(|w| w.primary);
assert!(
main_wt.is_none(),
"main is checked out in the clone, and main has no lane"
);
let lock = git::conflicts(&repo, graph.main_tip, lane("deps/lock").tip).unwrap();
assert!(git::lock_only(&lock), "{lock:?}");
let real = git::conflicts(&repo, graph.main_tip, lane("feat/conflict").tip).unwrap();
assert!(!git::lock_only(&real));
assert_eq!(
graph.unrelated,
["gh-pages"],
"an orphan branch is listed, not drawn"
);
// The spine holds the fork points and the merge, oldest first, ending at the tip.
assert_eq!(graph.spine.last().unwrap().id, graph.main_tip);
assert!(graph.spine.iter().any(|s| s.merge));
for l in graph.lanes.iter().filter(|l| !l.is_merged()) {
assert!(
graph.spine.iter().any(|s| s.id == l.fork),
"{} fork on spine",
l.name
);
}
}
#[test]
fn conflict_check_writes_nothing_to_the_clone() {
let f = Fixture::new();
let objects = |p: &std::path::Path| run(p, &["count-objects", "-v"]);
let before = objects(&f.clone);
let repo = git::open(&f.clone).unwrap();
let graph = git::build(&repo).unwrap();
for l in &graph.lanes {
git::conflicts(&repo, graph.main_tip, l.tip).unwrap();
}
assert_eq!(objects(&f.clone), before);
}
#[test]
fn dirty_counts_match_git_status_and_write_nothing() {
let f = Fixture::new();
let c = f.clone.as_path();
assert_eq!(git::dirty(c).unwrap(), 0);
std::fs::write(c.join("app.txt"), "changed\n").unwrap();
std::fs::write(c.join("new.txt"), "untracked\n").unwrap();
std::fs::write(c.join("staged.txt"), "staged\n").unwrap();
run(c, &["add", "staged.txt"]);
let index = std::fs::read(c.join(".git/index")).unwrap();
let porcelain = run(c, &["status", "--porcelain", "--untracked-files=all"]);
assert_eq!(git::dirty(c).unwrap(), porcelain.lines().count());
assert_eq!(std::fs::read(c.join(".git/index")).unwrap(), index);
}
#[test]
fn rail_graph_draws_every_branch_attached_to_main_with_parents_below() {
let f = Fixture::new();
let repo = git::open(&f.clone).unwrap();
let graph = git::build(&repo).unwrap();
// All lanes, merged ones too, as "show merged" draws them.
let lanes: Vec<&git::Lane> = graph.lanes.iter().collect();
let rows = branchgraph::rail::layout(&repo, &graph, &lanes).unwrap();
let at: std::collections::HashMap<_, _> = rows
.iter()
.enumerate()
.map(|(i, r)| (r.commit.id, i))
.collect();
let main = &rows[at[&graph.main_tip]];
assert_eq!(
(main.col, main.labels.first().map(String::as_str)),
(0, Some("main"))
);
for (i, r) in rows.iter().enumerate() {
for p in &r.parents {
assert!(at[p] > i, "{} drawn above its parent", r.commit.summary);
}
if let Some(next) = rows.get(i + 1) {
assert_eq!(
r.after,
next.before,
"columns continue from row {i} to {}",
i + 1
);
}
assert_eq!(
r.after.get(r.col).copied().flatten(),
r.parents.first().copied(),
"first parent continues the column"
);
}
for lane in &lanes {
let tip = &rows[at[&lane.tip]];
assert!(
tip.labels.contains(&lane.name),
"{} labelled at its tip",
lane.name
);
// Following first parents from the tip reaches a main row: no lane floats.
let mut r = tip;
let mut steps = 0;
while !r.lanes.is_empty() {
let p = r
.parents
.first()
.unwrap_or_else(|| panic!("{} ends without reaching main", lane.name));
r = &rows[at[p]];
steps += 1;
assert!(steps < 100);
}
}
// Two lanes fork at the same commit; both columns end there.
let old = lanes.iter().find(|l| l.name == "feat/old").unwrap();
let fork_row = &rows[at[&old.fork]];
assert!(
fork_row
.before
.iter()
.filter(|c| **c == Some(old.fork))
.count()
>= 2,
"both lanes wait for their shared fork point"
);
let long = lanes.iter().find(|l| l.name == "feat/long").unwrap();
assert_eq!(rows[at[&long.tip]].lanes, ["feat/long"]);
}