From ea4294c79f64a2a5739b1fca98d98427b91846a3 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Thu, 13 Apr 2023 10:48:20 +0200 Subject: [PATCH] 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. --- radicle-node/src/test/environment.rs | 45 ++++++++++++++++++++++++---- radicle-node/src/tests/e2e.rs | 45 ++++++++++++++++------------ radicle-node/src/wire/protocol.rs | 2 +- radicle/src/test/fixtures.rs | 42 +++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 27 deletions(-) diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index fece4817..93814f8b 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -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 { @@ -318,15 +328,17 @@ impl + Signer + Clone> Node { } } - /// 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 + Signer + Clone> Node { "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. diff --git a/radicle-node/src/tests/e2e.rs b/radicle-node/src/tests/e2e.rs index 652d3d77..668acae2 100644 --- a/radicle-node/src/tests/e2e.rs +++ b/radicle-node/src/tests/e2e.rs @@ -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 = 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 = 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()); diff --git a/radicle-node/src/wire/protocol.rs b/radicle-node/src/wire/protocol.rs index 6a77290f..b96f1364 100644 --- a/radicle-node/src/wire/protocol.rs +++ b/radicle-node/src/wire/protocol.rs @@ -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() { diff --git a/radicle/src/test/fixtures.rs b/radicle/src/test/fixtures.rs index d722ebd4..c20cc4b2 100644 --- a/radicle/src/test/fixtures.rs +++ b/radicle/src/test/fixtures.rs @@ -79,13 +79,53 @@ pub fn repository>(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::() + .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::*;