diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index fda80ca6..478b591a 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -1,11 +1,15 @@ use std::env; use std::path::Path; +use std::str::FromStr; use std::{thread, time}; use radicle::git; +use radicle::prelude::Id; use radicle::profile::Home; +use radicle::storage::{ReadRepository, WriteStorage}; use radicle::test::fixtures; +use radicle_node::service::tracking::Policy; use radicle_node::test::{ environment::{Config, Environment}, logger, @@ -267,3 +271,76 @@ fn rad_init_sync_and_clone() { ) .unwrap(); } + +#[test] +// +// alice -- seed -- bob +// +fn test_replication_via_seed() { + logger::init(log::Level::Debug); + + let mut environment = Environment::new(); + let alice = environment.node("alice"); + let bob = environment.node("bob"); + let seed = environment.node("seed"); + let working = environment.tmp().join("working"); + let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); + + let mut alice = alice.spawn(Config::default()); + let mut bob = bob.spawn(Config::default()); + let seed = seed.spawn(Config { + policy: Policy::Track, + ..Config::default() + }); + + alice.connect(&seed); + bob.connect(&seed); + + // Enough time for the next inventory from Seed to not be considered stale by Bob. + thread::sleep(time::Duration::from_secs(1)); + + alice.routes_to(&[]); + seed.routes_to(&[]); + bob.routes_to(&[]); + + // Initialize a repo as Alice. + fixtures::repository(working.join("alice")); + alice + .rad( + "init", + &[ + "--name", + "heartwood", + "--description", + "Radicle Heartwood Protocol & Stack", + "--default-branch", + "master", + ], + working.join("alice"), + ) + .unwrap(); + + alice.routes_to(&[(rid, alice.id), (rid, seed.id)]); + seed.routes_to(&[(rid, alice.id), (rid, seed.id)]); + bob.routes_to(&[(rid, alice.id), (rid, seed.id)]); + + bob.rad("clone", &[rid.to_string().as_str()], working.join("bob")) + .unwrap(); + + alice.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]); + seed.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]); + bob.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]); + + // Enough time for the Seed to be able to fetch Bob's fork. + thread::sleep(time::Duration::from_secs(1)); + + seed.storage + .repository(rid) + .unwrap() + .remote(&bob.id) + .unwrap(); + + // TODO: Seed should send Bob's ref announcement to Alice, after the fetch. + // Currently, it is relayed when received from Bob, before the fetch completes, + // which means that Alice fetches too early from Seed, and nothing is fetched. +} diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index c39ac800..70afc767 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -754,7 +754,7 @@ where // Discard inventory messages we've already seen, otherwise update // out last seen time. if !peer.inventory_announced(timestamp) { - debug!(target: "service", "Ignoring stale inventory announcement from {announcer}"); + debug!(target: "service", "Ignoring stale inventory announcement from {announcer} (t={})", self.clock.as_secs()); return Ok(false); } diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 46652c00..0441a0ed 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -162,12 +162,21 @@ impl NodeHandle { } /// Wait until this node's routing table contains the given routes. + #[track_caller] pub fn routes_to(&self, routes: &[(Id, NodeId)]) { - let mut remaining: BTreeSet<_> = routes.iter().collect(); + loop { + let mut remaining: BTreeSet<_> = routes.iter().collect(); - while !remaining.is_empty() { for (rid, nid) in self.handle.routing().unwrap() { - remaining.remove(&(rid, nid)); + if !remaining.remove(&(rid, nid)) { + panic!( + "Node::routes_to: unexpected route for {}: ({rid}, {nid})", + self.id + ); + } + } + if remaining.is_empty() { + break; } thread::sleep(Duration::from_millis(100)); } @@ -309,9 +318,12 @@ pub fn converge<'a, G: Signer + cyphernet::EcSign + 'static>( // First build the set of all routes. for node in &nodes { - let inv = node.storage.inventory().unwrap(); - - for rid in inv { + // Routes from the routing table. + for (rid, seed_id) in node.handle.routing().unwrap() { + all_routes.insert((rid, seed_id)); + } + // Routes from the local inventory. + for rid in node.storage.inventory().unwrap() { all_routes.insert((rid, node.id)); } }