node: More realistic concurrent fetch test

This implements a test which can optionally use a lot more data by
setting `RAD_TEST_SCALE` to a higher value.
This commit is contained in:
Alexis Sellier 2023-04-13 10:48:20 +02:00
parent 8205f8ad4e
commit ea4294c79f
No known key found for this signature in database
4 changed files with 107 additions and 27 deletions

View File

@ -59,6 +59,16 @@ impl Environment {
self.tempdir.path().join("misc")
}
/// Get the scale or "test size". This is used to scale tests with more data. Defaults to `1`.
pub fn scale(&self) -> usize {
env::var("RAD_TEST_SCALE")
.map(|s| {
s.parse()
.expect("repository: invalid value for `RAD_TEST_SCALE`")
})
.unwrap_or(1)
}
/// Create a new node in this environment. This should be used when a running node
/// is required. Use [`Environment::profile`] otherwise.
pub fn node(&mut self, name: &str) -> Node<MemorySigner> {
@ -318,15 +328,17 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
}
}
/// Populate a storage instance with a project.
pub fn project(&mut self, name: &str, description: &str) -> Id {
/// Populate a storage instance with a project from the given repository.
pub fn project_from(
&mut self,
name: &str,
description: &str,
repo: &git::raw::Repository,
) -> Id {
transport::local::register(self.storage.clone());
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = fixtures::repository(tmp.path());
let id = rad::init(
&repo,
repo,
name,
description,
refname!("master"),
@ -341,8 +353,29 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
"Initialized project {id} for node {}", self.signer.public_key()
);
// Push local branches to storage.
let mut refs = Vec::<(git::Qualified, git::Qualified)>::new();
for branch in repo.branches(Some(git::raw::BranchType::Local)).unwrap() {
let (branch, _) = branch.unwrap();
let name = git::RefString::try_from(branch.name().unwrap().unwrap()).unwrap();
refs.push((
git::lit::refs_heads(&name).into(),
git::lit::refs_heads(&name).into(),
));
}
git::push(repo, "rad", refs.iter().map(|(a, b)| (a, b))).unwrap();
id
}
/// Populate a storage instance with a project.
pub fn project(&mut self, name: &str, description: &str) -> Id {
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = fixtures::repository(tmp.path());
self.project_from(name, description, &repo)
}
}
/// Checks whether the nodes have converged in their routing tables.

View File

@ -2,14 +2,14 @@ use std::{collections::HashSet, thread, time};
use radicle::crypto::{test::signer::MockSigner, Signer};
use radicle::node::{FetchResult, Handle as _};
use radicle::prelude::Id;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::test::fixtures;
use radicle::{assert_matches, rad};
use crate::service;
use crate::service::tracking::Scope;
use crate::storage::git::transport;
use crate::test::environment::{converge, Node};
use crate::test::environment::{converge, Environment, Node};
use crate::test::logger;
#[test]
@ -462,23 +462,30 @@ fn test_fetch_up_to_date() {
fn test_concurrent_fetches() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let mut bob_repos: HashSet<Id> = HashSet::from_iter([
bob.project("bob-1", ""),
bob.project("bob-2", ""),
bob.project("bob-3", ""),
bob.project("bob-4", ""),
bob.project("bob-5", ""),
]);
let mut alice_repos: HashSet<Id> = HashSet::from_iter([
alice.project("alice-1", ""),
alice.project("alice-2", ""),
alice.project("alice-3", ""),
alice.project("alice-4", ""),
alice.project("alice-5", ""),
]);
let env = Environment::new();
let scale = env.scale();
let mut bob_repos = HashSet::new();
let mut alice_repos = HashSet::new();
let mut alice = Node::init(&env.tmp());
let mut bob = Node::init(&env.tmp());
for i in 0..scale.max(3) {
// Create a repo for Alice.
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale);
let rid = alice.project_from(&format!("alice-{i}"), "", &repo);
alice_repos.insert(rid);
// Create a repo for Bob.
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = fixtures::repository(tmp.path());
fixtures::populate(&repo, scale);
let rid = bob.project_from(&format!("bob-{i}"), "", &repo);
bob_repos.insert(rid);
}
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());

View File

@ -409,7 +409,7 @@ where
#[cfg(test)]
if c.receiver.is_empty() {
panic!("Wire:flush: redundant flush");
panic!("Wire::flush: redundant flush");
}
for data in c.receiver.try_iter() {

View File

@ -79,13 +79,53 @@ pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
repo.set_head("refs/heads/master").unwrap();
repo.checkout_head(None).unwrap();
// Look, I don't really understand why we have to do this, but we do.
drop(tree);
drop(head);
(repo, oid)
}
/// Populate a repository with commits, branches and blobs.
pub fn populate(repo: &git2::Repository, scale: usize) {
assert!(
scale <= 8,
"Scale parameter must be less than or equal to 8"
);
if scale == 0 {
return;
}
let head = repo.head().unwrap().peel_to_commit().unwrap();
let rng = fastrand::Rng::with_seed(42);
let mut buffer = vec![0; 1024 * 1024 * scale];
for _ in 0..scale {
let random = std::iter::repeat_with(|| rng.alphanumeric())
.take(7)
.collect::<String>()
.to_lowercase();
let name = format!("feature/{random}");
let signature = git2::Signature::now("Radicle", "radicle@radicle.xyz").unwrap();
rng.fill(&mut buffer);
let blob = repo.blob(&buffer).unwrap();
let mut builder = repo.treebuilder(None).unwrap();
builder.insert("random.txt", blob, 0o100_644).unwrap();
let tree_oid = builder.write().unwrap();
let tree = repo.find_tree(tree_oid).unwrap();
repo.commit(
Some(&format!("refs/heads/{name}")),
&signature,
&signature,
&format!("Initialize new branch feature/{random}"),
&tree,
&[&head],
)
.unwrap();
}
}
/// Generate random fixtures.
pub mod gen {
use super::*;