sigrefs/git/properties: Restore property tests

These tests fell under the radar when refactoring.
Restore them to the latest code so that these properties are tested.
This commit is contained in:
Fintan Halpenny 2026-04-28 09:49:54 +01:00 committed by Lorenz Leutgeb
parent 92347a307f
commit b482845e71
4 changed files with 140 additions and 55 deletions

View File

@ -28,9 +28,12 @@ pub use git2::{
AnnotatedCommit, Diff, DiffFindOptions, DiffOptions, DiffStats, MergeAnalysis, MergeOptions, AnnotatedCommit, Diff, DiffFindOptions, DiffOptions, DiffStats, MergeAnalysis, MergeOptions,
}; };
// Re-exports for `radicle-cli`.
pub mod build { pub mod build {
// Re-exports for `radicle-cli`.
pub use git2::build::CheckoutBuilder; pub use git2::build::CheckoutBuilder;
#[cfg(test)]
pub(crate) use git2::build::TreeUpdateBuilder;
} }
pub(crate) mod transport { pub(crate) mod transport {

View File

@ -5,6 +5,9 @@
pub mod object; pub mod object;
pub mod reference; pub mod reference;
#[cfg(test)]
mod properties;
use crypto::PublicKey; use crypto::PublicKey;
use radicle_git_metadata::author::Author; use radicle_git_metadata::author::Author;
use radicle_git_metadata::author::Time; use radicle_git_metadata::author::Time;

View File

@ -1,4 +1,7 @@
use crypto::{signature, test::signer::MockSigner, PublicKey, Signer as _}; use std::path::Path;
use crypto::test::signer::MockSigner;
use crypto::{PublicKey, Signer as _, signature};
use qcheck::TestResult; use qcheck::TestResult;
use qcheck_macros::quickcheck; use qcheck_macros::quickcheck;
use radicle_core::{NodeId, RepoId}; use radicle_core::{NodeId, RepoId};
@ -6,12 +9,16 @@ use radicle_git_metadata::author::{Author, Time};
use radicle_oid::Oid; use radicle_oid::Oid;
use tempfile::TempDir; use tempfile::TempDir;
use crate::storage::refs::sigrefs::{ use crate::git;
read::{CheckpointReason, Latest, SignedRefsReader, Tip}, use crate::storage::refs::sigrefs::VerifiedCommit;
write::{Committer, SignedRefsWriter, Update}, use crate::storage::refs::sigrefs::read::{SignedRefsReader, Tip};
}; use crate::storage::refs::sigrefs::write::{SignedRefsWriter, Update};
use crate::storage::refs::Refs; use crate::storage::refs::{IDENTITY_ROOT, Refs};
use super::Committer;
/// Newtype wrapper around [`Vec`] to keep the [`Arbitrary`] implementation
/// bounded to a smaller size.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct BoundedVec<T>(Vec<T>); struct BoundedVec<T>(Vec<T>);
@ -58,24 +65,69 @@ fn mock_committer() -> Committer {
Committer::new(mock_author()) Committer::new(mock_author())
} }
fn setup() -> (TempDir, git2::Repository) { /// A helper structure that sets up a minimal Radicle repository.
let dir = TempDir::new().unwrap(); struct Repository {
let repo = git2::Repository::init_bare(dir.path()).unwrap(); raw: git::raw::Repository,
(dir, repo) rid: RepoId,
root: Oid,
_tmp: TempDir,
} }
fn mock_root() -> RepoId { impl Repository {
RepoId::from(Oid::from_sha1([1; 20])) fn new() -> Repository {
let dir = TempDir::new().unwrap();
let repo = git::raw::Repository::init_bare(dir.path()).unwrap();
let (root, rid) = Self::write_identity(&repo);
Repository {
raw: repo,
rid,
root,
_tmp: dir,
}
}
/// Writes a mock blob that represents the identity document, and creates a
/// commit in the repository that contains this blob.
fn write_identity(repo: &git::raw::Repository) -> (Oid, RepoId) {
let blob = repo.blob(b"identity root").unwrap();
let empty = {
let empty = repo.treebuilder(None).unwrap();
let tree = empty.write().unwrap();
repo.find_tree(tree).unwrap()
};
let tree = {
let mut tb = git::raw::build::TreeUpdateBuilder::new();
tb.upsert(
Path::new("embeds").join(*crate::identity::doc::PATH),
blob,
git::raw::FileMode::Blob,
);
let tree = tb.create_updated(repo, &empty).unwrap();
repo.find_tree(tree).unwrap()
};
let author = git::raw::Signature::now("testy", "testy@example.com").unwrap();
let root = repo
.commit(
Some(IDENTITY_ROOT.as_str()),
&author,
&author,
"identity root",
&tree,
&[],
)
.unwrap();
(root.into(), RepoId::from(blob))
}
} }
fn write_log( fn write_log(
refs: Refs, refs: Refs,
rid: RepoId,
namespace: NodeId, namespace: NodeId,
signer: &MockSigner, signer: &MockSigner,
repo: &git2::Repository, repo: &git::raw::Repository,
) -> Update { ) -> Update {
SignedRefsWriter::new(namespace, signer, repo) SignedRefsWriter::new(refs, rid, namespace, repo, signer)
.with_refs(refs)
.write( .write(
mock_committer(), mock_committer(),
"test commit".to_string(), "test commit".to_string(),
@ -84,36 +136,42 @@ fn write_log(
.unwrap() .unwrap()
} }
fn read_log(namespace: NodeId, verifier: &Verifier, repo: &git2::Repository) -> Latest { fn read_log(
SignedRefsReader::new(mock_root(), Tip::Reference(namespace), repo, verifier) rid: RepoId,
namespace: NodeId,
verifier: &Verifier,
repo: &git::raw::Repository,
) -> VerifiedCommit {
SignedRefsReader::new(rid, Tip::Reference(namespace), repo, verifier)
.read() .read()
.unwrap() .unwrap()
.unwrap()
} }
#[quickcheck] #[quickcheck]
fn initial_commit_roundtrip(refs: Refs) -> bool { fn initial_commit_roundtrip(mut refs: Refs) -> bool {
let (_dir, repo) = setup(); let Repository {
raw: repo,
rid,
root,
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
let signer = MockSigner::default(); let signer = MockSigner::default();
let namespace = *signer.public_key(); let namespace = *signer.public_key();
let verifier = Verifier::new(&signer); let verifier = Verifier::new(&signer);
let update = write_log(refs.clone(), namespace, &signer, &repo); let update = write_log(refs.clone(), rid, namespace, &signer, &repo);
let head_oid = match update { let head_oid = match update {
Update::Changed { ref entry } => *entry.oid(), Update::Changed { ref entry, .. } => entry.oid(),
Update::Unchanged { .. } => return false, Update::Unchanged { .. } => return false,
}; };
let Latest { let verified_commit = read_log(rid, namespace, &verifier, &repo);
refs: expected, let head = *verified_commit.commit().oid();
checkpoint, let parent = verified_commit.commit().parent().copied();
.. let new_refs = verified_commit.into_sigrefs_at(namespace);
} = read_log(namespace, &verifier, &repo);
checkpoint.head() == head_oid head == head_oid && parent.is_none() && *new_refs == refs
&& checkpoint.ancestor() == head_oid
&& checkpoint.reason() == CheckpointReason::Root
&& expected == refs
} }
#[quickcheck] #[quickcheck]
@ -123,56 +181,72 @@ fn chain_roundtrip(chain: BoundedVec<Refs>) -> TestResult {
return TestResult::discard(); return TestResult::discard();
} }
let (_dir, repo) = setup(); let Repository {
raw: repo,
rid,
root,
_tmp,
} = Repository::new();
let signer = MockSigner::default(); let signer = MockSigner::default();
let namespace = *signer.public_key(); let namespace = *signer.public_key();
let verifier = Verifier::new(&signer); let verifier = Verifier::new(&signer);
let mut last_changed_head = None; let mut last_changed_head = None;
let mut expected_parent = None;
for refs in chain { for mut refs in chain {
let update = write_log(refs.clone(), namespace, &signer, &repo); refs.insert(IDENTITY_ROOT.to_ref_string(), root);
let update = write_log(refs.clone(), rid, namespace, &signer, &repo);
if let Update::Changed { ref entry } = update { if let Update::Changed { ref entry, .. } = update {
last_changed_head = Some(*entry.oid()); last_changed_head = Some(entry.oid());
} }
let Latest { let verified_commit = read_log(rid, namespace, &verifier, &repo);
refs: expected, let head = *verified_commit.commit().oid();
checkpoint, let parent = verified_commit.commit().parent().copied();
.. let new_refs = verified_commit.into_sigrefs_at(namespace);
} = read_log(namespace, &verifier, &repo);
if refs != expected { if refs != *new_refs {
return TestResult::failed();
}
if checkpoint.reason() != CheckpointReason::Root {
return TestResult::failed(); return TestResult::failed();
} }
if let Some(expected_head) = last_changed_head { if let Some(expected_head) = last_changed_head {
if checkpoint.head() != expected_head { if head != expected_head {
return TestResult::failed(); return TestResult::error(format!(
"expected commit to be {expected_head}, but found {head}"
));
}
if parent != expected_parent {
return TestResult::error(format!(
"expected parent commit to be {expected_parent:?}, but found {parent:?}"
));
} }
} }
expected_parent = Some(head);
} }
TestResult::passed() TestResult::passed()
} }
#[quickcheck] #[quickcheck]
fn idempotent_write(refs: Refs) -> bool { fn idempotent_write(mut refs: Refs) -> bool {
let (_dir, repo) = setup(); let Repository {
raw: repo,
rid,
root,
_tmp,
} = Repository::new();
refs.insert(IDENTITY_ROOT.to_ref_string(), root);
let signer = MockSigner::default(); let signer = MockSigner::default();
let namespace = *signer.public_key(); let namespace = *signer.public_key();
let first = write_log(refs.clone(), namespace, &signer, &repo); let first = write_log(refs.clone(), rid, namespace, &signer, &repo);
let head_oid = match first { let head_oid = match first {
Update::Changed { ref entry } => *entry.oid(), Update::Changed { ref entry, .. } => entry.oid(),
Update::Unchanged { .. } => return false, Update::Unchanged { .. } => return false,
}; };
let second = write_log(refs, namespace, &signer, &repo); let second = write_log(refs, rid, namespace, &signer, &repo);
matches!(second, Update::Unchanged { commit, .. } if commit == head_oid) matches!(second, Update::Unchanged { verified } if *verified.commit().oid() == head_oid)
} }

View File

@ -199,6 +199,11 @@ pub struct Commit {
} }
impl Commit { impl Commit {
#[cfg(test)]
pub(super) fn oid(&self) -> Oid {
self.oid
}
#[cfg(test)] #[cfg(test)]
pub(super) fn into_refs(self) -> Refs { pub(super) fn into_refs(self) -> Refs {
self.refs self.refs