Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! Integration tests against the real fixture corpus.
Matt W2//!
Matt W3//! The unit tests in `change_id.rs` run against byte literals transcribed from
Matt W4//! real commits, which proves the parser handles those shapes but not that the
Matt W5//! shapes are still what `jj` produces. These tests read the actual bare
Matt W6//! repositories built by `fixtures/gen.sh`, so a jj format change fails here.
Matt W7//!
Matt W8//! Objects are read via `git cat-file`, not a Git library, because `df-index`
Matt W9//! must not depend on one (spec §3 rule 1).
Matt W10//!
Matt W11//! Skipped with a warning if the corpus has not been generated, so a plain
Matt W12//! `cargo test` on a fresh checkout does not fail confusingly. CI runs
Matt W13//! `fixtures/gen.sh` first, and `corpus_is_present` fails when `DF_REQUIRE_FIXTURES`
Matt W14//! is set so a CI misconfiguration cannot silently skip all of this.
Matt W15
Matt W16use std::collections::HashSet;
Matt W17use std::path::{Path, PathBuf};
Matt W18use std::process::Command;
Matt W19
Matt W20use df_index::{extract_change_id, extract_conflict_trees};
Matt W21
Matt W22fn corpus_root() -> PathBuf {
Matt W23 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/repos")
Matt W24}
Matt W25
Matt W26fn repo(name: &str) -> Option<PathBuf> {
Matt W27 let p = corpus_root().join(format!("{name}.git"));
Matt W28 p.is_dir().then_some(p)
Matt W29}
Matt W30
Matt W31/// Run a git command against a bare repo, returning stdout as raw bytes.
Matt W32fn git(repo: &Path, args: &[&str]) -> Vec<u8> {
Matt W33 let out = Command::new("git")
Matt W34 .arg("--git-dir")
Matt W35 .arg(repo)
Matt W36 .args(args)
Matt W37 .output()
Matt W38 .expect("git must be on PATH to run corpus tests");
Matt W39 assert!(
Matt W40 out.status.success(),
Matt W41 "git {args:?} failed in {}: {}",
Matt W42 repo.display(),
Matt W43 String::from_utf8_lossy(&out.stderr)
Matt W44 );
Matt W45 out.stdout
Matt W46}
Matt W47
Matt W48/// Every commit object in the repo, as raw bytes.
Matt W49fn all_commits(repo: &Path) -> Vec<(String, Vec<u8>)> {
Matt W50 let listing = git(repo, &["cat-file", "--batch-all-objects", "--batch-check=%(objectname) %(objecttype)"]);
Matt W51 String::from_utf8_lossy(&listing)
Matt W52 .lines()
Matt W53 .filter_map(|l| {
Matt W54 let (oid, kind) = l.split_once(' ')?;
Matt W55 (kind == "commit").then(|| oid.to_owned())
Matt W56 })
Matt W57 .map(|oid| {
Matt W58 let raw = git(repo, &["cat-file", "commit", &oid]);
Matt W59 (oid, raw)
Matt W60 })
Matt W61 .collect()
Matt W62}
Matt W63
Matt W64#[test]
Matt W65fn corpus_is_present() {
Matt W66 if repo("basic").is_none() {
Matt W67 let required = std::env::var_os("DF_REQUIRE_FIXTURES").is_some();
Matt W68 assert!(
Matt W69 !required,
Matt W70 "DF_REQUIRE_FIXTURES is set but the corpus is missing — run fixtures/gen.sh"
Matt W71 );
Matt W72 eprintln!("warning: fixture corpus absent, corpus tests skipped; run fixtures/gen.sh");
Matt W73 }
Matt W74}
Matt W75
Matt W76#[test]
Matt W77fn every_jj_commit_yields_a_change_id() {
Matt W78 let Some(r) = repo("basic") else { return };
Matt W79 let commits = all_commits(&r);
Matt W80 assert!(!commits.is_empty(), "basic.git must contain commits");
Matt W81 for (oid, raw) in commits {
Matt W82 assert!(
Matt W83 extract_change_id(&raw).is_some(),
Matt W84 "jj-authored commit {oid} in basic.git yielded no change id"
Matt W85 );
Matt W86 }
Matt W87}
Matt W88
Matt W89#[test]
Matt W90fn no_plain_git_commit_yields_a_change_id() {
Matt W91 let Some(r) = repo("plain-git") else { return };
Matt W92 let commits = all_commits(&r);
Matt W93 assert!(!commits.is_empty(), "plain-git.git must contain commits");
Matt W94 for (oid, raw) in commits {
Matt W95 assert_eq!(
Matt W96 extract_change_id(&raw),
Matt W97 None,
Matt W98 "plain-git commit {oid} must not yield a change id"
Matt W99 );
Matt W100 }
Matt W101}
Matt W102
Matt W103/// The property the entire product rests on (spec §4): rewriting a change
Matt W104/// produces a new commit but preserves the change id.
Matt W105#[test]
Matt W106fn change_id_is_stable_across_five_rewrites() {
Matt W107 let Some(r) = repo("rewritten") else { return };
Matt W108 let commits = all_commits(&r);
Matt W109
Matt W110 assert_eq!(
Matt W111 commits.len(),
Matt W112 5,
Matt W113 "rewritten.git should retain all five revisions as objects"
Matt W114 );
Matt W115
Matt W116 let ids: HashSet<String> = commits
Matt W117 .iter()
Matt W118 .map(|(oid, raw)| {
Matt W119 extract_change_id(raw)
Matt W120 .unwrap_or_else(|| panic!("commit {oid} has no change id"))
Matt W121 .as_str()
Matt W122 .to_owned()
Matt W123 })
Matt W124 .collect();
Matt W125
Matt W126 assert_eq!(
Matt W127 ids.len(),
Matt W128 1,
Matt W129 "all five revisions must share one change id, got {ids:?}"
Matt W130 );
Matt W131
Matt W132 let oids: HashSet<&String> = commits.iter().map(|(o, _)| o).collect();
Matt W133 assert_eq!(oids.len(), 5, "the five revisions must be distinct commits");
Matt W134}
Matt W135
Matt W136#[test]
Matt W137fn merge_commits_carry_a_change_id() {
Matt W138 let Some(r) = repo("merge") else { return };
Matt W139 let merges: Vec<_> = all_commits(&r)
Matt W140 .into_iter()
Matt W141 .filter(|(_, raw)| {
Matt W142 // Count `parent ` headers in the header block.
Matt W143 raw.split(|&b| b == b'\n')
Matt W144 .take_while(|l| !l.is_empty())
Matt W145 .filter(|l| l.starts_with(b"parent "))
Matt W146 .count()
Matt W147 > 1
Matt W148 })
Matt W149 .collect();
Matt W150
Matt W151 assert!(!merges.is_empty(), "merge.git must contain a merge commit");
Matt W152 for (oid, raw) in merges {
Matt W153 assert!(
Matt W154 extract_change_id(&raw).is_some(),
Matt W155 "merge commit {oid} yielded no change id"
Matt W156 );
Matt W157 }
Matt W158}
Matt W159
Matt W160/// The parser hazard: `change-id` sits next to a multi-line `gpgsig` whose
Matt W161/// base64 body could otherwise be misread as headers.
Matt W162#[test]
Matt W163fn signed_commits_yield_a_change_id() {
Matt W164 let Some(r) = repo("signed") else { return };
Matt W165 let signed: Vec<_> = all_commits(&r)
Matt W166 .into_iter()
Matt W167 .filter(|(_, raw)| {
Matt W168 raw.split(|&b| b == b'\n')
Matt W169 .take_while(|l| !l.is_empty())
Matt W170 .any(|l| l.starts_with(b"gpgsig "))
Matt W171 })
Matt W172 .collect();
Matt W173
Matt W174 assert!(!signed.is_empty(), "signed.git must contain a signed commit");
Matt W175 for (oid, raw) in signed {
Matt W176 assert!(
Matt W177 extract_change_id(&raw).is_some(),
Matt W178 "signed commit {oid} yielded no change id"
Matt W179 );
Matt W180 }
Matt W181}
Matt W182
Matt W183/// Conflicted commits put `change-id` *after* the multi-line
Matt W184/// `jj:conflict-labels` header — the layout a naive parser gets wrong.
Matt W185#[test]
Matt W186fn conflicted_commits_yield_both_change_id_and_conflict_trees() {
Matt W187 let Some(r) = repo("conflict") else { return };
Matt W188 let conflicted: Vec<_> = all_commits(&r)
Matt W189 .into_iter()
Matt W190 .filter(|(_, raw)| extract_conflict_trees(raw).is_some())
Matt W191 .collect();
Matt W192
Matt W193 assert!(
Matt W194 !conflicted.is_empty(),
Matt W195 "conflict.git must contain a conflicted commit"
Matt W196 );
Matt W197
Matt W198 for (oid, raw) in conflicted {
Matt W199 assert!(
Matt W200 extract_change_id(&raw).is_some(),
Matt W201 "conflicted commit {oid} yielded no change id despite preceding jj: headers"
Matt W202 );
Matt W203 let c = extract_conflict_trees(&raw).unwrap();
Matt W204 assert_eq!(
Matt W205 c.sides.len(),
Matt W206 c.bases.len() + 1,
Matt W207 "conflict {oid} violates the sides == bases + 1 invariant"
Matt W208 );
Matt W209 // Every referenced tree must actually exist in the repo.
Matt W210 for t in c.sides.iter().chain(c.bases.iter()) {
Matt W211 let kind = git(&r, &["cat-file", "-t", t]);
Matt W212 assert_eq!(
Matt W213 String::from_utf8_lossy(&kind).trim(),
Matt W214 "tree",
Matt W215 "conflict side/base {t} is not a tree"
Matt W216 );
Matt W217 }
Matt W218 }
Matt W219}
Matt W220
Matt W221/// Unconflicted commits must not be misdetected as conflicted — otherwise every
Matt W222/// change in the product renders a conflict banner.
Matt W223#[test]
Matt W224fn unconflicted_repos_report_no_conflicts() {
Matt W225 for name in ["basic", "stack", "merge", "signed", "plain-git"] {
Matt W226 let Some(r) = repo(name) else { continue };
Matt W227 for (oid, raw) in all_commits(&r) {
Matt W228 assert_eq!(
Matt W229 extract_conflict_trees(&raw),
Matt W230 None,
Matt W231 "commit {oid} in {name}.git falsely reports a conflict"
Matt W232 );
Matt W233 }
Matt W234 }
Matt W235}
Matt W236
Matt W237/// A repo migrated to jj: the indexer must produce real ids for the jj commits
Matt W238/// and fall back to synthetic identity for the plain-git ones, in one history.
Matt W239#[test]
Matt W240fn mixed_history_splits_into_real_and_synthetic() {
Matt W241 let Some(r) = repo("mixed") else { return };
Matt W242 let commits = all_commits(&r);
Matt W243 let with = commits.iter().filter(|(_, c)| extract_change_id(c).is_some()).count();
Matt W244 let without = commits.len() - with;
Matt W245
Matt W246 assert!(with > 0, "mixed.git must contain at least one jj commit");
Matt W247 assert!(
Matt W248 without > 0,
Matt W249 "mixed.git must contain at least one plain-git commit"
Matt W250 );
Matt W251}
Matt W252
Matt W253/// Change ids read from the corpus must all be well-formed under our own
Matt W254/// validation — this is what catches an alphabet or width change in jj.
Matt W255#[test]
Matt W256fn all_corpus_change_ids_are_well_formed() {
Matt W257 let mut seen = 0usize;
Matt W258 for name in ["basic", "rewritten", "stack", "merge", "conflict", "signed", "mixed"] {
Matt W259 let Some(r) = repo(name) else { continue };
Matt W260 for (_, raw) in all_commits(&r) {
Matt W261 if let Some(id) = extract_change_id(&raw) {
Matt W262 let s = id.as_str();
Matt W263 assert_eq!(s.len(), 32, "change id {s} has unexpected width");
Matt W264 assert!(
Matt W265 s.bytes().all(|b| (b'k'..=b'z').contains(&b)),
Matt W266 "change id {s} leaves the reverse-hex alphabet"
Matt W267 );
Matt W268 seen += 1;
Matt W269 }
Matt W270 }
Matt W271 }
Matt W272 assert!(seen > 0, "corpus yielded no change ids at all");
Matt W273}

273 lines · Rust