pub mod pack;
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicUsize, Ordering};
use anyhow::{bail, ensure, Context};
use git_storage_trait::{Caps, GitOps, GitServe, ObjType, RefCas, RefRejection, RefUpdate};
use gix_hash::ObjectId;
use gix_object::Kind;
use crate::refname::HEAD;
use crate::secret::{SecretStore, SecretStoreExt};
pub const EMPTY_BLOB_SHA1: &str = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391";
pub const EMPTY_BLOB_SHA256: &str =
"473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813";
pub const EMPTY_TREE_SHA1: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
pub const EMPTY_TREE_SHA256: &str =
"6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321";
pub const HASH_KINDS: [gix_hash::Kind; 2] = [gix_hash::Kind::Sha1, gix_hash::Kind::Sha256];
pub struct Backend<G, S> {
pub git: G,
pub secrets: S,
}
pub trait Harness {
type Git: GitServe;
type Secrets: SecretStore;
fn name(&self) -> &str;
fn open(
&self,
slot: &str,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<Backend<Self::Git, Self::Secrets>>;
}
#[derive(Debug)]
pub struct Outcome {
pub check: &'static str,
pub hash_kind: gix_hash::Kind,
pub result: Result<(), String>,
}
impl Outcome {
pub fn passed(&self) -> bool {
self.result.is_ok()
}
}
type Check<H> = fn(&H, gix_hash::Kind) -> anyhow::Result<()>;
fn checks<H: Harness>() -> Vec<(&'static str, Check<H>)> {
vec![
(
"object_id_matches_git_ground_truth",
object_id_matches_git_ground_truth::<H>,
),
(
"pack_roundtrip_every_entry",
pack_roundtrip_every_entry::<H>,
),
(
"get_returns_the_verbatim_entry_bytes",
get_returns_the_verbatim_entry_bytes::<H>,
),
(
"size_is_post_resolution_not_stored_length",
size_is_post_resolution_not_stored_length::<H>,
),
(
"an_absent_object_is_absent",
an_absent_object_is_absent::<H>,
),
(
"extents_are_positional_and_stable",
extents_are_positional_and_stable::<H>,
),
("put_pack_is_idempotent", put_pack_is_idempotent::<H>),
(
"put_pack_observed_publishes_a_count_that_moves",
put_pack_observed_publishes_a_count_that_moves::<H>,
),
("refs_round_trip", refs_round_trip::<H>),
(
"update_ref_creates_only_when_absent",
update_ref_creates_only_when_absent::<H>,
),
(
"update_ref_is_a_real_compare_and_swap",
update_ref_is_a_real_compare_and_swap::<H>,
),
(
"update_ref_delete_checks_its_old_value",
update_ref_delete_checks_its_old_value::<H>,
),
(
"refs_that_cannot_coexist_are_refused",
refs_that_cannot_coexist_are_refused::<H>,
),
(
"put_refs_cas_applies_every_edit_or_none",
put_refs_cas_applies_every_edit_or_none::<H>,
),
(
"head_is_not_a_row_and_has_its_own_accessor",
head_is_not_a_row_and_has_its_own_accessor::<H>,
),
(
"tx_ids_report_what_happened",
tx_ids_report_what_happened::<H>,
),
(
"put_lands_the_pack_and_the_refs_together",
put_lands_the_pack_and_the_refs_together::<H>,
),
(
"reachable_returns_the_whole_closure",
reachable_returns_the_whole_closure::<H>,
),
(
"reachable_subtracts_what_the_client_has",
reachable_subtracts_what_the_client_has::<H>,
),
(
"reachable_tolerates_a_have_it_does_not_know",
reachable_tolerates_a_have_it_does_not_know::<H>,
),
(
"reachable_does_not_follow_a_submodule",
reachable_does_not_follow_a_submodule::<H>,
),
(
"reachable_follows_an_annotated_tag_to_its_target",
reachable_follows_an_annotated_tag_to_its_target::<H>,
),
(
"read_is_inflated_and_delta_resolved",
read_is_inflated_and_delta_resolved::<H>,
),
(
"header_and_sizes_agree_with_read",
header_and_sizes_agree_with_read::<H>,
),
(
"emit_pack_emits_the_set_it_was_given",
emit_pack_emits_the_set_it_was_given::<H>,
),
(
"select_agrees_with_the_graph_or_declines",
select_agrees_with_the_graph_or_declines::<H>,
),
(
"gc_keeps_everything_a_ref_points_at",
gc_keeps_everything_a_ref_points_at::<H>,
),
(
"the_store_survives_a_restart",
the_store_survives_a_restart::<H>,
),
("secret_put_get_delete", secret_put_get_delete::<H>),
(
"secret_get_or_create_survives_restart",
secret_get_or_create_survives_restart::<H>,
),
(
"secret_get_or_create_is_atomic",
secret_get_or_create_is_atomic::<H>,
),
]
}
pub fn check_names<H: Harness>() -> Vec<&'static str> {
checks::<H>().into_iter().map(|(name, _)| name).collect()
}
pub fn run_all<H: Harness>(harness: &H) -> Vec<Outcome> {
let mut outcomes = Vec::new();
for (check, run) in checks::<H>() {
for hash_kind in HASH_KINDS {
let result =
match std::panic::catch_unwind(AssertUnwindSafe(|| run(harness, hash_kind))) {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => Err(format!("{err:#}")),
Err(panic) => Err(format!("panicked: {}", panic_message(&panic))),
};
outcomes.push(Outcome {
check,
hash_kind,
result,
});
}
}
outcomes
}
fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = panic.downcast_ref::<&str>() {
(*s).to_owned()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic>".to_owned()
}
}
pub fn failures(outcomes: &[Outcome]) -> Vec<(&'static str, gix_hash::Kind, String)> {
outcomes
.iter()
.filter_map(|o| {
o.result
.as_ref()
.err()
.map(|msg| (o.check, o.hash_kind, msg.clone()))
})
.collect()
}
pub fn assert_all_pass(outcomes: &[Outcome]) -> usize {
let failed = failures(outcomes);
assert!(
!outcomes.is_empty(),
"the conformance suite ran zero checks: a broken runner, not a green backend"
);
if !failed.is_empty() {
let report = failed
.iter()
.map(|(check, kind, msg)| format!(" {check} [{kind}]: {msg}"))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} of {} conformance checks failed:\n{report}",
failed.len(),
outcomes.len()
);
}
outcomes.len()
}
pub struct Repo {
pub fixture: pack::Fixture,
pub empty_blob: ObjectId,
pub empty_tree: ObjectId,
pub blob: ObjectId,
pub delta_blob: ObjectId,
pub tree1: ObjectId,
pub commit1: ObjectId,
pub tree2: ObjectId,
pub commit2: ObjectId,
pub tag: ObjectId,
pub sub_tree: ObjectId,
pub sub_commit: ObjectId,
pub gitlink: ObjectId,
}
fn oid(hash_kind: gix_hash::Kind, kind: Kind, content: &[u8]) -> anyhow::Result<ObjectId> {
Ok(gix_object::compute_hash(hash_kind, kind, content).map_err(gix_error::Error::from)?)
}
fn tree_bytes(entries: &[(&str, u32, &gix_hash::oid)]) -> Vec<u8> {
let mut sorted: Vec<_> = entries.to_vec();
sorted.sort_by_key(|(name, _, _)| *name);
let mut out = Vec::new();
for (name, mode, id) in sorted {
out.extend_from_slice(format!("{mode:o} ").as_bytes());
out.extend_from_slice(name.as_bytes());
out.push(0);
out.extend_from_slice(id.as_bytes());
}
out
}
fn commit_bytes(tree: &gix_hash::oid, parents: &[ObjectId], message: &str) -> Vec<u8> {
let mut out = format!("tree {}\n", tree.to_hex());
for parent in parents {
out.push_str(&format!("parent {}\n", parent.to_hex()));
}
out.push_str("author Gunnar Test <gunnar@example.invalid> 1700000000 +0000\n");
out.push_str("committer Gunnar Test <gunnar@example.invalid> 1700000000 +0000\n\n");
out.push_str(message);
out.push('\n');
out.into_bytes()
}
fn tag_bytes(target: &gix_hash::oid, tag: &str) -> Vec<u8> {
let mut out = format!("object {}\n", target.to_hex());
out.push_str("type commit\n");
out.push_str(&format!("tag {tag}\n"));
out.push_str("tagger Gunnar Test <gunnar@example.invalid> 1700000000 +0000\n\n");
out.push_str("a tag\n");
out.into_bytes()
}
const BLOB: &[u8] = b"gunnar conformance blob\n";
const DELTA_BLOB: &[u8] =
b"a second blob, stored as a delta against the first, and deliberately a \
different length from the delta stream that encodes it\n";
pub fn repo(hash_kind: gix_hash::Kind) -> anyhow::Result<Repo> {
let empty_blob = oid(hash_kind, Kind::Blob, &[])?;
let empty_tree = oid(hash_kind, Kind::Tree, &[])?;
let blob = oid(hash_kind, Kind::Blob, BLOB)?;
let delta_blob = oid(hash_kind, Kind::Blob, DELTA_BLOB)?;
let tree1_bytes = tree_bytes(&[("file.txt", 0o100644, &blob)]);
let tree1 = oid(hash_kind, Kind::Tree, &tree1_bytes)?;
let commit1_bytes = commit_bytes(&tree1, &[], "root");
let commit1 = oid(hash_kind, Kind::Commit, &commit1_bytes)?;
let tree2_bytes = tree_bytes(&[
("file.txt", 0o100644, &blob),
("other.txt", 0o100644, &delta_blob),
]);
let tree2 = oid(hash_kind, Kind::Tree, &tree2_bytes)?;
let commit2_bytes = commit_bytes(&tree2, &[commit1], "second");
let commit2 = oid(hash_kind, Kind::Commit, &commit2_bytes)?;
let tag_bytes = tag_bytes(&commit1, "v0.1.0");
let tag = oid(hash_kind, Kind::Tag, &tag_bytes)?;
let gitlink = absent_oid(hash_kind, 0xcd);
let sub_tree_bytes = tree_bytes(&[
("file.txt", 0o100644, &blob),
("vendor", 0o160000, &gitlink),
]);
let sub_tree = oid(hash_kind, Kind::Tree, &sub_tree_bytes)?;
let sub_commit_bytes = commit_bytes(&sub_tree, &[], "with a submodule");
let sub_commit = oid(hash_kind, Kind::Commit, &sub_commit_bytes)?;
let mut builder = pack::PackBuilder::new(hash_kind);
let blob_at = builder.object(Kind::Blob, BLOB);
builder.delta(blob_at, DELTA_BLOB);
builder.object(Kind::Blob, Vec::new());
builder.object(Kind::Tree, Vec::new());
builder.object(Kind::Tree, tree1_bytes);
builder.object(Kind::Commit, commit1_bytes);
builder.object(Kind::Tree, tree2_bytes);
builder.object(Kind::Commit, commit2_bytes);
builder.object(Kind::Tag, tag_bytes);
builder.object(Kind::Tree, sub_tree_bytes);
builder.object(Kind::Commit, sub_commit_bytes);
Ok(Repo {
fixture: builder.build()?,
empty_blob,
empty_tree,
blob,
delta_blob,
tree1,
commit1,
tree2,
commit2,
tag,
sub_tree,
sub_commit,
gitlink,
})
}
fn absent_oid(hash_kind: gix_hash::Kind, fill: u8) -> ObjectId {
let hex: String = format!("{fill:02x}").repeat(hash_kind.len_in_bytes());
ObjectId::from_hex(hex.as_bytes()).expect("constructed a valid hex oid")
}
fn hex(id: &ObjectId) -> String {
id.to_hex().to_string()
}
fn sorted(mut ids: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
ids.sort();
ids
}
fn as_bytes(ids: &[ObjectId]) -> Vec<Vec<u8>> {
sorted(ids.iter().map(|id| id.as_slice().to_vec()).collect())
}
type Opened<H> = Backend<<H as Harness>::Git, <H as Harness>::Secrets>;
fn loaded<H: Harness>(
harness: &H,
slot: &str,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<(Opened<H>, Repo)> {
let backend = harness.open(slot, hash_kind)?;
let repo = repo(hash_kind)?;
backend
.git
.put_pack(&repo.fixture.bytes)
.context("put_pack refused the fixture pack")?;
Ok((backend, repo))
}
fn object_id_matches_git_ground_truth<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "ground_truth", hash_kind)?;
let (want_blob, want_tree) = match hash_kind {
gix_hash::Kind::Sha1 => (EMPTY_BLOB_SHA1, EMPTY_TREE_SHA1),
gix_hash::Kind::Sha256 => (EMPTY_BLOB_SHA256, EMPTY_TREE_SHA256),
other => bail!("no ground truth recorded for hash kind {other}"),
};
ensure!(
hex(&repo.empty_blob) == want_blob,
"the empty blob hashed to {} but git ({hash_kind}) says {want_blob}",
hex(&repo.empty_blob)
);
ensure!(
hex(&repo.empty_tree) == want_tree,
"the empty tree hashed to {} but git ({hash_kind}) says {want_tree}",
hex(&repo.empty_tree)
);
for (what, id) in [("blob", repo.empty_blob), ("tree", repo.empty_tree)] {
ensure!(
b.git.has(id.as_slice())?,
"the store does not hold the empty {what}, whose id git itself gives as {}",
hex(&id)
);
}
Ok(())
}
fn pack_roundtrip_every_entry<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "pack_roundtrip", hash_kind)?;
for entry in &repo.fixture.entries {
let id = entry.id;
ensure!(
b.git.has(id.as_slice())?,
"{:?} {} was in the pack but has() says no",
entry.kind,
hex(&id)
);
let stored = b
.git
.get(id.as_slice())?
.with_context(|| format!("get() found no {:?} {}", entry.kind, hex(&id)))?;
ensure!(
stored.obj_type == entry.obj_type,
"{} was stored as {} and comes back as {}; the type is the entry's type IN THE PACK, \
so a resolved type here means the verbatim bytes have been reinterpreted",
hex(&id),
entry.obj_type.as_str(),
stored.obj_type.as_str()
);
ensure!(
stored.uncompressed_size == entry.content.len() as u64,
"{} reports {} bytes and inflates to {}",
hex(&id),
stored.uncompressed_size,
entry.content.len()
);
ensure!(
b.git.size(id.as_slice())? == Some(entry.content.len() as u64),
"size() and get().uncompressed_size disagree about {}",
hex(&id)
);
ensure!(
stored.extent == b.git.extents(&[id.as_slice()])?[0].context("extents() lost it")?,
"get() and extents() disagree about where {} is",
hex(&id)
);
}
Ok(())
}
fn get_returns_the_verbatim_entry_bytes<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "verbatim", hash_kind)?;
for entry in &repo.fixture.entries {
let stored = b
.git
.get(entry.id.as_slice())?
.with_context(|| format!("get() found no {}", hex(&entry.id)))?;
let want = entry.bytes(&repo.fixture.bytes);
ensure!(
stored.bytes.len() == want.len(),
"{} came back as {} entry bytes; {} went in",
hex(&entry.id),
stored.bytes.len(),
want.len()
);
ensure!(
stored.bytes == want,
"{} came back with different entry bytes than went in; the payload has been \
re-encoded somewhere, and a pack-copying serve path would send it wrong",
hex(&entry.id)
);
}
Ok(())
}
fn size_is_post_resolution_not_stored_length<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "post_resolution", hash_kind)?;
let entry = repo
.fixture
.entry(&repo.delta_blob)
.context("the fixture lost its delta entry")?;
ensure!(
entry.obj_type == ObjType::OfsDelta,
"the fixture's delta entry is stored as {}; this check measures nothing without a delta",
entry.obj_type.as_str()
);
let resolved = entry.content.len() as u64;
let on_the_wire = entry.bytes(&repo.fixture.bytes).len() as u64;
ensure!(
resolved != on_the_wire,
"the fixture's delta stores exactly as many bytes as it resolves to, so this check \
cannot tell the two answers apart"
);
let stored = b
.git
.get(repo.delta_blob.as_slice())?
.context("get() found no delta object")?;
ensure!(
stored.uncompressed_size == resolved,
"a delta object reports {} bytes; it resolves to {resolved} and its entry is {on_the_wire}",
stored.uncompressed_size
);
ensure!(
b.git.size(repo.delta_blob.as_slice())? == Some(resolved),
"size() of a delta object is not its post-resolution size"
);
Ok(())
}
fn an_absent_object_is_absent<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, _repo) = loaded(harness, "absent", hash_kind)?;
let missing = absent_oid(hash_kind, 0xab);
ensure!(
!b.git.has(missing.as_slice())?,
"has() invented {}",
hex(&missing)
);
ensure!(
b.git.get(missing.as_slice())?.is_none(),
"get() invented {}",
hex(&missing)
);
ensure!(
b.git.size(missing.as_slice())?.is_none(),
"size() invented {}",
hex(&missing)
);
ensure!(
b.git.extents(&[missing.as_slice()])? == vec![None],
"extents() invented {}",
hex(&missing)
);
Ok(())
}
fn extents_are_positional_and_stable<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "extents", hash_kind)?;
let missing = absent_oid(hash_kind, 0xab);
let query: Vec<&[u8]> = vec![
repo.commit1.as_slice(),
missing.as_slice(),
repo.blob.as_slice(),
repo.delta_blob.as_slice(),
];
let first = b.git.extents(&query)?;
ensure!(
first.len() == query.len(),
"extents() answered {} of {} questions",
first.len(),
query.len()
);
ensure!(
first[1].is_none(),
"extents() answered for an object that is not there; every later answer in the \
vector is now attributed to the wrong oid"
);
for (i, id) in [(0, repo.commit1), (2, repo.blob), (3, repo.delta_blob)] {
ensure!(
first[i].is_some(),
"extents() has no extent for {}, which it holds",
hex(&id)
);
}
let second = b.git.extents(&query)?;
ensure!(
first == second,
"two identical extents() calls answered differently: {first:?} then {second:?}"
);
Ok(())
}
fn put_pack_is_idempotent<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "idempotent", hash_kind)?;
let before = b
.git
.get(repo.commit1.as_slice())?
.context("the first push did not land")?;
b.git
.put_pack(&repo.fixture.bytes)
.context("the same pack was refused the second time")?;
let after = b
.git
.get(repo.commit1.as_slice())?
.context("the second push lost an object the first one had")?;
ensure!(
before.bytes == after.bytes && before.uncompressed_size == after.uncompressed_size,
"re-pushing a pack changed an object that was already there"
);
for entry in &repo.fixture.entries {
ensure!(
b.git.has(entry.id.as_slice())?,
"{} went missing when its pack was pushed twice",
hex(&entry.id)
);
}
Ok(())
}
fn put_pack_observed_publishes_a_count_that_moves<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
const BLOBS: usize = 300;
let b = harness.open("pack_progress", hash_kind)?;
let mut builder = pack::PackBuilder::new(hash_kind);
for n in 0..BLOBS {
builder.object(Kind::Blob, format!("object number {n}\n").into_bytes());
}
let fixture = builder.build().context("building a many-object pack")?;
#[derive(Default)]
struct Series(std::sync::Mutex<Vec<(u64, u64)>>);
impl git_storage_trait::PackProgress for Series {
fn indexed(&self, done: u64, total: u64) {
self.0
.lock()
.unwrap_or_else(|p| p.into_inner())
.push((done, total));
}
}
let series = Series::default();
b.git
.put_pack_observed(&fixture.bytes, &series)
.context("put_pack_observed refused a pack put_pack would have taken")?;
for entry in &fixture.entries {
ensure!(
b.git.has(entry.id.as_slice())?,
"{} is not in the store after put_pack_observed",
hex(&entry.id)
);
}
let seen = series.0.into_inner().unwrap_or_else(|p| p.into_inner());
if !b.git.publishes_pack_progress() {
ensure!(
seen.is_empty(),
"`{}` answers publishes_pack_progress() = false and then published \
{} reading(s). One of the two is a lie, and a caller believes the \
declaration.",
harness.name(),
seen.len()
);
eprintln!(
"conformance: `{}` declines a live pack-progress count \
(publishes_pack_progress() = false); the property is vacuous for it",
harness.name()
);
return Ok(());
}
let total = BLOBS as u64;
ensure!(
!seen.is_empty(),
"`{}` answers publishes_pack_progress() = true and published nothing",
harness.name()
);
for (done, declared) in &seen {
ensure!(
*declared == total,
"the denominator moved: a reading declared {declared} objects for a pack \
of {total}"
);
ensure!(
*done <= total,
"a reading claims {done} of {total} objects are in"
);
}
ensure!(
seen.windows(2).all(|w| w[0].0 <= w[1].0),
"the count went backwards: {:?}",
seen.iter().map(|(d, _)| *d).collect::<Vec<_>>()
);
ensure!(
seen.last().map(|(done, _)| *done) == Some(total),
"the last reading is {:?}, not ({total}, {total}) — a bar that stops short \
of the end reads exactly like a transfer that hung",
seen.last()
);
let distinct = {
let mut values: Vec<u64> = seen.iter().map(|(done, _)| *done).collect();
values.dedup();
values.len()
};
ensure!(
distinct > 2,
"the count has {distinct} distinct value(s) over a pack of {total} objects. \
Two is what a store that publishes nothing looks like once the layers above \
it have added `(0, N)` and `(N, N)`; a count that moves is the whole claim."
);
Ok(())
}
fn refs_round_trip<H: Harness>(harness: &H, hash_kind: gix_hash::Kind) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "refs_roundtrip", hash_kind)?;
b.git.put_refs(&[
RefUpdate::set("refs/heads/main", hex(&repo.commit1)),
RefUpdate::set("refs/tags/v0.1.0", hex(&repo.tag)).with_peeled(hex(&repo.commit1)),
])?;
let rows = b.git.refs()?;
let names: Vec<&str> = rows.iter().map(|r| r.name.as_str()).collect();
ensure!(
names == vec!["refs/heads/main", "refs/tags/v0.1.0"],
"refs() returned {names:?}; the namespace is advertised in this order"
);
let main = &rows[0];
ensure!(
main.oid.as_deref() == Some(repo.commit1.as_slice()),
"refs/heads/main came back pointing at {:?}",
main.oid
);
let tag = &rows[1];
ensure!(
tag.peeled.as_deref() == Some(repo.commit1.as_slice()),
"an annotated tag came back with peeled {:?}; a client cannot resolve the tag without it",
tag.peeled
);
b.git.put_refs(&[RefUpdate::delete("refs/tags/v0.1.0")])?;
let after = b.git.refs()?;
let remaining: Vec<&str> = after.iter().map(|r| r.name.as_str()).collect();
ensure!(
remaining == vec!["refs/heads/main"],
"after a delete, refs() returned {remaining:?}"
);
Ok(())
}
fn update_ref_creates_only_when_absent<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "ref_create", hash_kind)?;
let name = "refs/heads/main";
b.git
.update_ref(name, None, Some(repo.commit1.as_slice()))?;
ensure!(
b.git.refs()?.iter().any(|r| r.name == name),
"a create was accepted and the reference is not there"
);
for (what, target) in [
("a different value", repo.commit2),
("ITS CURRENT VALUE", repo.commit1),
] {
if b.git
.update_ref(name, None, Some(target.as_slice()))
.is_ok()
{
bail!(
"creating an existing reference at {what} was accepted, so `old: None` is not \
a create guard on this backend"
);
}
let row = b
.git
.refs()?
.into_iter()
.find(|r| r.name == name)
.context("the refused create removed the reference")?;
ensure!(
row.oid.as_deref() == Some(repo.commit1.as_slice()),
"a rejected create still changed the stored value"
);
}
Ok(())
}
fn update_ref_is_a_real_compare_and_swap<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "ref_cas", hash_kind)?;
let name = "refs/heads/main";
b.git
.update_ref(name, None, Some(repo.commit1.as_slice()))?;
b.git
.update_ref(
name,
Some(repo.commit1.as_slice()),
Some(repo.commit2.as_slice()),
)
.context("a compare-and-swap at the correct old value was refused")?;
let current = |b: &Opened<H>| -> anyhow::Result<Option<Vec<u8>>> {
Ok(b.git
.refs()?
.into_iter()
.find(|r| r.name == name)
.and_then(|r| r.oid))
};
ensure!(
current(&b)?.as_deref() == Some(repo.commit2.as_slice()),
"a successful compare-and-swap did not move the reference"
);
if b.git
.update_ref(
name,
Some(repo.commit1.as_slice()),
Some(repo.tag.as_slice()),
)
.is_ok()
{
bail!("a stale-old-value update was ACCEPTED");
}
ensure!(
current(&b)?.as_deref() == Some(repo.commit2.as_slice()),
"a rejected compare-and-swap still changed the stored value"
);
if b.git
.update_ref(
"refs/heads/absent",
Some(repo.commit1.as_slice()),
Some(repo.commit2.as_slice()),
)
.is_ok()
{
bail!("an update naming an old value for a reference that does not exist was accepted");
}
Ok(())
}
fn update_ref_delete_checks_its_old_value<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "ref_delete", hash_kind)?;
let doomed = "refs/heads/doomed";
let kept = "refs/heads/kept";
for name in [doomed, kept] {
b.git
.update_ref(name, None, Some(repo.commit1.as_slice()))?;
}
if b.git
.update_ref(kept, Some(repo.commit2.as_slice()), None)
.is_ok()
{
bail!("a delete with a stale old value was accepted");
}
ensure!(
b.git.refs()?.iter().any(|r| r.name == kept),
"a rejected delete removed the reference anyway"
);
b.git
.update_ref(doomed, Some(repo.commit1.as_slice()), None)
.context("a delete at the correct old value was refused")?;
let remaining: Vec<String> = b.git.refs()?.into_iter().map(|r| r.name).collect();
ensure!(
remaining == vec![kept.to_string()],
"after the delete, refs() returned {remaining:?}"
);
Ok(())
}
fn refs_that_cannot_coexist_are_refused<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "ref_conflict", hash_kind)?;
let at = hex(&repo.commit1);
b.git.put_refs(&[RefUpdate::set("refs/heads/x", &at)])?;
if b.git
.put_refs(&[RefUpdate::set("refs/heads/x/y", &at)])
.is_ok()
{
bail!("refs/heads/x/y was created beside refs/heads/x; git refuses the pair");
}
ensure!(
!b.git.refs()?.iter().any(|r| r.name == "refs/heads/x/y"),
"the refused create left refs/heads/x/y behind"
);
let (b, repo) = loaded(harness, "ref_conflict_reverse", hash_kind)?;
let at = hex(&repo.commit1);
b.git.put_refs(&[RefUpdate::set("refs/heads/x/y", &at)])?;
if b.git
.put_refs(&[RefUpdate::set("refs/heads/x", &at)])
.is_ok()
{
bail!("refs/heads/x was created beside refs/heads/x/y; git refuses the pair");
}
let (b, repo) = loaded(harness, "ref_conflict_batch", hash_kind)?;
let at = hex(&repo.commit1);
if b.git
.put_refs(&[
RefUpdate::set("refs/heads/x", &at),
RefUpdate::set("refs/heads/x/y", &at),
])
.is_ok()
{
bail!("one batch created both refs/heads/x and refs/heads/x/y");
}
ensure!(
b.git.refs()?.is_empty(),
"a refused batch applied half of itself: {:?}",
b.git.refs()?
);
let (b, repo) = loaded(harness, "ref_conflict_control", hash_kind)?;
let at = hex(&repo.commit1);
b.git
.put_refs(&[
RefUpdate::set("refs/heads/x", &at),
RefUpdate::set("refs/heads/xy", &at),
])
.context(
"refs/heads/x and refs/heads/xy are not a conflict and must coexist; a backend that \
refuses every shared prefix passes every assertion above and breaks branch naming",
)?;
ensure!(
b.git.refs()?.len() == 2,
"the control's two non-conflicting references did not both land"
);
Ok(())
}
fn put_refs_cas_applies_every_edit_or_none<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "refs_cas", hash_kind)?;
let at = |name: &str| -> anyhow::Result<Option<Vec<u8>>> {
Ok(b.git
.refs()?
.into_iter()
.find(|r| r.name == name)
.and_then(|r| r.oid))
};
let (one, two) = ("refs/heads/one", "refs/heads/two");
b.git
.put_refs_cas(&[])
.context("an empty atomic batch was refused; it is a no-op, not an error")?;
b.git
.put_refs_cas(&[
RefCas {
name: one.to_owned(),
old: None,
new: Some(repo.commit1.as_slice()),
peeled: None,
},
RefCas {
name: two.to_owned(),
old: None,
new: Some(repo.commit1.as_slice()),
peeled: None,
},
])
.context("an atomic batch of two creates against an empty namespace was refused")?;
ensure!(
at(one)?.as_deref() == Some(repo.commit1.as_slice())
&& at(two)?.as_deref() == Some(repo.commit1.as_slice()),
"an accepted atomic batch did not apply both of its edits"
);
let err = b
.git
.put_refs_cas(&[
RefCas {
name: one.to_owned(),
old: Some(repo.commit1.as_slice()),
new: Some(repo.commit2.as_slice()),
peeled: None,
},
RefCas {
name: two.to_owned(),
old: Some(repo.commit2.as_slice()),
new: Some(repo.commit2.as_slice()),
peeled: None,
},
])
.err()
.context("an atomic batch with a stale expectation in it was ACCEPTED")?;
ensure!(
at(one)?.as_deref() == Some(repo.commit1.as_slice())
&& at(two)?.as_deref() == Some(repo.commit1.as_slice()),
"a refused atomic batch applied part of itself, which is the one thing --atomic \
promises never happens"
);
let rejection = RefRejection::of(&err).with_context(|| {
format!(
"the rejection carries no typed RefRejection, so receive-pack can only learn which \
ref lost by reading the message `{err:#}`"
)
})?;
ensure!(
rejection.is_cas_failure() && rejection.name() == two,
"the rejection named {:?} (cas_failure={}); the edit that lost was {two}",
rejection.name(),
rejection.is_cas_failure()
);
let err = b
.git
.put_refs_cas(&[RefCas {
name: one.to_owned(),
old: None,
new: Some(repo.commit1.as_slice()),
peeled: None,
}])
.err()
.context(
"a `must not exist` create was accepted against a reference that already exists and \
already holds the proposed value; the expectation is being short-circuited rather \
than evaluated, so two racing pushes would both be told they won",
)?;
ensure!(
RefRejection::of(&err).is_some_and(|r| r.is_cas_failure()),
"the same-value refusal is not a typed compare-and-swap rejection: {err:#}"
);
b.git
.put_refs_cas(&[
RefCas {
name: one.to_owned(),
old: Some(repo.commit1.as_slice()),
new: Some(repo.commit2.as_slice()),
peeled: None,
},
RefCas {
name: two.to_owned(),
old: Some(repo.commit1.as_slice()),
new: None,
peeled: None,
},
])
.context("an atomic batch whose every expectation held was refused")?;
ensure!(
at(one)?.as_deref() == Some(repo.commit2.as_slice()) && at(two)?.is_none(),
"the final atomic batch did not apply its update and its delete"
);
Ok(())
}
fn head_is_not_a_row_and_has_its_own_accessor<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "head", hash_kind)?;
ensure!(
b.git.head()?.is_none(),
"a repository that has never pointed HEAD answered one; `None` is what an empty \
repository has"
);
b.git
.put_refs(&[RefUpdate::set("refs/heads/main", hex(&repo.commit1))])?;
b.git
.set_head("refs/heads/main")
.context("set_head refused a symbolic target under refs/")?;
let rows = b.git.refs()?;
ensure!(
rows.iter().all(|r| r.name != HEAD),
"HEAD is a pseudo-ref and must not appear in the ref stream: {:?}",
rows.iter().map(|r| &r.name).collect::<Vec<_>>()
);
let names: Vec<&str> = rows.iter().map(|r| r.name.as_str()).collect();
ensure!(
names == vec!["refs/heads/main"],
"filtering HEAD out of the row stream must not have filtered anything else: {names:?}"
);
let head = b
.git
.head()?
.context("HEAD was just pointed and head() cannot find it")?;
ensure!(
head.name == HEAD,
"head() answered a row named {:?}",
head.name
);
ensure!(
head.symref_target.as_deref() == Some("refs/heads/main"),
"HEAD came back pointing at {:?}",
head.symref_target
);
ensure!(
head.oid.is_none(),
"a symbolic HEAD has no direct target of its own, and this one reports {:?}",
head.oid
);
b.git
.put_refs(&[RefUpdate::set("refs/heads/other", hex(&repo.commit2))])?;
b.git.set_head("refs/heads/other")?;
ensure!(
b.git.head()?.and_then(|r| r.symref_target).as_deref() == Some("refs/heads/other"),
"set_head did not move HEAD"
);
ensure!(
b.git.refs()?.iter().all(|r| r.name != HEAD),
"HEAD appeared in the ref stream once it had been moved"
);
b.git
.set_head_projected("refs/heads/main")
.context("set_head_projected refused a symbolic target under refs/")?;
ensure!(
b.git.head()?.and_then(|r| r.symref_target).as_deref() == Some("refs/heads/main"),
"set_head_projected did not move HEAD; it is set_head without the barrier, not without the write"
);
ensure!(
b.git.refs()?.iter().all(|r| r.name != HEAD),
"a projected HEAD appeared in the ref stream"
);
Ok(())
}
fn tx_ids_report_what_happened<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let b = harness.open("tx_ids", hash_kind)?;
let repo = repo(hash_kind)?;
let pack_tx = b.git.put_pack(&repo.fixture.bytes)?;
ensure!(
pack_tx.push_seq.is_none(),
"put_pack reported a push_seq ({:?}) for a transaction that carried no ref update",
pack_tx.push_seq
);
let first = b
.git
.put_refs(&[RefUpdate::set("refs/heads/main", hex(&repo.commit1))])?;
ensure!(
first.pack_id.is_none() && first.extent.is_none(),
"put_refs reported a pack ({:?}, {:?}) for a transaction that carried none",
first.pack_id,
first.extent
);
let second = b
.git
.put_refs(&[RefUpdate::set("refs/heads/other", hex(&repo.commit2))])?;
match (first.push_seq, second.push_seq) {
(None, None) => {} (Some(a), Some(b)) => ensure!(
b > a,
"two ref transactions reported push_seq {a} then {b}; a sequence that does not \
advance orders nothing"
),
(a, b) => bail!(
"push_seq was {a:?} for one ref transaction and {b:?} for the next; a backend \
either has an ordering authority or it does not"
),
}
Ok(())
}
fn put_lands_the_pack_and_the_refs_together<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let b = harness.open("put_together", hash_kind)?;
let repo = repo(hash_kind)?;
b.git.put(
&repo.fixture.bytes,
&[RefUpdate::set("refs/heads/main", hex(&repo.commit2))],
)?;
let row = b
.git
.refs()?
.into_iter()
.find(|r| r.name == "refs/heads/main")
.context("put() landed the pack and not the reference")?;
ensure!(
row.oid.as_deref() == Some(repo.commit2.as_slice()),
"put() left refs/heads/main pointing somewhere else"
);
for id in [repo.commit2, repo.tree2, repo.blob, repo.delta_blob] {
ensure!(
b.git.has(id.as_slice())?,
"put() advertised a reference whose closure it does not hold: {} is missing, \
which is the one crash ordering no later pass repairs",
hex(&id)
);
}
Ok(())
}
fn reachable_returns_the_whole_closure<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "reach_clone", hash_kind)?;
let got = sorted(b.git.reachable(&[repo.commit2.as_slice()], &[])?);
let want = as_bytes(&[
repo.commit2,
repo.commit1,
repo.tree2,
repo.tree1,
repo.blob,
repo.delta_blob,
]);
ensure!(
got == want,
"the closure of a clone came back with {} objects, not {}. An under-send is the \
failure no caller can detect: the clone ends short by everything beneath the tip \
and exits zero",
got.len(),
want.len()
);
Ok(())
}
fn reachable_subtracts_what_the_client_has<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "reach_incremental", hash_kind)?;
let got = sorted(
b.git
.reachable(&[repo.commit2.as_slice()], &[repo.commit1.as_slice()])?,
);
let want = as_bytes(&[repo.commit2, repo.tree2, repo.delta_blob]);
ensure!(
got == want,
"an incremental fetch was answered with {} objects, not {}. The blob and tree the \
client already has must not be sent again, and everything new must be",
got.len(),
want.len()
);
ensure!(
!got.is_empty(),
"an incremental fetch was answered with nothing, and the second commit is new"
);
Ok(())
}
fn reachable_tolerates_a_have_it_does_not_know<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "reach_unknown_have", hash_kind)?;
let stranger = absent_oid(hash_kind, 0xef);
let got = sorted(
b.git
.reachable(&[repo.commit1.as_slice()], &[stranger.as_slice()])
.context("a `have` this store does not hold was treated as an error")?,
);
let want = as_bytes(&[repo.commit1, repo.tree1, repo.blob]);
ensure!(
got == want,
"an unknown `have` changed the answer: {} objects, not {}",
got.len(),
want.len()
);
Ok(())
}
fn reachable_does_not_follow_a_submodule<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "reach_submodule", hash_kind)?;
let got = sorted(
b.git
.reachable(&[repo.sub_commit.as_slice()], &[])
.context("a repository with a submodule was reported as broken")?,
);
let want = as_bytes(&[repo.sub_commit, repo.sub_tree, repo.blob]);
ensure!(
!got.contains(&repo.gitlink.as_slice().to_vec()),
"the closure contains the gitlink commit {}, which lives in another repository and \
is not in this store; the resulting pack cannot be closed by any client",
hex(&repo.gitlink)
);
ensure!(
got == want,
"the closure of a commit with a submodule came back as {} objects, not {}",
got.len(),
want.len()
);
Ok(())
}
fn reachable_follows_an_annotated_tag_to_its_target<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "reach_tag", hash_kind)?;
let got = sorted(b.git.reachable(&[repo.tag.as_slice()], &[])?);
let want = as_bytes(&[repo.tag, repo.commit1, repo.tree1, repo.blob]);
ensure!(
got.contains(&repo.tag.as_slice().to_vec()),
"the tag object {} is not in its own closure; the reference being pushed names it, so \
a pack without it moves a ref onto an object the receiver does not have",
hex(&repo.tag)
);
ensure!(
got.contains(&repo.commit1.as_slice().to_vec()),
"the closure of the annotated tag {} does not contain the commit {} it names. This is \
the `is not in this repository (peeling a tag)` refusal at its source: the tag goes \
into the pack and nothing under it does",
hex(&repo.tag),
hex(&repo.commit1)
);
ensure!(
got == want,
"the closure of an annotated tag came back with {} objects, not {}",
got.len(),
want.len()
);
Ok(())
}
fn read_is_inflated_and_delta_resolved<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "serve_read", hash_kind)?;
for entry in &repo.fixture.entries {
let (obj_type, content) = b
.git
.read(entry.id.as_slice())?
.with_context(|| format!("read() found no {}", hex(&entry.id)))?;
ensure!(
!matches!(obj_type, ObjType::OfsDelta | ObjType::RefDelta),
"{} came back from read() typed {}; read() reports the RESOLVED kind, and a caller \
handed a delta type would parse a delta stream as an object",
hex(&entry.id),
obj_type.as_str()
);
ensure!(
obj_type == pack_type_of(entry.kind),
"{} is a {:?} and read() called it {}",
hex(&entry.id),
entry.kind,
obj_type.as_str()
);
ensure!(
content == entry.content,
"{} inflated to {} bytes and the fixture wrote {}",
hex(&entry.id),
content.len(),
entry.content.len()
);
}
let delta = repo
.fixture
.entry(&repo.delta_blob)
.context("the fixture lost its delta entry")?;
let stored = b
.git
.get(repo.delta_blob.as_slice())?
.context("get() found no delta object")?;
let (_, inflated) = b
.git
.read(repo.delta_blob.as_slice())?
.context("read() found no delta object")?;
ensure!(
stored.bytes != inflated,
"get() and read() returned the same bytes for an entry stored as {}; one of them is \
not doing its job",
delta.obj_type.as_str()
);
ensure!(
b.git
.read(absent_oid(hash_kind, 0xab).as_slice())?
.is_none(),
"read() invented an object that is not there"
);
Ok(())
}
fn header_and_sizes_agree_with_read<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "serve_header", hash_kind)?;
for entry in &repo.fixture.entries {
let (obj_type, size) = b
.git
.header(entry.id.as_slice())?
.with_context(|| format!("header() found no {}", hex(&entry.id)))?;
ensure!(
obj_type == pack_type_of(entry.kind) && size == entry.content.len() as u64,
"header() says {} is a {} of {size} bytes; it is a {:?} of {}",
hex(&entry.id),
obj_type.as_str(),
entry.kind,
entry.content.len()
);
}
ensure!(
b.git
.header(absent_oid(hash_kind, 0xab).as_slice())?
.is_none(),
"header() invented an object that is not there"
);
let missing = absent_oid(hash_kind, 0xab);
let query: Vec<&[u8]> = vec![
repo.commit1.as_slice(),
missing.as_slice(),
repo.delta_blob.as_slice(),
];
let sizes = b.git.sizes(&query)?;
ensure!(
sizes.len() == query.len(),
"sizes() answered {} of {} questions",
sizes.len(),
query.len()
);
ensure!(
sizes[1].is_none(),
"sizes() answered for an object that is not there; every later answer in the vector is \
now attributed to the wrong oid"
);
let want_delta = repo
.fixture
.entry(&repo.delta_blob)
.context("the fixture lost its delta entry")?
.content
.len() as u64;
ensure!(
sizes[2] == Some(want_delta),
"sizes() reports {:?} for a delta object that resolves to {want_delta} bytes; bulk and \
single-object sizes must be the same POST-RESOLUTION number",
sizes[2]
);
Ok(())
}
fn emit_pack_emits_the_set_it_was_given<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "serve_emit", hash_kind)?;
let caps = Caps::modern();
let mut out = Vec::new();
let stats = b
.git
.emit_pack(&[repo.commit2.as_slice()], &[], &caps, &mut out)
.context("emit_pack refused a single commit")?;
let ids = pack_ids(&out, hash_kind).context("emit_pack produced something unreadable")?;
ensure!(
ids == vec![repo.commit2],
"emit_pack was given ONE object and emitted {}: {:?}. `objects` is the exact set to \
emit, not a set of tips to close over — an engine that closes over it sends back \
everything a --filter or a --depth had just excluded, and the client cannot tell",
ids.len(),
ids.iter().map(hex).collect::<Vec<_>>()
);
ensure!(
stats.objects == ids.len() as u64,
"PackStats reports {} objects and the pack carries {}",
stats.objects,
ids.len()
);
ensure!(
stats.bytes == out.len() as u64,
"PackStats reports {} bytes and {} were written",
stats.bytes,
out.len()
);
ensure!(
stats.copied + stats.recompressed == stats.objects,
"PackStats accounts for {} of {} entries as copied or recompressed; the pair is the \
receipt that tells a pack copy from a re-deflate, and an entry in neither column \
makes it unreadable",
stats.copied + stats.recompressed,
stats.objects
);
let mut out = Vec::new();
let stats = b
.git
.emit_pack(&[repo.delta_blob.as_slice()], &[], &caps, &mut out)
.context("emit_pack refused an object the fixture stores as a delta")?;
let ids = pack_ids(&out, hash_kind)?;
ensure!(
ids == vec![repo.delta_blob],
"emit_pack was asked for exactly one object — {}, which this fixture stores as a delta \
against {} — and emitted {}: {:?}. Nothing may be added here, not by reachability and \
not by the delta format: an out-of-set base is an object the client did not ask for, \
and a tree one drags in children the pack does not carry, which is a clone stock git \
refuses while the server reports success",
hex(&repo.delta_blob),
hex(&repo.blob),
ids.len(),
ids.iter().map(hex).collect::<Vec<_>>()
);
ensure!(
stats.objects == ids.len() as u64,
"PackStats reports {} objects and the pack carries {}",
stats.objects,
ids.len()
);
ensure!(
stats.copied + stats.recompressed == stats.objects,
"PackStats accounts for {} of {} entries as copied or recompressed; an entry whose base \
was left behind had to be rebuilt, and a receipt that does not say so is not a receipt",
stats.copied + stats.recompressed,
stats.objects
);
let mut out = Vec::new();
let stats = b.git.emit_pack(&[], &[], &caps, &mut out)?;
ensure!(
stats.objects == 0 && pack_ids(&out, hash_kind)?.is_empty(),
"an emission of nothing produced {} objects",
stats.objects
);
Ok(())
}
fn select_agrees_with_the_graph_or_declines<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "serve_select", hash_kind)?;
if let Some(set) = b.git.select(&[repo.commit2.as_slice()], &[])? {
let want = sorted(b.git.reachable(&[repo.commit2.as_slice()], &[])?);
ensure!(
sorted(set.objects.clone()) == want,
"select() answered {} objects for a clone and reachable() says {}; two answers to \
one question is worse than one slow answer",
set.objects.len(),
want.len()
);
ensure!(
set.commits.iter().all(|id| set.objects.contains(id)),
"select() reported a commit that is not in the set it is a commit of"
);
let commits = sorted(set.commits.clone());
ensure!(
commits == as_bytes(&[repo.commit1, repo.commit2]),
"select() called {} of the objects commits; the closure of the second commit \
contains exactly two",
commits.len()
);
ensure!(
set.client_has.is_empty(),
"select() vouched {} objects as already held by a client that said it had none",
set.client_has.len()
);
}
if let Some(set) = b
.git
.select(&[repo.commit2.as_slice()], &[repo.commit1.as_slice()])?
{
let want = sorted(
b.git
.reachable(&[repo.commit2.as_slice()], &[repo.commit1.as_slice()])?,
);
ensure!(
sorted(set.objects.clone()) == want,
"select() and reachable() disagree about an incremental fetch: {} against {}",
set.objects.len(),
want.len()
);
ensure!(
!set.client_has.contains(repo.commit2.as_slice()),
"select() vouched an object THIS transfer carries as one the client already had; a \
thin delta against it names a base the client does not have"
);
}
let stranger = absent_oid(hash_kind, 0xef);
if let Ok(Some(set)) = b.git.select(&[stranger.as_slice()], &[]) {
bail!(
"select() answered {} objects for a tip this store does not hold ({}). The escape \
hatch exists for exactly this: an engine that always answers has to answer wrongly \
somewhere, and a clone short by everything beneath a tip still exits zero",
set.objects.len(),
hex(&stranger)
);
}
Ok(())
}
fn pack_type_of(kind: Kind) -> ObjType {
match kind {
Kind::Commit => ObjType::Commit,
Kind::Tree => ObjType::Tree,
Kind::Blob => ObjType::Blob,
Kind::Tag => ObjType::Tag,
}
}
fn pack_ids(bytes: &[u8], hash_kind: gix_hash::Kind) -> anyhow::Result<Vec<ObjectId>> {
let raw = pack::parse(bytes, hash_kind)?;
let mut by_offset: std::collections::BTreeMap<u64, (Kind, Vec<u8>)> = Default::default();
let mut ids = Vec::with_capacity(raw.len());
for entry in &raw {
let (kind, content) = match entry.base_offset {
None => (
pack::kind_of(entry.obj_type).context("a base entry with a delta type")?,
entry.payload.clone(),
),
Some(offset) => {
let (kind, base) = by_offset.get(&offset).with_context(|| {
format!(
"the emitted pack has a delta at {} whose base at {offset} it does not \
carry, so no client can close it",
entry.extent.0
)
})?;
(*kind, pack::apply_delta(base, &entry.payload)?)
}
};
by_offset.insert(entry.extent.0, (kind, content.clone()));
ids.push(
gix_object::compute_hash(hash_kind, kind, &content).map_err(gix_error::Error::from)?,
);
}
Ok(ids)
}
fn gc_keeps_everything_a_ref_points_at<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let (b, repo) = loaded(harness, "gc", hash_kind)?;
b.git
.put_refs(&[RefUpdate::set("refs/heads/main", hex(&repo.commit2))])?;
let report = b.git.gc().context("gc failed")?;
ensure!(
!report.strategy.is_empty(),
"a GcReport with no strategy name cannot be attributed to an implementation"
);
for id in [
repo.commit2,
repo.commit1,
repo.tree2,
repo.tree1,
repo.blob,
repo.delta_blob,
] {
ensure!(
b.git.has(id.as_slice())?,
"gc dropped {}, which refs/heads/main still reaches. A repack that loses one tree \
deep inside history leaves every reference resolvable and every clone broken",
hex(&id)
);
}
let rows: Vec<String> = b.git.refs()?.into_iter().map(|r| r.name).collect();
ensure!(
rows == vec!["refs/heads/main".to_string()],
"gc changed the ref namespace: {rows:?}"
);
b.git.gc().context("a second gc failed")?;
ensure!(
b.git.has(repo.blob.as_slice())?,
"a second gc dropped an object the first one kept"
);
Ok(())
}
fn the_store_survives_a_restart<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let slot = "restart";
let repo = repo(hash_kind)?;
let stored = {
let b = harness.open(slot, hash_kind)?;
b.git.put(
&repo.fixture.bytes,
&[RefUpdate::set("refs/heads/main", hex(&repo.commit1))],
)?;
b.git
.get(repo.commit1.as_slice())?
.context("the object is not there before the restart")?
};
let b = harness.open(slot, hash_kind)?;
let after = b
.git
.get(repo.commit1.as_slice())?
.context("the object did not survive a restart")?;
ensure!(
after.bytes == stored.bytes,
"the object's bytes changed across a restart"
);
let row = b
.git
.refs()?
.into_iter()
.find(|r| r.name == "refs/heads/main")
.context("the reference did not survive a restart")?;
ensure!(
row.oid.as_deref() == Some(repo.commit1.as_slice()),
"the reference moved across a restart"
);
Ok(())
}
fn secret_put_get_delete<H: Harness>(harness: &H, hash_kind: gix_hash::Kind) -> anyhow::Result<()> {
let b = harness.open("secret_basic", hash_kind)?;
ensure!(
b.secrets.get("absent")?.is_none(),
"get() invented a secret"
);
b.secrets.put("k", b"v1")?;
ensure!(
b.secrets.get("k")? == Some(b"v1".to_vec()),
"put/get lost the value"
);
b.secrets.put("k", b"v2")?;
ensure!(
b.secrets.get("k")? == Some(b"v2".to_vec()),
"put did not overwrite"
);
b.secrets.delete("k")?;
ensure!(b.secrets.get("k")?.is_none(), "delete did not delete");
b.secrets.delete("k")?;
Ok(())
}
fn secret_get_or_create_survives_restart<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let slot = "secret_restart";
let calls = AtomicUsize::new(0);
let mint = || {
let n = calls.fetch_add(1, Ordering::SeqCst);
Ok(format!("ssh-ed25519-fake-key-{n}").into_bytes())
};
let first = {
let b = harness.open(slot, hash_kind)?;
b.secrets
.get_or_create_with(crate::secret::SSH_HOST_KEY, mint)?
};
ensure!(
calls.load(Ordering::SeqCst) == 1,
"the first get_or_create called mk {} times",
calls.load(Ordering::SeqCst)
);
let second = {
let b = harness.open(slot, hash_kind)?;
b.secrets
.get_or_create_with(crate::secret::SSH_HOST_KEY, mint)?
};
ensure!(
calls.load(Ordering::SeqCst) == 1,
"the host key was RE-MINTED after a restart (mk ran {} times), so every client \
would see a host-key-changed warning",
calls.load(Ordering::SeqCst)
);
ensure!(
first == second,
"the host key changed across a restart: {first:?} then {second:?}"
);
let third = {
let b = harness.open(slot, hash_kind)?;
b.secrets.get(crate::secret::SSH_HOST_KEY)?
};
ensure!(
third.as_deref() == Some(first.as_slice()),
"get() disagrees with get_or_create after a restart"
);
Ok(())
}
fn secret_get_or_create_is_atomic<H: Harness>(
harness: &H,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<()> {
let b = harness.open("secret_race", hash_kind)?;
let calls = AtomicUsize::new(0);
const THREADS: usize = 8;
let results = std::thread::scope(|scope| {
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let secrets = &b.secrets;
let calls = &calls;
scope.spawn(move || {
secrets.get_or_create_with(crate::secret::SSH_HOST_KEY, || {
let n = calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(5));
Ok(format!("host-key-{n}").into_bytes())
})
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("a get_or_create thread panicked"))
.collect::<Vec<_>>()
});
let values: Vec<Vec<u8>> = results.into_iter().collect::<crate::Result<Vec<_>>>()?;
ensure!(
calls.load(Ordering::SeqCst) == 1,
"{THREADS} concurrent get_or_create calls minted {} keys; the host key must \
never be regenerated by a race",
calls.load(Ordering::SeqCst)
);
let first = &values[0];
ensure!(
values.iter().all(|v| v == first),
"concurrent get_or_create callers got different bytes back"
);
Ok(())
}
pub use harness::*;
mod harness {
use super::{Backend, Harness};
use crate::mem::{MemGitServe, MemSecretStore};
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Default)]
pub struct MemHarness {
git: Mutex<HashMap<(String, gix_hash::Kind), MemGitServe>>,
secrets: Mutex<HashMap<(String, gix_hash::Kind), MemSecretStore>>,
}
impl MemHarness {
fn git_for(&self, slot: &str, hash_kind: gix_hash::Kind) -> MemGitServe {
self.git
.lock()
.expect("harness lock")
.entry((slot.to_owned(), hash_kind))
.or_insert_with(|| MemGitServe::new(hash_kind))
.clone()
}
fn secrets_for(&self, slot: &str, hash_kind: gix_hash::Kind) -> MemSecretStore {
self.secrets
.lock()
.expect("harness lock")
.entry((slot.to_owned(), hash_kind))
.or_default()
.clone()
}
}
impl Harness for MemHarness {
type Git = MemGitServe;
type Secrets = MemSecretStore;
fn name(&self) -> &str {
"mem"
}
fn open(
&self,
slot: &str,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<Backend<Self::Git, Self::Secrets>> {
Ok(Backend {
git: self.git_for(slot, hash_kind),
secrets: self.secrets_for(slot, hash_kind),
})
}
}
}
#[cfg(test)]
mod red;
#[cfg(test)]
mod green {
use super::*;
#[test]
fn the_control_conforms() {
let ran = assert_all_pass(&run_all(&MemHarness::default()));
assert_eq!(
ran,
check_names::<MemHarness>().len() * HASH_KINDS.len(),
"the runner did not run every check against every hash kind"
);
}
}