crates/gunnar-store/tests/conformance.rs
//! The conformance suite, driven from **outside** the crate through nothing but
//! the public API.
//!
//! The suite also runs as a unit test, which is where the deliberately broken
//! backends live. This file exists for a different reason: `znippy-plugin-git`
//! and `storage-git-gix` consume the suite exactly like this — from another
//! repository, over the public surface — and if that surface stops being enough
//! to run it, that breaks here first.
use std::path::Path;
use std::sync::Arc;
use gunnar_store::conformance::{self, Harness, HASH_KINDS};
use gunnar_store::git_storage_trait::{Caps, GitOps, GitServe};
use gunnar_store::RepoStore;
fn run<H: Harness>(harness: &H) -> usize {
let outcomes = conformance::run_all(harness);
let expected = conformance::check_names::<H>().len() * HASH_KINDS.len();
assert_eq!(
outcomes.len(),
expected,
"[{}] the runner produced {} outcomes for {expected} expected check/hash-kind pairs",
harness.name(),
outcomes.len()
);
// Both hash kinds must genuinely have run: sha256 green is never inferred
// from sha1 green.
for hash_kind in HASH_KINDS {
let ran = outcomes.iter().filter(|o| o.hash_kind == hash_kind).count();
assert_eq!(
ran,
expected / HASH_KINDS.len(),
"[{}] only {ran} checks ran against {hash_kind}",
harness.name()
);
}
conformance::assert_all_pass(&outcomes)
}
// 62 = 31 checks x 2 hash kinds. Asserted rather than counted at runtime, so
// that losing a check is a failure and not a quieter suite. It was 46 until
// 2026-08-10, when the suite was retargeted from `GitOps` at `GitServe` and
// gained six: atomic CAS, HEAD, `read`, `header`/`sizes`, `emit_pack` and
// `select`; 58 until 2026-08-31, when `reachable` gained the annotated-tag
// edge that had been costing six repositories their second copy; and 60 until
// 2026-09-08, when `put_pack_observed` gained a live object count and the
// suite gained the check that it MOVES.
const EXPECTED: usize = 62;
#[test]
fn the_in_memory_control_conforms() {
assert_eq!(run(&conformance::MemHarness::default()), EXPECTED);
}
/// **The outside oracle for the fixture packfile.**
///
/// `conformance::pack` writes the packs the suite pushes, and the in-memory
/// control parses them back. Both were written together, so their agreeing is
/// worth nothing on its own (one implementation, no twin). `gunnar_pack::index_pack` is an
/// independent implementation of git's format with no stake in either: it walks
/// the same bytes, resolves the same `OFS_DELTA`, and produces the same object
/// ids — or this module has stopped emitting git's format, whatever the rest of
/// the suite says.
///
/// This is the same pattern `znippy-plugin-git` uses with its gix oracle in
/// `pack_walk.rs`, and it is a dev-dependency for the same reason: the oracle
/// proves the parser and never ships.
#[test]
fn the_fixture_pack_is_a_real_packfile() {
for hash_kind in HASH_KINDS {
let repo = conformance::repo(hash_kind).expect("build the fixture");
let dir = tempfile::tempdir().expect("temporary directory");
let mut bytes = std::io::Cursor::new(repo.fixture.bytes.clone());
// `thread_limit(1)`: measured, not assumed — never nest a fan-out. This runs inside a
// test harness that is already running tests in parallel.
let indexed = gunnar_pack::index_pack(
&mut bytes,
Some(dir.path()),
&gunnar_pack::IndexOptions::new(hash_kind).thread_limit(1),
None,
)
.unwrap_or_else(|err| {
panic!("gitoxide refused a pack the conformance fixture calls valid: {err:#}")
});
assert_eq!(
indexed.num_objects as usize,
repo.fixture.entries.len(),
"[{hash_kind}] the oracle found {} objects and the fixture claims {}",
indexed.num_objects,
repo.fixture.entries.len()
);
}
}
/// The seam is usable as a trait object, which is how the server holds it.
///
/// `RepoStore::git()` hands back `Arc<dyn GitServe>` and the push path clones
/// that handle into an ingest that outlives the borrow. A compile-and-run
/// guarantee, not a stylistic preference.
///
/// It also pins the other half of the 2026-08-10 decision: the handle is usable
/// as **both** halves of the contract family through one method — the twelve by
/// the supertrait, `HEAD` by the accessor — which is what makes a second
/// accessor unnecessary.
#[test]
fn the_seam_is_usable_as_a_trait_object() {
struct OneRepo {
git_dir: std::path::PathBuf,
git: Arc<dyn GitServe>,
}
impl RepoStore for OneRepo {
fn git_dir(&self) -> &Path {
&self.git_dir
}
fn hash_kind(&self) -> gunnar_store::gix_hash::Kind {
gunnar_store::gix_hash::Kind::Sha256
}
fn git(&self) -> Arc<dyn GitServe> {
Arc::clone(&self.git)
}
}
let hash_kind = gunnar_store::gix_hash::Kind::Sha256;
let repo = conformance::repo(hash_kind).expect("build the fixture");
let store: Arc<dyn RepoStore> = Arc::new(OneRepo {
git_dir: std::path::PathBuf::from("/does/not/have/to/exist"),
git: Arc::new(gunnar_store::mem::MemGitServe::new(hash_kind)),
});
// The push path: clone the handle out and use it after the borrow is gone.
let git = store.git();
drop(store);
git.put_pack(&repo.fixture.bytes).expect("put_pack");
assert!(git.has(repo.commit1.as_slice()).expect("has"));
// The reading half, through the same handle: no second accessor, no
// downcast, and `HEAD` reachable where `refs()` deliberately does not carry
// it.
git.set_head("refs/heads/main").expect("set_head");
assert!(
git.refs().expect("refs").iter().all(|r| r.name != "HEAD"),
"HEAD is a pseudo-ref and must not be advertised as a row"
);
assert_eq!(
git.head()
.expect("head")
.and_then(|r| r.symref_target)
.as_deref(),
Some("refs/heads/main")
);
}
/// **The outside oracle for an EMITTED pack.**
///
/// `emit_pack_emits_the_set_it_was_given` reads the emitted pack back with the
/// suite's own parser, which can only prove membership: that parser and the
/// fixture writer were written together (one implementation, no twin). This drives
/// `gunnar_pack::index_pack` — an implementation of git's format with no stake
/// in gunnar's — over the same bytes, so *"the emitter produces a packfile"* is
/// checked by something that would have refused one.
///
/// A dev-dependency, so a downstream backend running the suite never links it.
#[test]
fn an_emitted_pack_is_a_real_packfile() {
for hash_kind in HASH_KINDS {
let repo = conformance::repo(hash_kind).expect("build the fixture");
let git = gunnar_store::mem::MemGitServe::new(hash_kind);
git.put_pack(&repo.fixture.bytes).expect("put_pack");
// The delta object **and the base it names**, so the emitted pack
// carries an OFS_DELTA whose base distance the emitter had to REWRITE —
// the one part of a pack copy that cannot be a memcpy, and the part a
// whole-object writer would paper over.
//
// The base is named explicitly, where this used to rely on the emitter
// dragging it in. Since 2026-08-14 no emitter does: a stored delta is
// reused only when its base is also being packed, and a request holding
// the delta alone is answered with the object rebuilt whole. That would
// leave this test asserting `recompressed == 0` over a pack with no
// delta in it at all — green, and measuring nothing.
let mut out = Vec::new();
let stats = git
.emit_pack(
&[
repo.delta_blob.as_slice(),
repo.blob.as_slice(),
repo.commit2.as_slice(),
],
&[],
&Caps::modern(),
&mut out,
)
.expect("emit_pack");
let dir = tempfile::tempdir().expect("temporary directory");
let mut bytes = std::io::Cursor::new(out.clone());
let indexed = gunnar_pack::index_pack(
&mut bytes,
Some(dir.path()),
&gunnar_pack::IndexOptions::new(hash_kind).thread_limit(1),
None,
)
.unwrap_or_else(|err| {
panic!("gitoxide refused a pack the in-memory emitter produced: {err:#}")
});
assert_eq!(
indexed.num_objects as u64, stats.objects,
"[{hash_kind}] the oracle found {} objects and PackStats claims {}",
indexed.num_objects, stats.objects
);
// The premise, or `recompressed == 0` is satisfied by a pack with no
// delta in it at all: the subject really is stored as an OFS_DELTA, so
// the copy arm — the one that rewrites the distance — is the arm that
// ran.
let stored = git
.get(repo.delta_blob.as_slice())
.expect("get")
.expect("the store holds the fixture's delta object");
assert_eq!(
stored.obj_type,
gunnar_store::git_storage_trait::ObjType::OfsDelta,
"[{hash_kind}] premise: the fixture's delta object is not stored as an OFS_DELTA, so \
this pack carries no delta and asserts nothing about rewriting a base distance"
);
assert_eq!(
stats.recompressed, 0,
"[{hash_kind}] the in-memory emitter re-deflated {} entries; every base this request \
names is in the request, so every entry must be a copy",
stats.recompressed
);
assert_eq!(
stats.copied, stats.objects,
"[{hash_kind}] {} of {} entries were copied",
stats.copied, stats.objects
);
}
}