use std::collections::BTreeSet;
use git_storage_trait::{
Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
RefUpdate, Stored, TxId,
};
use super::{run_all, Backend, Harness, Outcome};
use crate::error::Result;
use crate::secret::SecretStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Break {
None,
Cas,
UnderSend,
StoredSize,
Truncate,
Regenerate,
HeadIsARow,
PackCloses,
TagStopsAtItself,
TwoValueCount,
}
struct BrokenGit<G> {
inner: G,
brk: Break,
}
impl<G: GitServe> GitOps for BrokenGit<G> {
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_pack_observed(
&self,
bytes: &[u8],
progress: &dyn git_storage_trait::PackProgress,
) -> anyhow::Result<TxId> {
if self.brk != Break::TwoValueCount {
return self.inner.put_pack_observed(bytes, progress);
}
let total = u64::from(u32::from_be_bytes([
bytes[8], bytes[9], bytes[10], bytes[11],
]));
progress.indexed(0, total);
let tx = self.inner.put_pack(bytes)?;
progress.indexed(total, total);
Ok(tx)
}
fn publishes_pack_progress(&self) -> bool {
self.brk == Break::TwoValueCount || self.inner.publishes_pack_progress()
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
let Some(mut stored) = self.inner.get(oid)? else {
return Ok(None);
};
match self.brk {
Break::Truncate => {
stored.bytes.pop();
}
Break::StoredSize => stored.uncompressed_size = stored.bytes.len() as u64,
_ => {}
}
Ok(Some(stored))
}
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.inner.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
if self.brk == Break::StoredSize {
return Ok(self.inner.get(oid)?.map(|s| s.bytes.len() as 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>> {
let mut rows = self.inner.refs()?;
if self.brk == Break::HeadIsARow {
if let Some(head) = self.inner.head()? {
rows.push(head);
rows.sort_by(|a, b| a.name.cmp(&b.name));
}
}
Ok(rows)
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
if self.brk == Break::Cas {
let current = self
.inner
.refs()?
.into_iter()
.find(|r| r.name == name)
.and_then(|r| r.oid);
return self.inner.update_ref(name, current.as_deref(), new);
}
self.inner.update_ref(name, old, new)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
if self.brk == Break::UnderSend {
let tips: BTreeSet<Vec<u8>> = want.iter().map(|id| id.to_vec()).collect();
return Ok(tips.into_iter().collect());
}
if self.brk == Break::TagStopsAtItself {
let mut commits: Vec<Oid<'_>> = Vec::new();
let mut out: BTreeSet<Vec<u8>> = BTreeSet::new();
for id in want {
match self.inner.header(id)? {
Some((ObjType::Commit, _)) => commits.push(id),
_ => {
out.insert(id.to_vec());
}
}
}
out.extend(self.inner.reachable(&commits, have)?);
return Ok(out.into_iter().collect());
}
self.inner.reachable(want, have)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
if self.brk == Break::Cas {
let held = self.inner.refs()?;
let current: Vec<Option<Vec<u8>>> = edits
.iter()
.map(|e| {
held.iter()
.find(|r| r.name == e.name)
.and_then(|r| r.oid.clone())
})
.collect();
let rewritten: Vec<RefCas<'_>> = edits
.iter()
.zip(¤t)
.map(|(e, current)| RefCas {
name: e.name.clone(),
old: current.as_deref(),
new: e.new,
peeled: e.peeled.clone(),
})
.collect();
return self.inner.put_refs_cas(&rewritten);
}
self.inner.put_refs_cas(edits)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.inner.gc()
}
}
impl<G: GitServe> GitServe for BrokenGit<G> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.inner.read(oid)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.inner.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.inner.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
if self.brk == Break::HeadIsARow {
return Ok(None);
}
self.inner.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.inner.set_head(target)
}
fn emit_pack(
&self,
objects: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
if self.brk == Break::PackCloses {
let closure = self.inner.reachable(objects, have)?;
let refs: Vec<Oid<'_>> = closure.iter().map(|id| id.as_slice()).collect();
return self.inner.emit_pack(&refs, have, caps, out);
}
self.inner.emit_pack(objects, have, caps, out)
}
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
self.inner.select(want, have)
}
}
struct BrokenSecrets<S> {
inner: S,
brk: Break,
}
impl<S: SecretStore> SecretStore for BrokenSecrets<S> {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
self.inner.get(key)
}
fn put(&self, key: &str, value: &[u8]) -> Result<()> {
self.inner.put(key, value)
}
fn delete(&self, key: &str) -> Result<()> {
self.inner.delete(key)
}
fn get_or_create(&self, key: &str, mk: &mut dyn FnMut() -> Result<Vec<u8>>) -> Result<Vec<u8>> {
if self.brk == Break::Regenerate {
let value = mk()?;
self.inner.put(key, &value)?;
return Ok(value);
}
self.inner.get_or_create(key, mk)
}
}
struct BrokenHarness<H> {
inner: H,
brk: Break,
}
impl<H: Harness> Harness for BrokenHarness<H> {
type Git = BrokenGit<H::Git>;
type Secrets = BrokenSecrets<H::Secrets>;
fn name(&self) -> &str {
self.inner.name()
}
fn open(
&self,
slot: &str,
hash_kind: gix_hash::Kind,
) -> anyhow::Result<Backend<Self::Git, Self::Secrets>> {
let backend = self.inner.open(slot, hash_kind)?;
Ok(Backend {
git: BrokenGit {
inner: backend.git,
brk: self.brk,
},
secrets: BrokenSecrets {
inner: backend.secrets,
brk: self.brk,
},
})
}
}
fn red_checks<H: Harness>(inner: H, brk: Break) -> (BTreeSet<&'static str>, Vec<Outcome>) {
let outcomes = run_all(&BrokenHarness { inner, brk });
let failed = outcomes
.iter()
.filter(|o| !o.passed())
.map(|o| o.check)
.collect();
(failed, outcomes)
}
#[track_caller]
fn assert_red(
backend: &str,
failed: &BTreeSet<&'static str>,
must_fail: &[&str],
must_pass: &[&str],
) {
for check in must_fail {
assert!(
failed.contains(check),
"[{backend}] the conformance suite did NOT catch a deliberately broken backend: \
`{check}` passed when it had to fail. Failing checks were {failed:?}"
);
}
for check in must_pass {
assert!(
!failed.contains(check),
"[{backend}] `{check}` failed against a backend that is not broken in that way. \
the suite is failing indiscriminately, which is its own kind of blindness. \
Failing checks were {failed:?}"
);
}
}
const CAS_MUST_FAIL: &[&str] = &[
"update_ref_creates_only_when_absent",
"update_ref_is_a_real_compare_and_swap",
"update_ref_delete_checks_its_old_value",
"put_refs_cas_applies_every_edit_or_none",
];
const CAS_MUST_PASS: &[&str] = &[
"pack_roundtrip_every_entry",
"refs_round_trip",
"reachable_returns_the_whole_closure",
"secret_get_or_create_survives_restart",
];
#[test]
fn the_wrappers_themselves_are_transparent() {
let (failed, outcomes) = red_checks(super::MemHarness::default(), Break::None);
assert!(
failed.is_empty(),
"the pass-through broken-backend wrapper does not itself conform: {failed:?}"
);
assert!(!outcomes.is_empty(), "the suite ran nothing");
}
#[test]
fn the_suite_catches_a_broken_compare_and_swap() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::Cas);
assert_red("mem", &failed, CAS_MUST_FAIL, CAS_MUST_PASS);
}
#[test]
fn the_suite_catches_a_reachability_index_that_under_sends() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::UnderSend);
assert_red(
"mem",
&failed,
&[
"reachable_returns_the_whole_closure",
"reachable_subtracts_what_the_client_has",
"reachable_tolerates_a_have_it_does_not_know",
"reachable_does_not_follow_a_submodule",
],
&[
"pack_roundtrip_every_entry",
"get_returns_the_verbatim_entry_bytes",
"update_ref_is_a_real_compare_and_swap",
"gc_keeps_everything_a_ref_points_at",
"the_store_survives_a_restart",
"secret_put_get_delete",
],
);
}
#[test]
fn the_suite_catches_a_reachability_that_stops_at_a_tag() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::TagStopsAtItself);
assert_red(
"mem",
&failed,
&["reachable_follows_an_annotated_tag_to_its_target"],
&[
"reachable_returns_the_whole_closure",
"reachable_subtracts_what_the_client_has",
"reachable_does_not_follow_a_submodule",
"pack_roundtrip_every_entry",
"update_ref_is_a_real_compare_and_swap",
"gc_keeps_everything_a_ref_points_at",
"the_store_survives_a_restart",
],
);
}
#[test]
fn the_suite_catches_a_size_that_is_not_post_resolution() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::StoredSize);
assert_red(
"mem",
&failed,
&["size_is_post_resolution_not_stored_length"],
&[
"get_returns_the_verbatim_entry_bytes",
"update_ref_is_a_real_compare_and_swap",
"reachable_returns_the_whole_closure",
"secret_put_get_delete",
],
);
}
#[test]
fn the_suite_catches_a_store_that_loses_bytes() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::Truncate);
assert_red(
"mem",
&failed,
&["get_returns_the_verbatim_entry_bytes"],
&[
"update_ref_is_a_real_compare_and_swap",
"reachable_returns_the_whole_closure",
"secret_put_get_delete",
],
);
}
#[test]
fn the_suite_catches_a_backend_that_calls_head_a_row() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::HeadIsARow);
assert_red(
"mem",
&failed,
&["head_is_not_a_row_and_has_its_own_accessor"],
&[
"refs_round_trip",
"update_ref_is_a_real_compare_and_swap",
"pack_roundtrip_every_entry",
"reachable_returns_the_whole_closure",
"emit_pack_emits_the_set_it_was_given",
],
);
}
#[test]
fn the_suite_catches_an_emit_pack_that_closes_over_its_input() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::PackCloses);
assert_red(
"mem",
&failed,
&["emit_pack_emits_the_set_it_was_given"],
&[
"read_is_inflated_and_delta_resolved",
"header_and_sizes_agree_with_read",
"select_agrees_with_the_graph_or_declines",
"reachable_returns_the_whole_closure",
"pack_roundtrip_every_entry",
"head_is_not_a_row_and_has_its_own_accessor",
],
);
}
#[test]
fn the_suite_catches_a_count_that_only_has_two_values() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::TwoValueCount);
assert_red(
"mem",
&failed,
&["put_pack_observed_publishes_a_count_that_moves"],
&[
"put_pack_is_idempotent",
"pack_roundtrip_every_entry",
"get_returns_the_verbatim_entry_bytes",
"update_ref_is_a_real_compare_and_swap",
"reachable_returns_the_whole_closure",
"the_store_survives_a_restart",
],
);
}
#[test]
fn the_suite_catches_a_regenerated_host_key() {
let (failed, _) = red_checks(super::MemHarness::default(), Break::Regenerate);
assert_red(
"mem",
&failed,
&[
"secret_get_or_create_survives_restart",
"secret_get_or_create_is_atomic",
],
&[
"secret_put_get_delete",
"update_ref_is_a_real_compare_and_swap",
"pack_roundtrip_every_entry",
],
);
}