crates/gunnar-wire/tests/common/mod.rs
//! A repository built object by object in memory, and the little pkt-line
//! reader the byte-level tests read responses with.
//!
//! In-memory on purpose: these tests are about the *protocol*, and a fixture
//! that needs a filesystem would quietly stop the appliance case from being
//! testable at all.
//!
//! # 2026-08-10 — one store, not two
//!
//! It used to hold a `MemObjectStore` **and** a `MemRefStore`, because
//! `ObjectStore` and `RefStore` were two traits. They are one now
//! ([`GitServe`], which extends `GitOps`), so the fixture holds one
//! [`MemGitServe`] and the references live in it. That is not only tidier: an
//! advertisement and the pack it promises are now provably one snapshot,
//! because there is only one thing to snapshot.
//!
//! Two consequences worth naming, because tests here depend on both:
//!
//! * **Objects arrive as packs.** `GitOps` has no `write(kind, bytes)`; the
//! only way in is [`GitOps::put_pack`]. So [`commit`] builds a three-object
//! pack (blob, tree, commit) with `gunnar_pack`'s writer — the same writer
//! the shipping send-pack path uses, rather than a second spelling of git's
//! format here (one implementation).
//! * **`HEAD` is not a row** (`locked_on_target.md`'s first contract question). It is set through
//! [`GitServe::set_head`] and read through [`GitServe::head`], and
//! `Fixture::build` asserts it does not appear in `refs()`.
#![allow(dead_code)]
use gix_hash::{Kind, ObjectId};
use gunnar_pktline::{decode, Decoded, Packet};
use gunnar_store::git_storage_trait::{
Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
RefUpdate, Stored, TxId,
};
use gunnar_store::mem::MemGitServe;
/// The history every byte-level test runs against.
///
/// ```text
/// one ── two ─────── merge ── four refs/heads/main
/// └─── three ──────┘ refs/heads/side
/// (annotated v1 on merge)
/// ```
///
/// A **merge**, not a line. A three-commit chain makes negotiation look easy:
/// the client's first `have` is always the answer. The second parent is what
/// catches a traversal that only follows `parent[0]`.
pub struct Fixture {
/// Objects *and* references, wrapped in [`Counting`] so the cost suites can
/// ask what a request charged the store.
pub git: Counting<MemGitServe>,
pub kind: Kind,
pub one: ObjectId,
pub two: ObjectId,
pub three: ObjectId,
pub merge: ObjectId,
pub four: ObjectId,
pub tag: ObjectId,
}
impl Fixture {
pub fn build(kind: Kind) -> Self {
let git = Counting::new(MemGitServe::new(kind));
let one = commit(&git, kind, "a.txt", "one", &[], 1_700_000_001);
let two = commit(&git, kind, "b.txt", "two", &[one], 1_700_000_002);
let three = commit(&git, kind, "c.txt", "three", &[one], 1_700_000_003);
let merge = commit(&git, kind, "d.txt", "merge", &[two, three], 1_700_000_004);
let four = commit(&git, kind, "e.txt", "four", &[merge], 1_700_000_005);
let tag = write_object(&git, kind, gix_object::Kind::Tag, &tag_bytes(&merge, "v1"));
git.put_refs(&[
RefUpdate::set("refs/heads/main", four.to_string()),
RefUpdate::set("refs/heads/side", three.to_string()),
RefUpdate::set("refs/tags/v1", tag.to_string()),
])
.expect("install the references");
git.set_head("refs/heads/main").expect("point HEAD at main");
// Asserted rather than assumed: `HEAD` is a pseudo-ref and has
// an accessor, not a row. Every advertisement test below reads the ref
// stream, and a store that published `HEAD` in it would make each of
// them assert the wrong shape while still passing.
assert!(
!git.refs()
.expect("list refs")
.iter()
.any(|row| row.name == "HEAD"),
"the fixture store publishes HEAD as a row; the contract says it must not"
);
Fixture {
git,
kind,
one,
two,
three,
merge,
four,
tag,
}
}
/// Append `count` commits onto `tip`, returning the new tip.
pub fn extend(&self, tip: ObjectId, count: usize, prefix: &str) -> ObjectId {
self.extend_dated(tip, count, prefix, |index| 1_700_100_000 + index as i64)
}
/// [`Self::extend`] with the committer date chosen per commit.
///
/// Negotiation is bounded by the oldest `have`'s committer date, so a test
/// that wants a *constant* traversal cost across rounds has to control that
/// date rather than inherit an ascending one.
pub fn extend_dated(
&self,
tip: ObjectId,
count: usize,
prefix: &str,
at: impl Fn(usize) -> i64,
) -> ObjectId {
let mut head = tip;
for index in 0..count {
// Distinct content per commit, so the object count is
// `3 * count` and not silently smaller because forty commits
// shared one blob. A test whose expected number depends on
// accidental deduplication is a test nobody can read.
head = commit(
&self.git,
self.kind,
&format!("{prefix}{index}.txt"),
&format!("{prefix} {index}"),
&[head],
at(index),
);
}
head
}
/// Every commit id from `tip` back down its first-parent line, `count`
/// long, newest first, the order a git client offers `have`s in.
pub fn first_parent_haves(&self, tip: ObjectId, count: usize) -> Vec<ObjectId> {
let mut out = Vec::with_capacity(count);
let mut current = tip;
for _ in 0..count {
out.push(current);
let (_, data) = self
.git
.read(current.as_bytes())
.expect("read a commit")
.expect("the commit is here");
let parent = gix_object::CommitRef::from_bytes(&data, self.kind)
.expect("parse a commit")
.parents()
.next();
match parent {
Some(id) => current = id,
None => break,
}
}
out
}
// ── the reference namespace, through the one store ──────────────────────
//
// `RefStore::transaction` / `read` / `set_head` are gone. These are the
// three shapes the suites used, spelled once here rather than at every
// call site: `put_refs` is the force update `RefEdit::force` was, and
// `refs()` is the listing `read` walked.
/// Force `name` to `id`, whatever it held.
pub fn force_ref(&self, name: &str, id: ObjectId) {
self.git
.put_refs(&[RefUpdate::set(name, id.to_string())])
.unwrap_or_else(|e| panic!("install {name}: {e:#}"));
}
/// What `name` points at, or `None` if it is not there.
///
/// Direct references only, which is every reference these suites install.
/// A symbolic one would need [`gunnar_wire::RefSnapshot`], and the tests
/// that care about symrefs use it.
pub fn read_ref(&self, name: &str) -> Option<ObjectId> {
self.git
.refs()
.expect("list refs")
.into_iter()
.find(|row| row.name == name)
.and_then(|row| row.oid)
.map(|raw| ObjectId::try_from(raw.as_slice()).expect("a well-sized oid"))
}
/// Every reference name in the store, sorted, `HEAD` excluded because it
/// is not a row.
pub fn ref_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.git
.refs()
.expect("list refs")
.into_iter()
.map(|row| row.name)
.collect();
names.sort();
names
}
/// Delete every reference, leaving the objects where they are.
///
/// What a mirror push into a fresh repository sees: a store that holds the
/// history and names none of it. It used to be spelled *"build a second,
/// empty `MemRefStore` beside the shared object store"*, which one store
/// cannot do — and this says the same thing about the same store.
pub fn clear_refs(&self) {
let names: Vec<RefUpdate> = self
.git
.refs()
.expect("list refs")
.into_iter()
.map(|row| RefUpdate::delete(row.name))
.collect();
if !names.is_empty() {
self.git.put_refs(&names).expect("delete every reference");
}
}
}
/// A store that reports what was asked of it.
///
/// `has`, `header` and `read` are counted **separately and deliberately not
/// folded together**: an existence probe is not the same cost as inflating an
/// object, and a change that turned reads into probes would otherwise look like
/// a change that removed work.
///
/// # It wraps a `GitServe` now, and it no longer emits anything itself
///
/// It used to carry a whole-object pack writer, because `MemObjectStore` had
/// nothing to copy and the serve path refuses a store that cannot emit. That is
/// gone: [`MemGitServe::emit_pack`] is a genuine pack copy (`gunnar-store`
/// 2026-08-10), verified against `gunnar_pack::index_pack` as an outside
/// oracle, so this forwards to it and counts nothing extra. Three tests that
/// were `#[ignore]`d for want of an emitting backend run again because of it.
pub struct Counting<S = MemGitServe> {
pub inner: S,
reads: std::sync::atomic::AtomicUsize,
/// Reads that came back a **commit**, kept apart from the rest.
///
/// The walk's guard is a *ratio* — reads of commits over commits in the
/// walk — and a total that mixes commits with their trees and blobs cannot
/// express it. `nornir-bench`'s `commit_reads_per_commit` is the same
/// number on a million-commit repository; this is the same number in
/// `cargo test`, so the invariant is held by something that runs on every
/// change and not only by a benchmark someone remembers to run.
commit_reads: std::sync::atomic::AtomicUsize,
headers: std::sync::atomic::AtomicUsize,
contains: std::sync::atomic::AtomicUsize,
/// Reference transactions (`put_refs_cas` calls): each is a durable commit on a
/// real engine, so the count for a push is its fsync count (GUN-PERF).
ref_txns: std::sync::atomic::AtomicUsize,
}
impl<S: GitServe> Counting<S> {
pub fn new(inner: S) -> Self {
Counting {
inner,
reads: std::sync::atomic::AtomicUsize::new(0),
commit_reads: std::sync::atomic::AtomicUsize::new(0),
headers: std::sync::atomic::AtomicUsize::new(0),
contains: std::sync::atomic::AtomicUsize::new(0),
ref_txns: std::sync::atomic::AtomicUsize::new(0),
}
}
pub fn reset(&self) {
for counter in [
&self.reads,
&self.commit_reads,
&self.headers,
&self.contains,
&self.ref_txns,
] {
counter.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn read_count(&self) -> usize {
self.reads.load(std::sync::atomic::Ordering::SeqCst)
}
/// How many of the reads answered with a commit.
pub fn commit_read_count(&self) -> usize {
self.commit_reads.load(std::sync::atomic::Ordering::SeqCst)
}
/// How many reference transactions (`put_refs_cas` calls) were made.
pub fn ref_txn_count(&self) -> usize {
self.ref_txns.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn contains_count(&self) -> usize {
self.contains.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn touches(&self) -> usize {
self.reads.load(std::sync::atomic::Ordering::SeqCst)
+ self.headers.load(std::sync::atomic::Ordering::SeqCst)
+ self.contains.load(std::sync::atomic::Ordering::SeqCst)
}
}
/// The twelve, forwarded. [`GitOps::has`] and `put_refs_cas` are counted; the rest are writes
/// or bulk index questions that no cost test asserts on, and inventing a
/// counter nothing reads would be a number that could drift unnoticed.
impl<S: GitServe> GitOps for Counting<S> {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
self.inner.put_pack(bytes)
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
self.inner.get(oid)
}
/// Counted. This is the existence probe `ObjectStore::contains` was, and
/// `contains_count()` is what `negotiate_cost.rs` asserts a floor on.
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.contains
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
self.inner.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
self.inner.extents(oids)
}
fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
self.inner.refs()
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
self.inner.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
self.ref_txns
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
self.inner.reachable(want, have)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.inner.gc()
}
}
impl<S: GitServe> GitServe for Counting<S> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let got = self.inner.read(oid)?;
// Charged on what came back, not on what was asked for: the caller
// does not know the kind before the read, so counting by the answer is
// the only spelling that cannot be gamed by asking differently.
if matches!(got, Some((ObjType::Commit, _))) {
self.commit_reads
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
Ok(got)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.headers
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.inner.header(oid)
}
/// Counted **once per id asked about**, not once per call. `sizes` is the
/// bulk spelling of `header` and the negotiation path calls it a thousand
/// at a time; charging it one would let a change that moved work from
/// `header` into `sizes` read as a change that removed the work.
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.headers
.fetch_add(oids.len(), std::sync::atomic::Ordering::SeqCst);
self.inner.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
self.inner.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.inner.set_head(target)
}
fn emit_pack(
&self,
want: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
self.inner.emit_pack(want, have, caps, out)
}
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
self.inner.select(want, have)
}
}
/// A reference name, checked against the rules that used to live in
/// `FullName::new`.
///
/// `FullName` died with `RefStore`; the rules did not, and they are
/// [`gunnar_store::refname::validate`] now — one authority, the same one
/// `receive_pack::parse_command` uses as its path-traversal boundary. This
/// keeps the *validating* half at the fixture's call sites so a test cannot
/// quietly install `refs/heads/../../etc/passwd` and call it a reference.
pub fn name(text: &str) -> String {
gunnar_store::refname::validate(text).expect("test reference names are valid");
text.to_owned()
}
/// Store `objects` as one pack and hand back their ids, in the order given.
///
/// `GitOps` takes packs and nothing else, which is the contract being honest:
/// an engine that stores pack entries verbatim has no place to put a loose
/// object. The writer is `gunnar_pack`'s — the one the shipping send-pack path
/// uses — rather than a second spelling of git's entry format in a test module
/// (one implementation); `common/mod.rs` writing its own headers is exactly
/// the twinning that let the old fixture and the code under test agree with
/// each other and with nothing else.
pub fn write_pack_of(
git: &dyn GitOps,
kind: Kind,
objects: &[(gix_object::Kind, Vec<u8>)],
) -> Vec<ObjectId> {
let mut bytes = Vec::new();
let count = u32::try_from(objects.len()).expect("a fixture under 4 billion objects");
let mut writer =
gunnar_pack::PackWriter::new(&mut bytes, count, &gunnar_pack::WriteOptions::new(kind))
.expect("start a pack");
let mut ids = Vec::with_capacity(objects.len());
for (obj_kind, data) in objects {
writer.add_object(*obj_kind, data).expect("add an object");
ids.push(gix_object::compute_hash(kind, *obj_kind, data).expect("hash an object"));
}
writer.finish().expect("finish the pack");
git.put_pack(&bytes).expect("store the pack");
ids
}
/// One object, stored.
pub fn write_object(
git: &dyn GitOps,
kind: Kind,
obj_kind: gix_object::Kind,
bytes: &[u8],
) -> ObjectId {
write_pack_of(git, kind, &[(obj_kind, bytes.to_vec())])[0]
}
/// One commit, with its own blob and tree, on `parents`, dated `at`.
///
/// `pub` because the traversal tests in `src/graph.rs` build shapes
/// [`Fixture::extend_dated`] cannot — a merge of two lines, above all — and a
/// second commit writer under `src/` is exactly the twin one implementation
/// forbids.
///
/// The three objects go in as **one** pack rather than three. Nothing in these
/// suites can see the difference, and it keeps a 300-commit `extend` from
/// leaving nine hundred packs behind.
pub fn commit(
git: &dyn GitOps,
kind: Kind,
file: &str,
content: &str,
parents: &[ObjectId],
at: i64,
) -> ObjectId {
let blob_bytes = format!("{content}\n").into_bytes();
let blob =
gix_object::compute_hash(kind, gix_object::Kind::Blob, &blob_bytes).expect("hash the blob");
let tree_bytes = tree_bytes(file, &blob);
let tree =
gix_object::compute_hash(kind, gix_object::Kind::Tree, &tree_bytes).expect("hash the tree");
let mut text = format!("tree {}\n", tree.to_hex());
for parent in parents {
text.push_str(&format!("parent {}\n", parent.to_hex()));
}
text.push_str(&format!(
"author Gunnar Test <gunnar@example.invalid> {at} +0000\n\
committer Gunnar Test <gunnar@example.invalid> {at} +0000\n\n{content}\n"
));
let ids = write_pack_of(
git,
kind,
&[
(gix_object::Kind::Blob, blob_bytes),
(gix_object::Kind::Tree, tree_bytes),
(gix_object::Kind::Commit, text.into_bytes()),
],
);
ids[2]
}
fn tree_bytes(file: &str, blob: &gix_hash::oid) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"100644 ");
out.extend_from_slice(file.as_bytes());
out.push(0);
out.extend_from_slice(blob.as_bytes());
out
}
fn tag_bytes(target: &gix_hash::oid, tag: &str) -> Vec<u8> {
format!(
"object {}\ntype commit\ntag {tag}\n\
tagger Gunnar Test <gunnar@example.invalid> 1700000006 +0000\n\nrelease\n",
target.to_hex()
)
.into_bytes()
}
/// Every pkt-line in a buffered response, plus whatever tail was not framed.
pub struct Response {
pub packets: Vec<Packet>,
pub tail: Vec<u8>,
}
impl Response {
pub fn parse(bytes: &[u8]) -> Self {
let mut packets = Vec::new();
let mut rest = bytes;
while let Ok(Decoded::Packet(packet, used)) = decode(rest) {
packets.push(packet);
rest = &rest[used..];
}
Response {
packets,
tail: rest.to_vec(),
}
}
/// The text of every data packet, newline stripped.
pub fn lines(&self) -> Vec<String> {
self.packets
.iter()
.filter_map(|packet| packet.text_str().map(str::to_owned))
.collect()
}
/// Is there a data packet whose text is exactly this?
pub fn has_line(&self, text: &str) -> bool {
self.lines().iter().any(|line| line == text)
}
/// The payload of one sideband, concatenated.
///
/// The band byte is stripped here and nowhere else, so a test that gets a
/// valid pack out of this has proven the band byte was present. That is
/// the assertion, not an incidental detail.
pub fn band(&self, band: u8) -> Vec<u8> {
let mut out = Vec::new();
for packet in &self.packets {
if let Packet::Data(payload) = packet {
if payload.first() == Some(&band) {
out.extend_from_slice(&payload[1..]);
}
}
}
out
}
/// The bytes that follow the `packfile` section header, undecoded.
pub fn after_packfile_header(&self) -> Vec<u8> {
let mut out = Vec::new();
let mut seen = false;
for packet in &self.packets {
if let Packet::Data(payload) = packet {
if seen {
out.extend_from_slice(payload);
} else if payload.strip_suffix(b"\n").unwrap_or(payload) == b"packfile" {
seen = true;
}
}
}
out
}
}
/// Frame `lines` as a v2 argument section: caps, delim, arguments, flush.
pub fn v2_request(command: &str, capabilities: &[&str], arguments: &[&str]) -> Vec<u8> {
let mut out = Vec::new();
gunnar_pktline::write_text(&mut out, format!("command={command}").as_bytes()).unwrap();
for capability in capabilities {
gunnar_pktline::write_text(&mut out, capability.as_bytes()).unwrap();
}
gunnar_pktline::write_delim(&mut out).unwrap();
for argument in arguments {
gunnar_pktline::write_text(&mut out, argument.as_bytes()).unwrap();
}
gunnar_pktline::write_flush(&mut out).unwrap();
out
}
/// How many objects a pack holds, according to **gix's own indexer** rather
/// than to gunnar re-reading its own bytes.
pub fn index_and_count(pack: &[u8], kind: Kind) -> u32 {
let mut cursor = std::io::Cursor::new(pack);
gunnar_pack::index_pack(
&mut cursor,
None,
&gunnar_pack::IndexOptions::new(kind),
None,
)
.expect("gix could not index the pack gunnar produced")
.num_objects
}
// ── the receive-pack side ───────────────────────────────────────────────────
pub fn push_bytes(commands: &[(&str, &str, &str)], capabilities: &str, pack: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
for (index, (old, new, name)) in commands.iter().enumerate() {
let mut line = format!("{old} {new} {name}").into_bytes();
if index == 0 {
line.push(0);
line.extend_from_slice(capabilities.as_bytes());
}
gunnar_pktline::write_text(&mut out, &line).unwrap();
}
gunnar_pktline::write_flush(&mut out).unwrap();
out.extend_from_slice(pack);
out
}
/// `gunnar-pack`'s receipt in the words the protocol reports it in.
///
/// The two structs are deliberately not related by a `From` impl in either
/// crate: `gunnar-wire` is a **dev**-dependent of `gunnar-pack` now and must
/// stay that way (see `gunnar-wire/src/pack.rs`), so the mapping lives here, in
/// the only place that holds both.
fn as_receipt(indexed: gunnar_pack::IndexedPack) -> gunnar_wire::pack::IndexedPack {
gunnar_wire::pack::IndexedPack {
num_objects: indexed.num_objects,
pack_id: indexed.pack_id,
index_id: indexed.index_id,
pack_version: gunnar_wire::pack::PackVersion::from_u32(match indexed.pack_version {
gix_pack::data::Version::V3 => 3,
_ => 2,
}),
hash_kind: indexed.hash_kind,
index_path: indexed.index_path,
data_path: indexed.data_path,
keep_path: indexed.keep_path,
// `gunnar-pack` writes a real `.pack`, so `data_path` IS the
// measurement here and there is nothing to carry beside it. The field
// exists for the engine that writes no file.
stored_bytes: None,
}
}
/// The objects are already in the store, so the ingest only has to *validate*
/// the pack. Storing a pushed pack is a storage decision, and the real one is
/// proven by `real_git_interop.rs` against a filesystem repository.
///
/// Generic over the error on purpose. This module is compiled **twice**: into
/// the library's own unit tests and into every integration test. It can name
/// `gunnar_wire::` types in both, since `lib.rs` declares
/// `extern crate self as gunnar_wire`, but callers still differ in what they
/// want the failure as — `gunnar_wire::Error` here, `anyhow::Error` there — and
/// a second copy of the helper is what one implementation forbids.
pub fn validating_ingest<E>(
kind: Kind,
) -> impl Fn(&mut dyn std::io::BufRead) -> std::result::Result<gunnar_wire::pack::IndexedPack, E>
where
E: From<gunnar_wire::Error>,
{
move |pack: &mut dyn std::io::BufRead| {
gunnar_pack::index_pack(pack, None, &gunnar_pack::IndexOptions::new(kind), None)
.map(as_receipt)
.map_err(|err| gunnar_wire::Error::pack(err).into())
}
}
pub fn empty_pack(kind: Kind) -> Vec<u8> {
gunnar_pack::empty_pack(kind).expect("an empty pack is legal")
}
/// [`validating_ingest`] with the per-entry allocation ceiling set explicitly.
///
/// The ceiling is what stands between a receive path and a pack that asks it to
/// materialise more memory than the machine has, so a test needs to be able to
/// move it.
pub fn limited_ingest<E>(
kind: Kind,
limit: usize,
) -> impl Fn(&mut dyn std::io::BufRead) -> std::result::Result<gunnar_wire::pack::IndexedPack, E>
where
E: From<gunnar_wire::Error>,
{
move |pack: &mut dyn std::io::BufRead| {
gunnar_pack::index_pack(
pack,
None,
&gunnar_pack::IndexOptions::new(kind).alloc_limit_bytes(limit),
None,
)
.map(as_receipt)
.map_err(|err| gunnar_wire::Error::pack(err).into())
}
}
/// A store that **declines to answer reachability**, forwarding everything
/// else.
///
/// # Why a wire test needs one
///
/// `select_objects`' step 0 asks [`GitServe::select`] before it walks anything,
/// and [`MemGitServe`] answers: it is a real reachability walk over an
/// in-memory store. So a fixture built on it serves a clone without ever
/// running the traversal — which is correct, and fatal to any test whose
/// subject *is* the traversal. The walk ceiling, the walk pool and their two
/// refusal sentences are charged against ids the walk holds, and a walk that
/// never runs holds none: `walk_limit.rs` saw a one-byte ceiling admit a clone.
///
/// `Ok(None)` is not a hack here. It is the contract's own escape hatch —
/// *"cannot answer cheaply, walk it yourself"* — and the frozen
/// `GitServe` signature says it MUST stay reachable, because the Arrow arm returns it whenever its
/// reachability projection is empty, which a clean restart produces. This is a
/// backend in exactly that state, which is also the state the old
/// `MemObjectStore` fixture was permanently in (`reachable()` → `None`).
///
/// It borrows rather than owns so it can be laid over the fixture's own store
/// for one test without rebuilding the repository.
pub struct Unindexed<'a>(pub &'a dyn GitServe);
impl GitOps for Unindexed<'_> {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
self.0.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
self.0.put_pack(bytes)
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.0.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
self.0.get(oid)
}
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.0.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
self.0.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
self.0.extents(oids)
}
fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
self.0.refs()
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
self.0.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
self.0.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
self.0.reachable(want, have)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.0.gc()
}
}
impl GitServe for Unindexed<'_> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.0.read(oid)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.0.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.0.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
self.0.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.0.set_head(target)
}
fn emit_pack(
&self,
want: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
self.0.emit_pack(want, have, caps, out)
}
/// **The whole point.** No index, so no answer.
fn select(&self, _want: &[Oid<'_>], _have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
Ok(None)
}
}
// ── the byte-ceiling fixtures, shared by the two files that measure one ─────
//
// `tests/pack_ceiling.rs` (one push against its ceiling) and
// `tests/concurrent_pack_ceiling.rs` (eight at once against one namespace's
// room) need the same two things: a pack whose size on the wire means what it
// says, and a reader that records what was actually taken out of it. They were
// one file's private helpers until the second file wanted them; a second copy
// is what one implementation forbids, and a drifting copy is worse than a
// shared one here, because both files assert EXACT byte counts against them.
/// A packfile of `blobs` incompressible blobs of `size` bytes.
///
/// **Incompressible on purpose.** `PackBuilder` deflates every entry, so a
/// megabyte of `b'a'` is a few hundred bytes on the wire and a ceiling test
/// built on it would be measuring zlib. The bytes come from a 64-bit LCG, which
/// deflate cannot shrink, so the pack's size on the wire is within a percent of
/// `blobs * size` and the fixture means what it says.
pub fn incompressible_pack(kind: Kind, blobs: usize, size: usize) -> Vec<u8> {
let mut pb = gunnar_store::conformance::pack::PackBuilder::new(kind);
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
for _ in 0..blobs {
let mut body = vec![0u8; size];
for byte in body.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*byte = (state >> 33) as u8;
}
pb.object(gix_object::Kind::Blob, body);
}
pb.build().expect("a pack of whole objects is legal").bytes
}
/// A `BufRead` that records **what was actually taken out of it**.
///
/// This is the applied output both ceiling files turn on: not what the server
/// decided, but how much of the client's upload it asked for before it stopped.
pub struct Tracked {
inner: std::io::Cursor<Vec<u8>>,
/// Bytes the server took off the wire, command list included.
pub taken: u64,
}
impl Tracked {
pub fn new(bytes: Vec<u8>) -> Self {
Tracked {
inner: std::io::Cursor::new(bytes),
taken: 0,
}
}
}
impl std::io::Read for Tracked {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read = std::io::Read::read(&mut self.inner, buf)?;
self.taken += read as u64;
Ok(read)
}
}
impl std::io::BufRead for Tracked {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
std::io::BufRead::fill_buf(&mut self.inner)
}
fn consume(&mut self, amount: usize) {
std::io::BufRead::consume(&mut self.inner, amount);
self.taken += amount as u64;
}
}