radicle: port quorum code to Canonical
Remove the old `quorum` function in favour of using the newly introduced `Canonical` code. Port over the tests to use the `Canonical` type. The change is minimised by introducing a helper function called `quorum` in the test module that acts like the old function. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
7fd9c3be8f
commit
54d23545ff
|
|
@ -105,7 +105,7 @@ pub enum Error {
|
||||||
Repository(#[from] radicle::storage::RepositoryError),
|
Repository(#[from] radicle::storage::RepositoryError),
|
||||||
/// Quorum error.
|
/// Quorum error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] radicle::storage::git::QuorumError),
|
Quorum(#[from] radicle::git::canonical::QuorumError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push command.
|
/// Push command.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ pub struct Canonical {
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum QuorumError {
|
pub enum QuorumError {
|
||||||
/// Could not determine a quorum [`Oid`].
|
/// Could not determine a quorum [`Oid`].
|
||||||
#[error("no quorum was found")]
|
#[error("no quorum was found")]
|
||||||
NoQuorum,
|
NoQuorum,
|
||||||
/// An error occurred from [`git2`].
|
/// An error occurred from [`git2`].
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
@ -69,7 +69,12 @@ impl Canonical {
|
||||||
Ok(tip) => {
|
Ok(tip) => {
|
||||||
tips.insert(*delegate, tip);
|
tips.insert(*delegate, tip);
|
||||||
}
|
}
|
||||||
Err(e) if super::ext::is_not_found_err(&e) => {}
|
Err(e) if super::ext::is_not_found_err(&e) => {
|
||||||
|
log::warn!(
|
||||||
|
target: "radicle",
|
||||||
|
"Missing `refs/namespaces/{delegate}/{name}` while calculating the canonical reference"
|
||||||
|
);
|
||||||
|
}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,3 +190,270 @@ impl Canonical {
|
||||||
Ok((*longest).into())
|
Ok((*longest).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use crypto::test::signer::MockSigner;
|
||||||
|
use radicle_crypto::Signer;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::assert_matches;
|
||||||
|
use crate::git;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
/// Test helper to construct a Canonical and get the quorum
|
||||||
|
fn quorum(
|
||||||
|
heads: &[git::raw::Oid],
|
||||||
|
threshold: usize,
|
||||||
|
repo: &git::raw::Repository,
|
||||||
|
) -> Result<Oid, QuorumError> {
|
||||||
|
let tips = heads
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, head)| {
|
||||||
|
let signer = MockSigner::from_seed([(i + 1) as u8; 32]);
|
||||||
|
let did = Did::from(signer.public_key());
|
||||||
|
(did, (*head).into())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Canonical { tips }.quorum(threshold, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quorum_properties() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (repo, c0) = fixtures::repository(tmp.path());
|
||||||
|
let c0: git::Oid = c0.into();
|
||||||
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
||||||
|
let a2 = fixtures::commit("A2", &[*a1], &repo);
|
||||||
|
let d1 = fixtures::commit("D1", &[*c0], &repo);
|
||||||
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
||||||
|
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
||||||
|
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
||||||
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
||||||
|
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
||||||
|
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
||||||
|
let mut rng = fastrand::Rng::new();
|
||||||
|
let choices = [*c0, *c1, *c2, *b2, *a1, *a2, *d1, *m1, *m2];
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
let count = rng.usize(1..=choices.len());
|
||||||
|
let threshold = rng.usize(1..=count);
|
||||||
|
let mut heads = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..count {
|
||||||
|
let ix = rng.usize(0..choices.len());
|
||||||
|
heads.push(choices[ix]);
|
||||||
|
}
|
||||||
|
rng.shuffle(&mut heads);
|
||||||
|
|
||||||
|
if let Ok(canonical) = quorum(&heads, threshold, &repo) {
|
||||||
|
assert!(heads.contains(&canonical));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quorum() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (repo, c0) = fixtures::repository(tmp.path());
|
||||||
|
let c0: git::Oid = c0.into();
|
||||||
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
||||||
|
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
||||||
|
let c3 = fixtures::commit("C3", &[*c1], &repo);
|
||||||
|
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
||||||
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
||||||
|
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
||||||
|
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
||||||
|
|
||||||
|
eprintln!("C0: {c0}");
|
||||||
|
eprintln!("C1: {c1}");
|
||||||
|
eprintln!("C2: {c2}");
|
||||||
|
eprintln!("C3: {c3}");
|
||||||
|
eprintln!("B2: {b2}");
|
||||||
|
eprintln!("A1: {a1}");
|
||||||
|
eprintln!("M1: {m1}");
|
||||||
|
eprintln!("M2: {m2}");
|
||||||
|
|
||||||
|
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
||||||
|
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
|
||||||
|
assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
|
||||||
|
assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
|
||||||
|
// C1
|
||||||
|
// |
|
||||||
|
// C0
|
||||||
|
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
||||||
|
|
||||||
|
// C2
|
||||||
|
// |
|
||||||
|
// C1
|
||||||
|
// |
|
||||||
|
// C0
|
||||||
|
assert_eq!(quorum(&[*c1, *c2], 1, &repo).unwrap(), c2);
|
||||||
|
assert_eq!(quorum(&[*c1, *c2], 2, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*c0, *c1, *c2], 3, &repo).unwrap(), c0);
|
||||||
|
assert_eq!(quorum(&[*c1, *c1, *c2], 2, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*c1, *c1, *c2], 1, &repo).unwrap(), c2);
|
||||||
|
assert_eq!(quorum(&[*c2, *c2, *c1], 1, &repo).unwrap(), c2);
|
||||||
|
|
||||||
|
// B2 C2
|
||||||
|
// \|
|
||||||
|
// C1
|
||||||
|
// |
|
||||||
|
// C0
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c1, *c2, *b2], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(quorum(&[*c2, *b2], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*b2, *c2], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*c2, *b2], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*b2, *c2], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*b2, *b2, *c2], 2, &repo).unwrap(), b2);
|
||||||
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
|
||||||
|
// B2 C2 C3
|
||||||
|
// \ | /
|
||||||
|
// C1
|
||||||
|
// |
|
||||||
|
// C0
|
||||||
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*b2, *c2, *c2], 3, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
|
||||||
|
// B2 C2
|
||||||
|
// \|
|
||||||
|
// A1 C1
|
||||||
|
// \|
|
||||||
|
// C0
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c2, *b2, *a1], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c2, *b2, *a1], 2, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c2, *b2, *a1], 3, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
|
||||||
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
|
||||||
|
assert_eq!(quorum(&[*c0, *c2, *b2, *a1], 3, &repo).unwrap(), c0);
|
||||||
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*a1, *a1, *c2], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
|
||||||
|
// M2 M1
|
||||||
|
// /\ /\
|
||||||
|
// \ B2 C2
|
||||||
|
// \ \|
|
||||||
|
// A1 C1
|
||||||
|
// \|
|
||||||
|
// C0
|
||||||
|
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
||||||
|
assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m2, *m1], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*m1, *m2, *c2], 1, &repo),
|
||||||
|
Err(QuorumError::NoQuorum)
|
||||||
|
);
|
||||||
|
assert_matches!(quorum(&[*m1, *a1], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m1, *a1], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
|
||||||
|
assert_eq!(quorum(&[*m1, *m1, *c2], 2, &repo).unwrap(), m1);
|
||||||
|
assert_eq!(quorum(&[*m2, *m2, *b2], 2, &repo).unwrap(), m2);
|
||||||
|
assert_eq!(quorum(&[*m2, *m2, *a1], 2, &repo).unwrap(), m2);
|
||||||
|
assert_eq!(quorum(&[*m1, *m1, *b2, *b2], 2, &repo).unwrap(), m1);
|
||||||
|
assert_eq!(quorum(&[*m1, *m1, *c2, *c2], 2, &repo).unwrap(), m1);
|
||||||
|
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 3, &repo).unwrap(), c1);
|
||||||
|
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 4, &repo).unwrap(), c0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quorum_merges() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (repo, c0) = fixtures::repository(tmp.path());
|
||||||
|
let c0: git::Oid = c0.into();
|
||||||
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
||||||
|
let c2 = fixtures::commit("C2", &[*c0], &repo);
|
||||||
|
let c3 = fixtures::commit("C3", &[*c0], &repo);
|
||||||
|
|
||||||
|
let m1 = fixtures::commit("M1", &[*c1, *c2], &repo);
|
||||||
|
let m2 = fixtures::commit("M2", &[*c2, *c3], &repo);
|
||||||
|
|
||||||
|
eprintln!("C0: {c0}");
|
||||||
|
eprintln!("C1: {c1}");
|
||||||
|
eprintln!("C2: {c2}");
|
||||||
|
eprintln!("C3: {c3}");
|
||||||
|
eprintln!("M1: {m1}");
|
||||||
|
eprintln!("M2: {m2}");
|
||||||
|
|
||||||
|
// M2 M1
|
||||||
|
// /\ /\
|
||||||
|
// C1 C2 C3
|
||||||
|
// \| /
|
||||||
|
// C0
|
||||||
|
assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
|
||||||
|
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
||||||
|
|
||||||
|
// M3/M2 M1
|
||||||
|
// /\ /\
|
||||||
|
// C1 C2 C3
|
||||||
|
// \| /
|
||||||
|
// C0
|
||||||
|
assert_matches!(quorum(&[*m1, *m3], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m1, *m3], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m3, *m1], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m3, *m1], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m3, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
assert_matches!(quorum(&[*m3, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub use radicle_git_ext::Oid;
|
||||||
|
|
||||||
use crate::cob;
|
use crate::cob;
|
||||||
use crate::collections::RandomMap;
|
use crate::collections::RandomMap;
|
||||||
use crate::git::ext as git_ext;
|
use crate::git::{canonical, ext as git_ext};
|
||||||
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
|
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
|
||||||
use crate::identity::{Did, PayloadError};
|
use crate::identity::{Did, PayloadError};
|
||||||
use crate::identity::{Doc, DocAt, DocError};
|
use crate::identity::{Doc, DocAt, DocError};
|
||||||
|
|
@ -116,7 +116,7 @@ pub enum RepositoryError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GitExt(#[from] git_ext::Error),
|
GitExt(#[from] git_ext::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] git::QuorumError),
|
Quorum(#[from] canonical::QuorumError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Refs(#[from] refs::Error),
|
Refs(#[from] refs::Error),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use once_cell::sync::Lazy;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::crypto::Unverified;
|
use crate::crypto::Unverified;
|
||||||
|
use crate::git::canonical::Canonical;
|
||||||
use crate::identity::doc::DocError;
|
use crate::identity::doc::DocError;
|
||||||
use crate::identity::{doc::DocAt, Doc, RepoId};
|
use crate::identity::{doc::DocAt, Doc, RepoId};
|
||||||
use crate::identity::{Identity, Project};
|
use crate::identity::{Identity, Project};
|
||||||
|
|
@ -743,25 +744,8 @@ impl ReadRepository for Repository {
|
||||||
let project = doc.project()?;
|
let project = doc.project()?;
|
||||||
let branch_ref = git::refs::branch(project.default_branch());
|
let branch_ref = git::refs::branch(project.default_branch());
|
||||||
let raw = self.raw();
|
let raw = self.raw();
|
||||||
let mut heads = Vec::new();
|
let oid = Canonical::default_branch(self, &project, &doc.delegates)?
|
||||||
|
.quorum(doc.threshold, raw)?;
|
||||||
for delegate in doc.delegates.iter() {
|
|
||||||
let r = match self.reference_oid(delegate, &branch_ref) {
|
|
||||||
Ok(oid) => oid,
|
|
||||||
Err(e) if ext::is_not_found_err(&e) => {
|
|
||||||
log::warn!(
|
|
||||||
target: "radicle",
|
|
||||||
"Missing `refs/namespaces/{delegate}/{branch_ref}` while calculating the canonical head"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
heads.push(*r);
|
|
||||||
}
|
|
||||||
|
|
||||||
let oid = self::quorum(&heads, doc.threshold, raw)?;
|
|
||||||
Ok((branch_ref, oid))
|
Ok((branch_ref, oid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -896,96 +880,6 @@ impl SignRepository for Repository {
|
||||||
Ok(signed)
|
Ok(signed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum QuorumError {
|
|
||||||
#[error("no quorum was found")]
|
|
||||||
NoQuorum,
|
|
||||||
#[error(transparent)]
|
|
||||||
Git(#[from] git2::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes the quorum or "canonical" head based on the given heads and the
|
|
||||||
/// threshold. This can be described as the latest commit that is included in
|
|
||||||
/// at least `threshold` histories. In case there are multiple heads passing
|
|
||||||
/// the threshold, and they are divergent, an error is returned.
|
|
||||||
///
|
|
||||||
/// Also returns an error if `heads` is empty or `threshold` cannot be satisified with
|
|
||||||
/// the number of heads given.
|
|
||||||
pub fn quorum(
|
|
||||||
heads: &[git::raw::Oid],
|
|
||||||
threshold: usize,
|
|
||||||
repo: &git::raw::Repository,
|
|
||||||
) -> Result<Oid, QuorumError> {
|
|
||||||
let mut candidates = BTreeMap::<_, usize>::new();
|
|
||||||
|
|
||||||
// Build a list of candidate commits and count how many "votes" each of them has.
|
|
||||||
// Commits get a point for each direct vote, as well as for being part of the ancestry
|
|
||||||
// of a commit given to this function. Only commits given to the function are considered.
|
|
||||||
for (i, head) in heads.iter().enumerate() {
|
|
||||||
let head = Oid::from(*head);
|
|
||||||
|
|
||||||
// Add a direct vote for this head.
|
|
||||||
*candidates.entry(head).or_default() += 1;
|
|
||||||
|
|
||||||
// Compare this head to all other heads ahead of it in the list.
|
|
||||||
for other in heads.iter().skip(i + 1) {
|
|
||||||
// N.b. if heads are equal then skip it, otherwise it will end up as
|
|
||||||
// a double vote.
|
|
||||||
if *head == *other {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let base = repo.merge_base(*head, *other)?;
|
|
||||||
|
|
||||||
if base == *other || base == *head {
|
|
||||||
*candidates.entry(Oid::from(base)).or_default() += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Keep commits which pass the threshold.
|
|
||||||
candidates.retain(|_, votes| *votes >= threshold);
|
|
||||||
|
|
||||||
// Keep track of the longest identity branch.
|
|
||||||
let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoQuorum)?;
|
|
||||||
|
|
||||||
// Now that all scores are calculated, figure out what is the longest branch
|
|
||||||
// that passes the threshold. In case of divergence, return an error.
|
|
||||||
for head in candidates.keys() {
|
|
||||||
let base = repo.merge_base(**head, *longest)?;
|
|
||||||
|
|
||||||
if base == *longest {
|
|
||||||
// `head` is a successor of `longest`. Update `longest`.
|
|
||||||
//
|
|
||||||
// o head
|
|
||||||
// |
|
|
||||||
// o longest (base)
|
|
||||||
// |
|
|
||||||
//
|
|
||||||
longest = *head;
|
|
||||||
} else if base == **head || *head == longest {
|
|
||||||
// `head` is an ancestor of `longest`, or equal to it. Do nothing.
|
|
||||||
//
|
|
||||||
// o longest o longest, head (base)
|
|
||||||
// | |
|
|
||||||
// o head (base) OR o
|
|
||||||
// | |
|
|
||||||
//
|
|
||||||
} else {
|
|
||||||
// The merge base between `head` and `longest` (`base`)
|
|
||||||
// is neither `head` nor `longest`. Therefore, the branches have
|
|
||||||
// diverged.
|
|
||||||
//
|
|
||||||
// longest head
|
|
||||||
// \ /
|
|
||||||
// o (base)
|
|
||||||
// |
|
|
||||||
//
|
|
||||||
return Err(QuorumError::NoQuorum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((*longest).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod trailers {
|
pub mod trailers {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -1045,250 +939,12 @@ mod tests {
|
||||||
use crypto::test::signer::MockSigner;
|
use crypto::test::signer::MockSigner;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::assert_matches;
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::storage::refs::SIGREFS_BRANCH;
|
use crate::storage::refs::SIGREFS_BRANCH;
|
||||||
use crate::storage::{ReadRepository, ReadStorage};
|
use crate::storage::{ReadRepository, ReadStorage};
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quorum_properties() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let (repo, c0) = fixtures::repository(tmp.path());
|
|
||||||
let c0: git::Oid = c0.into();
|
|
||||||
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
||||||
let a2 = fixtures::commit("A2", &[*a1], &repo);
|
|
||||||
let d1 = fixtures::commit("D1", &[*c0], &repo);
|
|
||||||
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
||||||
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
|
||||||
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
|
||||||
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
||||||
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
|
||||||
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
|
||||||
let mut rng = fastrand::Rng::new();
|
|
||||||
let choices = [*c0, *c1, *c2, *b2, *a1, *a2, *d1, *m1, *m2];
|
|
||||||
|
|
||||||
for _ in 0..100 {
|
|
||||||
let count = rng.usize(1..=choices.len());
|
|
||||||
let threshold = rng.usize(1..=count);
|
|
||||||
let mut heads = Vec::new();
|
|
||||||
|
|
||||||
for _ in 0..count {
|
|
||||||
let ix = rng.usize(0..choices.len());
|
|
||||||
heads.push(choices[ix]);
|
|
||||||
}
|
|
||||||
rng.shuffle(&mut heads);
|
|
||||||
|
|
||||||
if let Ok(canonical) = quorum(&heads, threshold, &repo) {
|
|
||||||
assert!(heads.contains(&canonical));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quorum() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let (repo, c0) = fixtures::repository(tmp.path());
|
|
||||||
let c0: git::Oid = c0.into();
|
|
||||||
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
||||||
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
|
||||||
let c3 = fixtures::commit("C3", &[*c1], &repo);
|
|
||||||
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
|
||||||
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
||||||
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
|
||||||
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
|
||||||
|
|
||||||
eprintln!("C0: {c0}");
|
|
||||||
eprintln!("C1: {c1}");
|
|
||||||
eprintln!("C2: {c2}");
|
|
||||||
eprintln!("C3: {c3}");
|
|
||||||
eprintln!("B2: {b2}");
|
|
||||||
eprintln!("A1: {a1}");
|
|
||||||
eprintln!("M1: {m1}");
|
|
||||||
eprintln!("M2: {m2}");
|
|
||||||
|
|
||||||
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
|
||||||
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
|
|
||||||
assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
|
|
||||||
assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
|
|
||||||
// C1
|
|
||||||
// |
|
|
||||||
// C0
|
|
||||||
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
|
||||||
|
|
||||||
// C2
|
|
||||||
// |
|
|
||||||
// C1
|
|
||||||
// |
|
|
||||||
// C0
|
|
||||||
assert_eq!(quorum(&[*c1, *c2], 1, &repo).unwrap(), c2);
|
|
||||||
assert_eq!(quorum(&[*c1, *c2], 2, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2], 3, &repo).unwrap(), c0);
|
|
||||||
assert_eq!(quorum(&[*c1, *c1, *c2], 2, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*c1, *c1, *c2], 1, &repo).unwrap(), c2);
|
|
||||||
assert_eq!(quorum(&[*c2, *c2, *c1], 1, &repo).unwrap(), c2);
|
|
||||||
|
|
||||||
// B2 C2
|
|
||||||
// \|
|
|
||||||
// C1
|
|
||||||
// |
|
|
||||||
// C0
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c1, *c2, *b2], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(quorum(&[*c2, *b2], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*b2, *c2], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*c2, *b2], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*b2, *c2], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*b2, *b2, *c2], 2, &repo).unwrap(), b2);
|
|
||||||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
|
|
||||||
// B2 C2 C3
|
|
||||||
// \ | /
|
|
||||||
// C1
|
|
||||||
// |
|
|
||||||
// C0
|
|
||||||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*b2, *c2, *c2], 3, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
|
|
||||||
// B2 C2
|
|
||||||
// \|
|
|
||||||
// A1 C1
|
|
||||||
// \|
|
|
||||||
// C0
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c2, *b2, *a1], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c2, *b2, *a1], 2, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c2, *b2, *a1], 3, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
|
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
|
|
||||||
assert_eq!(quorum(&[*c0, *c2, *b2, *a1], 3, &repo).unwrap(), c0);
|
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*a1, *a1, *c2], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
|
|
||||||
// M2 M1
|
|
||||||
// /\ /\
|
|
||||||
// \ B2 C2
|
|
||||||
// \ \|
|
|
||||||
// A1 C1
|
|
||||||
// \|
|
|
||||||
// C0
|
|
||||||
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
|
||||||
assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m2, *m1], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(
|
|
||||||
quorum(&[*m1, *m2, *c2], 1, &repo),
|
|
||||||
Err(QuorumError::NoQuorum)
|
|
||||||
);
|
|
||||||
assert_matches!(quorum(&[*m1, *a1], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m1, *a1], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
|
|
||||||
assert_eq!(quorum(&[*m1, *m1, *c2], 2, &repo).unwrap(), m1);
|
|
||||||
assert_eq!(quorum(&[*m2, *m2, *b2], 2, &repo).unwrap(), m2);
|
|
||||||
assert_eq!(quorum(&[*m2, *m2, *a1], 2, &repo).unwrap(), m2);
|
|
||||||
assert_eq!(quorum(&[*m1, *m1, *b2, *b2], 2, &repo).unwrap(), m1);
|
|
||||||
assert_eq!(quorum(&[*m1, *m1, *c2, *c2], 2, &repo).unwrap(), m1);
|
|
||||||
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 3, &repo).unwrap(), c1);
|
|
||||||
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 4, &repo).unwrap(), c0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quorum_merges() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let (repo, c0) = fixtures::repository(tmp.path());
|
|
||||||
let c0: git::Oid = c0.into();
|
|
||||||
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
||||||
let c2 = fixtures::commit("C2", &[*c0], &repo);
|
|
||||||
let c3 = fixtures::commit("C3", &[*c0], &repo);
|
|
||||||
|
|
||||||
let m1 = fixtures::commit("M1", &[*c1, *c2], &repo);
|
|
||||||
let m2 = fixtures::commit("M2", &[*c2, *c3], &repo);
|
|
||||||
|
|
||||||
eprintln!("C0: {c0}");
|
|
||||||
eprintln!("C1: {c1}");
|
|
||||||
eprintln!("C2: {c2}");
|
|
||||||
eprintln!("C3: {c3}");
|
|
||||||
eprintln!("M1: {m1}");
|
|
||||||
eprintln!("M2: {m2}");
|
|
||||||
|
|
||||||
// M2 M1
|
|
||||||
// /\ /\
|
|
||||||
// C1 C2 C3
|
|
||||||
// \| /
|
|
||||||
// C0
|
|
||||||
assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
|
|
||||||
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
|
||||||
|
|
||||||
// M3/M2 M1
|
|
||||||
// /\ /\
|
|
||||||
// C1 C2 C3
|
|
||||||
// \| /
|
|
||||||
// C0
|
|
||||||
assert_matches!(quorum(&[*m1, *m3], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m1, *m3], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m3, *m1], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m3, *m1], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m3, *m2], 1, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
assert_matches!(quorum(&[*m3, *m2], 2, &repo), Err(QuorumError::NoQuorum));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_remote_refs() {
|
fn test_remote_refs() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue