Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage refs sigrefs property mock.rs
use qcheck::{Arbitrary, Gen};
use radicle_core::RepoId;
use radicle_git_metadata::author::{Author, Time};
use radicle_oid::Oid;
use tempfile::TempDir;

use crate::identity::doc;
use crate::storage::refs::sigrefs::git::Committer;
use crate::storage::refs::{IDENTITY_ROOT, Refs};

/// A `Vec<T>` whose [`Arbitrary`] instance caps the length at
/// [`Self::MAX_LEN`], preventing the property runner from generating inputs
/// that would make the test prohibitively slow.
#[derive(Clone, Debug)]
pub struct BoundedVec<T>(pub Vec<T>);

impl<T> BoundedVec<T> {
    const MAX_LEN: usize = 16;
}

impl<T: Arbitrary> Arbitrary for BoundedVec<T> {
    fn arbitrary(g: &mut Gen) -> Self {
        let len = usize::arbitrary(g) % (Self::MAX_LEN + 1);
        BoundedVec((0..len).map(|_| T::arbitrary(g)).collect())
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        let inner: Vec<Vec<T>> = self.0.shrink().collect();
        Box::new(inner.into_iter().map(BoundedVec))
    }
}

/// A Radicle Git repository fixture.
///
/// It is initialized in a [`TempDir`], and starts off with a single blob to
/// emulate the identity document.
pub struct Fixture {
    /// The underlying Git repository.
    repo: git2::Repository,
    /// The [`RepoId`] of the initial identity document blob.
    rid: RepoId,
    /// The commit that points to the identity document, which provides the
    /// [`RepoId`].
    identity_commit: Oid,
    _dir: TempDir,
}

impl Fixture {
    /// Initialise a bare git repository and write the minimal object graph
    /// required for identity-root verification to succeed:
    ///
    /// ```text
    /// identity-commit
    ///   └─ tree
    ///        └─ embeds/
    ///             └─ <doc::PATH>  (blob whose OID becomes the RepoId)
    /// ```
    pub fn new() -> Self {
        let dir = TempDir::new().unwrap();
        let repo = git2::Repository::init_bare(dir.path()).unwrap();

        let (identity_commit, rid) = Self::identity_commit(&repo);

        Self {
            _dir: dir,
            repo,
            rid,
            identity_commit,
        }
    }

    /// Return the [`RepoId`] of the fixture.
    pub fn rid(&self) -> RepoId {
        self.rid
    }

    /// Return the underlying Git repository of the fixture.
    pub fn repo(&self) -> &git2::Repository {
        &self.repo
    }

    /// Return a [`Committer`] with a fixed, stable [`Author`].
    pub fn committer(&self) -> Committer {
        Committer::new(Author {
            name: "radicle".to_string(),
            email: "radicle@test".to_string(),
            time: Time::new(0, 0),
        })
    }

    /// Patch an arbitrary [`Refs`] so that its [`IDENTITY_ROOT`] entry
    /// points at the fixture's identity commit, satisfying the
    /// `debug_assert` in [`SignedRefsWriter::new`] and the identity
    /// verification in [`crate::storage::refs::sigrefs::read`].
    pub fn with_identity_root(&self, mut refs: Refs) -> Refs {
        refs.insert(IDENTITY_ROOT.to_ref_string(), self.identity_commit);
        refs
    }

    fn identity_commit(repo: &git2::Repository) -> (Oid, RepoId) {
        let (doc_tree, rid) = Self::write_doc_blob(repo);
        let tree = Self::write_embeds(repo, doc_tree);
        let tree = repo.find_tree(tree).unwrap();
        let sig = git2::Signature::new("radicle", "radicle@test", &git2::Time::new(0, 0)).unwrap();
        let oid = repo
            .commit(None, &sig, &sig, "identity root", &tree, &[])
            .unwrap();
        (oid.into(), rid)
    }

    fn write_doc_blob(repo: &git2::Repository) -> (git2::Oid, RepoId) {
        let doc_blob_oid = repo.blob(b"identity").unwrap();
        let rid = RepoId::from(Oid::from(doc_blob_oid));

        let mut tb = repo.treebuilder(None).unwrap();
        tb.insert(
            doc::PATH.as_os_str(),
            doc_blob_oid,
            git2::FileMode::Blob.into(),
        )
        .unwrap();
        let oid = tb.write().unwrap();
        (oid, rid)
    }

    fn write_embeds(repo: &git2::Repository, doc: git2::Oid) -> git2::Oid {
        let mut tb = repo.treebuilder(None).unwrap();
        tb.insert("embeds", doc, git2::FileMode::Tree.into())
            .unwrap();
        tb.write().unwrap()
    }
}