Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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:?}"
    );
}