Skip to content

Commit

Permalink
test: improve commit and submodule testing
Browse files Browse the repository at this point in the history
Two of the tests in radicle-git-ext and radicle-surf relied on using
the Git repository itself. This can end up being an issue when
sandboxing is involved.

This improves the situation by being able to generate commit data to
add to a temporary repository.

It also adds a helper for creating a local, temporary submodule for
testing the submodule interactions in the radicle-surf API.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
  • Loading branch information
FintanH committed Mar 13, 2024
1 parent f4a9580 commit b4ca7f3
Show file tree
Hide file tree
Showing 15 changed files with 461 additions and 77 deletions.
120 changes: 86 additions & 34 deletions radicle-git-ext/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,51 @@ use trailers::{OwnedTrailer, Trailer, Trailers};

use crate::author::Author;

pub type Commit = CommitData<Oid, Oid>;

impl Commit {
/// Read the [`Commit`] from the `repo` that is expected to be found at
/// `oid`.
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
let odb = repo.odb()?;
let object = odb.read(oid)?;
Ok(Commit::try_from(object.data())?)
}

/// Write the given [`Commit`] to the `repo`. The resulting `Oid`
/// is the identifier for this commit.
pub fn write(&self, repo: &git2::Repository) -> Result<Oid, error::Write> {
let odb = repo.odb().map_err(error::Write::Odb)?;
self.verify_for_write(&odb)?;
Ok(odb.write(ObjectType::Commit, self.to_string().as_bytes())?)
}

fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
for parent in &self.parents {
verify_object(odb, parent, ObjectType::Commit)?;
}
verify_object(odb, &self.tree, ObjectType::Tree)?;

Ok(())
}
}

/// A git commit in its object description form, i.e. the output of
/// `git cat-file` for a commit object.
#[derive(Debug)]
pub struct Commit {
tree: Oid,
parents: Vec<Oid>,
pub struct CommitData<Tree, Parent> {
tree: Tree,
parents: Vec<Parent>,
author: Author,
committer: Author,
headers: Headers,
message: String,
trailers: Vec<OwnedTrailer>,
}

impl Commit {
impl<Tree, Parent> CommitData<Tree, Parent> {
pub fn new<P, I, T>(
tree: Oid,
tree: Tree,
parents: P,
author: Author,
committer: Author,
Expand All @@ -50,7 +79,7 @@ impl Commit {
trailers: I,
) -> Self
where
P: IntoIterator<Item = Oid>,
P: IntoIterator<Item = Parent>,
I: IntoIterator<Item = T>,
OwnedTrailer: From<T>,
{
Expand All @@ -67,30 +96,17 @@ impl Commit {
}
}

/// Read the [`Commit`] from the `repo` that is expected to be found at
/// `oid`.
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
let odb = repo.odb()?;
let object = odb.read(oid)?;
Ok(Commit::try_from(object.data())?)
}

/// Write the given [`Commit`] to the `repo`. The resulting `Oid`
/// is the identifier for this commit.
pub fn write(&self, repo: &git2::Repository) -> Result<Oid, error::Write> {
let odb = repo.odb().map_err(error::Write::Odb)?;
self.verify_for_write(&odb)?;
Ok(odb.write(ObjectType::Commit, self.to_string().as_bytes())?)
}

/// The tree [`Oid`] this commit points to.
pub fn tree(&self) -> Oid {
self.tree
/// The tree this commit points to.
pub fn tree(&self) -> &Tree {
&self.tree
}

/// The parent [`Oid`]s of this commit.
pub fn parents(&self) -> impl Iterator<Item = Oid> + '_ {
self.parents.iter().copied()
/// The parents of this commit.
pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
where
Parent: Clone,
{
self.parents.iter().cloned()
}

/// The author of this commit, i.e. the header corresponding to `author`.
Expand Down Expand Up @@ -136,13 +152,49 @@ impl Commit {
self.trailers.iter()
}

fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
for parent in &self.parents {
verify_object(odb, parent, ObjectType::Commit)?;
}
verify_object(odb, &self.tree, ObjectType::Tree)?;
/// Convert the `CommitData::tree` into a value of type `U`. The
/// conversion function `f` can be fallible.
///
/// For example, `map_tree` can be used to turn raw tree data into
/// an `Oid` by writing it to a repository.
pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
where
F: FnOnce(Tree) -> Result<U, E>,
{
Ok(CommitData {
tree: f(self.tree)?,
parents: self.parents,
author: self.author,
committer: self.committer,
headers: self.headers,
message: self.message,
trailers: self.trailers,
})
}

Ok(())
/// Convert the `CommitData::parents` into a vector containing
/// values of type `U`. The conversion function `f` can be
/// fallible.
///
/// For example, `map_parents` can be used to resolve the `Oid`s
/// to their respective `git2::Commit`s.
pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
where
F: FnMut(Parent) -> Result<U, E>,
{
Ok(CommitData {
tree: self.tree,
parents: self
.parents
.into_iter()
.map(f)
.collect::<Result<Vec<_>, _>>()?,
author: self.author,
committer: self.committer,
headers: self.headers,
message: self.message,
trailers: self.trailers,
})
}
}

Expand Down
1 change: 1 addition & 0 deletions radicle-git-ext/src/commit/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";
pub struct Headers(pub(super) Vec<(String, String)>);

/// A `gpgsig` signature stored in a [`crate::commit::Commit`].
#[derive(Debug)]
pub enum Signature<'a> {
/// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
Pgp(Cow<'a, str>),
Expand Down
2 changes: 1 addition & 1 deletion radicle-git-ext/t/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ features = ["vendored-libgit2"]
path = ".."
features = ["serde", "minicbor"]

[dev-dependencies.test-helpers]
[dependencies.test-helpers]
path = "../../test/test-helpers"
44 changes: 22 additions & 22 deletions radicle-git-ext/t/src/commit.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::{io, str::FromStr as _, string::ToString as _};

use proptest::proptest;
use radicle_git_ext::{
author::{self, Author},
commit::{headers::Headers, trailers::OwnedTrailer, Commit},
};
use test_helpers::tempdir::WithTmpDir;
use test_helpers::tempdir::{self, WithTmpDir};

use crate::gen;

const NO_TRAILER: &str = "\
tree 50d6ef440728217febf9e35716d8b0296608d7f8
Expand Down Expand Up @@ -152,27 +155,23 @@ fn test_conversion() {
assert_eq!(Commit::from_str(UNSIGNED).unwrap().to_string(), UNSIGNED);
}

#[test]
fn valid_commits() {
let radicle_git = format!(
"file://{}",
git2::Repository::discover(".").unwrap().path().display()
);
let repo = WithTmpDir::new(|path| {
let repo = git2::Repository::clone(&radicle_git, path)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
Ok::<_, io::Error>(repo)
})
.unwrap();

let mut walk = repo.revwalk().unwrap();
walk.push_head().unwrap();

// take the first 20 commits and make sure we can parse them
for oid in walk.take(20) {
let oid = oid.unwrap();
let commit = Commit::read(&repo, oid);
assert!(commit.is_ok(), "Oid: {oid}, Error: {commit:?}")
proptest! {
#[test]
fn valid_commits(commits in proptest::collection::vec(gen::commit::commit(), 5..20)) {
let repo = tempdir::WithTmpDir::new(|path| {
git2::Repository::init(path).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}).unwrap();
let commits = gen::commit::write_commits(&repo, commits).unwrap();
repo.reference("refs/heads/master", *commits.last().unwrap(), true, "").unwrap();

let mut walk = repo.revwalk().unwrap();
walk.push_head().unwrap();

for oid in walk.take(20) {
let oid = oid.unwrap();
let commit = Commit::read(&repo, oid);
assert!(commit.is_ok(), "Oid: {oid}, Error: {commit:?}");
}
}
}

Expand All @@ -182,6 +181,7 @@ fn write_valid_commit() {
git2::Repository::init(path).map_err(|err| io::Error::new(io::ErrorKind::Other, err))
})
.unwrap();

let author = Author {
name: "Terry".to_owned(),
email: "terry.pratchett@proton.mail".to_owned(),
Expand Down
11 changes: 11 additions & 0 deletions radicle-git-ext/t/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@

//! Provides proptest generators

use proptest::strategy::Strategy;

pub mod commit;
pub mod urn;

pub fn alphanumeric() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_]+"
}

pub fn alpha() -> impl Strategy<Value = String> {
"[a-zA-Z]+"
}
94 changes: 94 additions & 0 deletions radicle-git-ext/t/src/gen/commit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::convert::Infallible;

use proptest::strategy::Strategy;
use radicle_git_ext::commit::{self, CommitData};

mod author;
mod headers;
mod trailers;

pub use author::author;
pub use headers::headers;
pub use trailers::{trailer, trailers};

use super::alphanumeric;

pub fn commit() -> impl Strategy<Value = CommitData<TreeData, Infallible>> {
(
TreeData::gen(),
author(),
author(),
headers(),
alphanumeric(),
trailers(3),
)
.prop_map(|(tree, author, committer, headers, message, trailers)| {
CommitData::new(tree, vec![], author, committer, headers, message, trailers)
})
}

pub fn write_commits(
repo: &git2::Repository,
linear: Vec<CommitData<TreeData, Infallible>>,
) -> Result<Vec<git2::Oid>, commit::error::Write> {
let mut parent = None;
let mut commits = Vec::new();
for commit in linear {
let commit = commit.map_tree(|tree| tree.write(repo))?;
let commit = match parent {
Some(parent) => commit
.map_parents::<git2::Oid, Infallible, _>(|_| Ok(parent))
.unwrap(),
None => commit
.map_parents::<git2::Oid, Infallible, _>(|_| unreachable!("no parents"))
.unwrap(),
};
let oid = commit.write(repo)?;
commits.push(oid);
parent = Some(oid);
}
Ok(commits)
}

#[derive(Clone, Debug)]
pub enum TreeData {
Blob { name: String, data: String },
Tree { name: String, inner: Vec<TreeData> },
}

impl TreeData {
fn gen() -> impl Strategy<Value = Self> {
let leaf =
(alphanumeric(), alphanumeric()).prop_map(|(name, data)| Self::Blob { name, data });
leaf.prop_recursive(8, 16, 5, |inner| {
(proptest::collection::vec(inner, 1..5), alphanumeric())
.prop_map(|(inner, name)| Self::Tree { name, inner })
})
}

fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
let mut builder = repo.treebuilder(None)?;
self.write_(repo, &mut builder)?;
builder.write()
}

fn write_(
&self,
repo: &git2::Repository,
builder: &mut git2::TreeBuilder,
) -> Result<git2::Oid, git2::Error> {
match self {
Self::Blob { name, data } => {
let oid = repo.blob(data.as_bytes())?;
builder.insert(name, oid, git2::FileMode::Blob.into())?;
}
Self::Tree { name, inner } => {
for data in inner {
let oid = data.write_(repo, builder)?;
builder.insert(name, oid, git2::FileMode::Tree.into())?;
}
}
}
builder.write()
}
}
19 changes: 19 additions & 0 deletions radicle-git-ext/t/src/gen/commit/author.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use proptest::strategy::{Just, Strategy};
use radicle_git_ext::author::{Author, Time};

use crate::gen;

pub fn author() -> impl Strategy<Value = Author> {
gen::alphanumeric().prop_flat_map(move |name| {
(Just(name), gen::alphanumeric()).prop_flat_map(|(name, domain)| {
(Just(name), Just(domain), (0..1000i64)).prop_map(move |(name, domain, time)| {
let email = format!("{name}@{domain}");
Author {
name,
email,
time: Time::new(time, 0),
}
})
})
})
}
Loading

0 comments on commit b4ca7f3

Please sign in to comment.