crates/gunnar-pack/tests/deltified.rs
//! **`write_pack_deltified`, judged by stock `git`.**
//!
//! The third writing path (`crate::delta`), and the one that closes the defect
//! measured on 2026-08-03: `gunnar-client` pushed 407.08 MiB into a server at
//! **0.0% deltified** against stock git's 88.14 MiB at 74.0%, over 19 real
//! repositories, because `send-pack` called [`gunnar_pack::write_pack`] — which
//! emits no deltas at all.
//!
//! # Correctness is the first assertion here, not the last
//!
//! A smaller pack is worth nothing if one delta rebuilds one byte wrong: the
//! failure surfaces as a corrupt repository at an arbitrarily later clone. So
//! every pack in this file goes through **`git index-pack --strict`**, which
//! reconstructs and re-hashes every object in it, and every object's bytes are
//! then read back with **`git cat-file`** and compared to what went in.
//! `git fsck --full --strict` runs over the result. Nothing here trusts a gunnar
//! reader about a gunnar writer.
//!
//! # And the acceptance is proved capable of refusing
//!
//! "`git index-pack --strict` accepted it" is exactly the sentence that would
//! also be true of a pack with no deltas in it, and of a harness that never ran
//! git at all. Two controls sit against that:
//!
//! * [`the_whole_object_writer_is_what_zero_deltas_looks_like`] runs the same
//! object set through [`gunnar_pack::write_pack`] and requires **0** deltas
//! and a much bigger pack — so the delta counter is not reporting a constant
//! and `entry_kinds` is not blind;
//! * [`git_index_pack_rejects_a_corrupted_delta`] flips one byte inside a delta
//! payload and requires git to **refuse** — so the acceptance above is a
//! verdict rather than a formality.
use std::path::Path;
use gix_hash::{Kind, ObjectId};
use gunnar_pack::{write_pack, write_pack_deltified, DeltaOptions, WriteOptions};
mod common;
use common::{entry_kinds, git, git_available, git_raw, MemObjects};
/// A connected history in memory: a chain of commits over a file that grows and
/// is rewritten, plus a per-revision sibling file.
///
/// **Connected**, so `index-pack --strict` and `fsck --full --strict` can run
/// their reachability check rather than being handed a bag of loose blobs, and
/// **redundant**, so there is something to delta. A corpus of unique random
/// content deltas to nothing, and would make a working encoder
/// indistinguishable from a dead one.
fn history(hash_kind: Kind) -> (MemObjects, Vec<ObjectId>) {
let mut store = MemObjects::new(hash_kind);
let mut ids = Vec::new();
let mut parent: Option<ObjectId> = None;
let mut body: String = (0..600)
.map(|i| format!("line {i} of the corpus, with enough text on it to matter\n"))
.collect();
for rev in 0..30 {
body.push_str(&format!("revision {rev} appended this line\n"));
let blob = store.insert_blob(body.clone().into_bytes());
let sibling = store.insert_blob(format!("{body}sibling {rev}\n").into_bytes());
ids.push(blob);
ids.push(sibling);
let tree = store.insert_tree(vec![
gix_object::tree::Entry {
mode: gix_object::tree::EntryKind::Blob.into(),
filename: "corpus.txt".into(),
oid: blob,
},
gix_object::tree::Entry {
mode: gix_object::tree::EntryKind::Blob.into(),
filename: format!("sib{rev}.txt").into(),
oid: sibling,
},
]);
ids.push(tree);
let parents: Vec<ObjectId> = parent.into_iter().collect();
let commit = store.insert_commit(tree, &parents, &format!("revision {rev}"));
ids.push(commit);
parent = Some(commit);
}
// Every id, deduplicated by the writer itself. The tip commit is last, which
// is what `fsck` is pointed at.
(store, ids)
}
/// Install a pack into a fresh bare repository of the right hash kind with
/// **`git index-pack --strict`**, and read back what git says is inside:
/// `(repository, pack bytes, bases, ofs deltas, ref deltas)`.
///
/// `--strict` is what turns "the bytes parsed" into "every object resolved and
/// re-hashed to the id it claims". The repository is where the pack lands, so
/// `cat-file` and `fsck` can be pointed at it afterwards — which is exactly what
/// `receive-pack` does with a pushed pack.
///
/// **The repository is initialised with the pack's own `--object-format`**, and
/// that is not a detail. `git index-pack` run outside a repository assumes
/// SHA-1, so a SHA-256 pack handed to it decodes as garbage. The first version
/// of this helper did exactly that and failed with *"too-short tree object"* —
/// which reads as a bug in the writer, not in the harness, and would have been
/// chased there. Had the loop run SHA-256 first, the SHA-1 case would have been
/// the one that "passed".
fn index_and_inspect(
pack_bytes: &[u8],
hash_kind: Kind,
dir: &Path,
name: &str,
) -> (std::path::PathBuf, u64, usize, usize, usize) {
let repo = dir.join(format!("{name}.git"));
std::fs::create_dir_all(&repo).expect("mkdir");
let fmt = match hash_kind {
Kind::Sha256 => "--object-format=sha256",
_ => "--object-format=sha1",
};
git(&["init", "-q", "--bare", fmt, "."], &repo);
let pack = repo.join("objects/pack/in.pack");
std::fs::write(&pack, pack_bytes).expect("saving the pack");
git(
&["index-pack", "--strict", "-v", "objects/pack/in.pack"],
&repo,
);
let idx = pack.with_extension("idx");
assert!(
idx.is_file(),
"git index-pack wrote no .idx next to {pack:?}"
);
let (base, ofs, refs) = entry_kinds(&idx, &pack, &repo);
(repo, pack_bytes.len() as u64, base, ofs, refs)
}
/// Every object git can now see, read back and compared byte-for-byte with what
/// the store held. Returns how many were checked.
fn every_object_reads_back(repo: &Path, store: &MemObjects, ids: &[ObjectId]) -> usize {
let mut checked = 0;
for id in ids {
let (kind, want) = store.raw(id).expect("fixture object");
let hex = id.to_string();
let kind_name = std::str::from_utf8(kind.as_bytes()).expect("ascii kind");
let got = git_raw(&["cat-file", kind_name, &hex], repo);
assert!(
got.status.success(),
"git cat-file could not read {hex} back out of the pack: {}",
String::from_utf8_lossy(&got.stderr).trim()
);
assert_eq!(
got.stdout.as_slice(),
want.as_slice(),
"{hex} came back with different bytes than went in"
);
checked += 1;
}
checked
}
/// The headline, on both hash kinds.
#[test]
fn deltas_are_computed_and_stock_git_accepts_and_reconstructs_them() {
assert!(
git_available(),
"this test requires `git` on PATH; a pack format claim cannot be proven without it"
);
for hash_kind in [Kind::Sha1, Kind::Sha256] {
let (store, ids) = history(hash_kind);
let dir = tempfile::tempdir().expect("tempdir");
let mut bytes = Vec::new();
let outcome = write_pack_deltified(
&ids,
&store,
&mut bytes,
&WriteOptions::new(hash_kind),
&DeltaOptions::default(),
)
.expect("writing the deltified pack");
// ── the writer's own applied output ─────────────────────────────────
assert!(
outcome.objects_deltified > 0,
"{hash_kind:?}: {} objects went in and not one delta came out",
outcome.num_objects
);
assert_eq!(
outcome.objects_deltified + outcome.objects_recompressed,
outcome.num_objects as usize,
"{hash_kind:?}: every entry is either a whole object or a computed delta"
);
assert_eq!(
outcome.objects_copied_from_pack, 0,
"nothing was copied here"
);
// ── and stock git's, over the same artefact ─────────────────────────
let (repo, size, base, ofs, refs) =
index_and_inspect(&bytes, hash_kind, dir.path(), "deltified");
assert_eq!(
ofs, outcome.objects_deltified,
"{hash_kind:?}: the writer counted {} deltas, git found {ofs} ofs-deltas",
outcome.objects_deltified
);
assert_eq!(refs, 0, "{hash_kind:?}: ofs_delta is on, so no ref-deltas");
assert_eq!(
base + ofs,
outcome.num_objects as usize,
"{hash_kind:?}: git and the writer disagree about how many entries there are"
);
// ── correctness, which comes first even when it is printed last ─────
let checked = every_object_reads_back(&repo, &store, &ids);
assert_eq!(
checked,
ids.len(),
"{hash_kind:?}: only {checked} of {} objects were read back",
ids.len()
);
let tip = ids.last().expect("a tip commit");
git(&["update-ref", "refs/heads/main", &tip.to_string()], &repo);
git(&["fsck", "--full", "--strict", "--no-progress"], &repo);
println!(
"{hash_kind:?}: {ofs}/{} deltified, {size} pack bytes, {checked} objects verified",
outcome.num_objects
);
}
}
/// **The negative control for the metric.** Same objects, the writer that was
/// being called, which must come back at zero deltas and a much bigger pack.
///
/// Without this, `objects_deltified` could be a field nothing ever sets to zero
/// and `entry_kinds` could be counting every entry as a delta, and both tests
/// would still be green.
#[test]
fn the_whole_object_writer_is_what_zero_deltas_looks_like() {
assert!(git_available(), "this test requires `git` on PATH");
let hash_kind = Kind::Sha1;
let (store, ids) = history(hash_kind);
let dir = tempfile::tempdir().expect("tempdir");
let options = WriteOptions::new(hash_kind);
let mut whole = Vec::new();
let old = write_pack(&ids, &store, &mut whole, &options).expect("write_pack");
assert_eq!(
old.objects_deltified, 0,
"`write_pack` has no encoder; a non-zero count means the field is being filled \
in by something other than the code that computed a delta"
);
let mut small = Vec::new();
let new = write_pack_deltified(&ids, &store, &mut small, &options, &DeltaOptions::default())
.expect("write_pack_deltified");
assert_eq!(
old.num_objects, new.num_objects,
"the two writers must carry the same object set for their sizes to compare"
);
let (_, _, old_base, old_ofs, old_ref) =
index_and_inspect(&whole, hash_kind, dir.path(), "whole");
let (_, _, _, new_ofs, _) = index_and_inspect(&small, hash_kind, dir.path(), "small");
assert_eq!(
(old_ofs, old_ref),
(0, 0),
"stock git found {old_ofs} ofs-deltas and {old_ref} ref-deltas in a pack written \
by a writer that has no encoder. Either it grew one, or `entry_kinds` is \
counting something that is not a delta — and then every number here is worthless"
);
assert_eq!(old_base, old.num_objects as usize);
assert!(new_ofs > 0, "the deltified pack must actually hold deltas");
let ratio = whole.len() as f64 / small.len() as f64;
assert!(
ratio > 2.0,
"the deltified pack is {} bytes against the whole-object pack's {} — only \
{ratio:.2}x, which is not the shape of a working encoder on a redundant corpus",
small.len(),
whole.len()
);
println!(
"whole {} B / deltified {} B = {ratio:.2}x smaller",
whole.len(),
small.len()
);
}
/// **The negative control for the acceptance.** One byte flipped inside a delta
/// payload, and `git index-pack --strict` must refuse.
///
/// Every "git accepted it" above is worth exactly what this is worth.
#[test]
fn git_index_pack_rejects_a_corrupted_delta() {
assert!(git_available(), "this test requires `git` on PATH");
let hash_kind = Kind::Sha1;
let (store, ids) = history(hash_kind);
let dir = tempfile::tempdir().expect("tempdir");
let mut bytes = Vec::new();
let outcome = write_pack_deltified(
&ids,
&store,
&mut bytes,
&WriteOptions::new(hash_kind),
&DeltaOptions::default(),
)
.expect("writing the deltified pack");
assert!(outcome.objects_deltified > 0, "nothing to corrupt");
// Find a delta's offset the way `entry_kinds` does — through git — so the
// corruption lands inside a delta and not in some header this test guessed
// at. Index the good pack first; that also proves it was good.
let good = dir.path().join("good.pack");
std::fs::write(&good, &bytes).expect("save");
git(
&["index-pack", "--strict", &good.to_string_lossy()],
dir.path(),
);
let listing = git(
&[
"verify-pack",
"-v",
&good.with_extension("idx").to_string_lossy(),
],
dir.path(),
);
let offset: usize = listing
.lines()
.filter_map(|l| {
let f: Vec<&str> = l.split_whitespace().collect();
(f.len() >= 7 && f[0].bytes().all(|b| b.is_ascii_hexdigit())).then(|| f[4].to_owned())
})
.next()
.expect("git named at least one delta entry")
.parse()
.expect("a pack offset");
// Well past the entry header, inside its deflate stream. Not the trailer.
let at = offset + 8;
assert!(
at < bytes.len() - 40,
"the pack is too small to corrupt safely"
);
bytes[at] ^= 0xff;
let bad = dir.path().join("bad.pack");
std::fs::write(&bad, &bytes).expect("save");
let out = git_raw(
&["index-pack", "--strict", &bad.to_string_lossy()],
dir.path(),
);
assert!(
!out.status.success(),
"git index-pack --strict ACCEPTED a pack with a corrupted delta at byte {at}. \
Every acceptance in this file is therefore a formality, not a verdict.\nstdout: \
{}",
String::from_utf8_lossy(&out.stdout)
);
}
/// The memory ceiling. A budget small enough to force many batches must still
/// produce one valid pack, and must still find deltas.
///
/// This is the path a real push takes on a repository bigger than the budget,
/// and it is the one place the batching arithmetic — offsets recorded per batch,
/// bases always earlier in the same batch — can be wrong in a way that only
/// shows up as a corrupt pack.
#[test]
fn a_small_memory_budget_batches_and_the_pack_is_still_correct() {
assert!(git_available(), "this test requires `git` on PATH");
let hash_kind = Kind::Sha1;
let (store, ids) = history(hash_kind);
let dir = tempfile::tempdir().expect("tempdir");
let one_batch = DeltaOptions::default();
let many = DeltaOptions {
// ~64 KiB against a corpus of roughly 40 KiB per blob: several batches,
// and small enough that some hold a single object.
memory_budget: 64 * 1024,
..DeltaOptions::default()
};
let mut big = Vec::new();
let a = write_pack_deltified(
&ids,
&store,
&mut big,
&WriteOptions::new(hash_kind),
&one_batch,
)
.expect("one batch");
let mut chopped = Vec::new();
let b = write_pack_deltified(
&ids,
&store,
&mut chopped,
&WriteOptions::new(hash_kind),
&many,
)
.expect("many batches");
assert_eq!(a.num_objects, b.num_objects, "both packs hold every object");
assert!(
b.objects_deltified > 0,
"batching found no deltas at all, which is what a broken batch boundary would \
also look like"
);
// The batched pack is allowed to be worse — the window cannot see across a
// seam — but it must not be worse than the whole-object writer.
assert!(
b.objects_deltified <= a.objects_deltified,
"batching found MORE deltas ({}) than a single window ({}), which cannot happen \
if the batches are contiguous runs of the same size order",
b.objects_deltified,
a.objects_deltified
);
let (repo, _, _, ofs, refs) = index_and_inspect(&chopped, hash_kind, dir.path(), "batched");
assert_eq!(every_object_reads_back(&repo, &store, &ids), ids.len());
let tip = ids.last().expect("tip");
git(&["update-ref", "refs/heads/main", &tip.to_string()], &repo);
git(&["fsck", "--full", "--strict", "--no-progress"], &repo);
assert_eq!(refs, 0);
assert_eq!(ofs, b.objects_deltified);
println!(
"one batch: {}/{} deltified, {} B; many batches: {}/{} deltified, {} B",
a.objects_deltified,
a.num_objects,
big.len(),
b.objects_deltified,
b.num_objects,
chopped.len()
);
}
/// A reader without the `ofs-delta` capability still gets the compression, named
/// by object id instead of by distance — the same rule
/// [`gunnar_pack::write_pack_copy`] follows, and `tests/ofs_delta_off.rs`
/// proves for the copy path.
#[test]
fn without_ofs_delta_the_deltas_are_named_by_base_id() {
assert!(git_available(), "this test requires `git` on PATH");
let hash_kind = Kind::Sha1;
let (store, ids) = history(hash_kind);
let dir = tempfile::tempdir().expect("tempdir");
let mut bytes = Vec::new();
let outcome = write_pack_deltified(
&ids,
&store,
&mut bytes,
&WriteOptions::new(hash_kind).ofs_delta(false),
&DeltaOptions::default(),
)
.expect("writing a ref-delta pack");
let (repo, _, _, ofs, refs) = index_and_inspect(&bytes, hash_kind, dir.path(), "refdelta");
assert_eq!(
ofs, 0,
"a reader without the capability was sent {ofs} OBJ_OFS_DELTA entries it cannot \
decode"
);
assert_eq!(
refs, outcome.objects_deltified,
"the writer counted {} deltas and git found {refs} ref-deltas",
outcome.objects_deltified
);
assert!(refs > 0, "the compression must survive the rename");
assert_eq!(every_object_reads_back(&repo, &store, &ids), ids.len());
}
/// An empty request is still a legal, empty pack — and reports no deltas rather
/// than failing on a window over nothing.
#[test]
fn an_empty_pack_round_trips() {
let store = MemObjects::new(Kind::Sha1);
let mut bytes = Vec::new();
let outcome = write_pack_deltified(
&[],
&store,
&mut bytes,
&WriteOptions::new(Kind::Sha1),
&DeltaOptions::default(),
)
.expect("an empty deltified pack");
assert_eq!(outcome.num_objects, 0);
assert_eq!(outcome.objects_deltified, 0);
assert_eq!(bytes, gunnar_pack::empty_pack(Kind::Sha1).unwrap());
}
/// A missing object fails the whole pack rather than shortening it — the same
/// contract [`gunnar_pack::write_pack`] has, and it must be caught in the
/// **header** pass, before any content is read.
#[test]
fn a_missing_object_is_refused() {
let (store, mut ids) = history(Kind::Sha1);
ids.push(ObjectId::from_hex(b"0123456789012345678901234567890123456789").unwrap());
let mut bytes = Vec::new();
let err = write_pack_deltified(
&ids,
&store,
&mut bytes,
&WriteOptions::new(Kind::Sha1),
&DeltaOptions::default(),
)
.expect_err("a pack naming an absent object must not be written");
assert!(
matches!(err, gunnar_pack::Error::ObjectMissing { .. }),
"expected ObjectMissing, got {err:?}"
);
}