Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Create shared test environment
Alexis Sellier committed 3 years ago
commit b542ddcd6c44f8e1768222f4cf2daee06c8c98aa
parent abb288e7428daef0ddf034150cfc9668d5fadf57
6 files changed +293 -259
modified Cargo.lock
@@ -1852,6 +1852,7 @@ dependencies = [
 "radicle",
 "radicle-cob",
 "radicle-crypto",
+
 "radicle-node",
 "serde",
 "serde_json",
 "serde_yaml",
modified radicle-cli/Cargo.toml
@@ -43,5 +43,6 @@ path = "../radicle-crypto"
pretty_assertions = { version = "1.3.0" }
tempfile = { version = "3.3.0" }
radicle = { path = "../radicle", features = ["test"] }
+
radicle-node = { path = "../radicle-node", features = ["test"] }
shlex = { version = "1.1.0" }
snapbox = { version = "0.4.3" }
modified radicle-cli/tests/commands.rs
@@ -1,56 +1,14 @@
use std::env;
use std::path::Path;

-
use radicle::crypto::ssh::Keystore;
-
use radicle::crypto::KeyPair;
use radicle::git;
-
use radicle::profile::{Home, Profile};
-
use radicle::storage::git::transport;
-
use radicle::storage::git::Storage;
+
use radicle::profile::Home;
use radicle::test::fixtures;

+
use radicle_node::test::environment::Environment;
+

mod framework;
use framework::TestFormula;
-
use radicle_crypto::Seed;
-

-
/// Test environment.
-
pub struct Environment {
-
    tempdir: tempfile::TempDir,
-
    users: usize,
-
}
-

-
impl Environment {
-
    /// Create a new test environment.
-
    fn new() -> Self {
-
        Self {
-
            tempdir: tempfile::tempdir().unwrap(),
-
            users: 0,
-
        }
-
    }
-

-
    /// Create a new profile in this environment.
-
    fn profile(&mut self, name: &str) -> Profile {
-
        let home = Home::new(self.tempdir.path().join(name)).init().unwrap();
-
        let storage = Storage::open(home.storage()).unwrap();
-
        let keystore = Keystore::new(&home.keys());
-
        let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
-

-
        transport::local::register(storage.clone());
-
        keystore
-
            .store(keypair.clone(), "radicle", "radicle".to_owned())
-
            .unwrap();
-

-
        // Ensures that each user has a unique but deterministic public key.
-
        self.users += 1;
-

-
        Profile {
-
            home,
-
            storage,
-
            keystore,
-
            public_key: keypair.pk.into(),
-
        }
-
    }
-
}

/// Run a CLI test file.
fn test<'a>(
modified radicle-node/src/test.rs
@@ -1,4 +1,5 @@
pub mod arbitrary;
+
pub mod environment;
pub mod gossip;
pub mod handle;
pub mod logger;
added radicle-node/src/test/environment.rs
@@ -0,0 +1,264 @@
+
use std::mem::ManuallyDrop;
+
use std::path::Path;
+
use std::{
+
    collections::{BTreeMap, BTreeSet},
+
    iter, net, thread,
+
    time::Duration,
+
};
+

+
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
+
use radicle::crypto::test::signer::MockSigner;
+
use radicle::crypto::{KeyPair, Seed, Signature, Signer};
+
use radicle::git::refname;
+
use radicle::identity::Id;
+
use radicle::node::Handle as _;
+
use radicle::profile::Home;
+
use radicle::profile::Profile;
+
use radicle::rad;
+
use radicle::storage::ReadStorage;
+
use radicle::test::fixtures;
+
use radicle::Storage;
+

+
use crate::node::NodeId;
+
use crate::storage::git::transport;
+
use crate::{runtime, runtime::Handle, service, Runtime};
+

+
/// Test environment.
+
pub struct Environment {
+
    tempdir: tempfile::TempDir,
+
    users: usize,
+
}
+

+
impl Default for Environment {
+
    fn default() -> Self {
+
        Self {
+
            tempdir: tempfile::tempdir().unwrap(),
+
            users: 0,
+
        }
+
    }
+
}
+

+
impl Environment {
+
    /// Create a new test environment.
+
    pub fn new() -> Self {
+
        Self::default()
+
    }
+

+
    /// 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> {
+
        let profile = self.profile(name);
+
        let signer = MemorySigner::load(&profile.keystore, "radicle".to_owned().into()).unwrap();
+

+
        Node {
+
            home: profile.home,
+
            signer,
+
            storage: profile.storage,
+
        }
+
    }
+

+
    /// Create a new profile in this environment.
+
    /// This should be used when a running node is not required.
+
    pub fn profile(&mut self, name: &str) -> Profile {
+
        let home = Home::new(self.tempdir.path().join(name)).init().unwrap();
+
        let storage = Storage::open(home.storage()).unwrap();
+
        let keystore = Keystore::new(&home.keys());
+
        let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
+

+
        transport::local::register(storage.clone());
+
        keystore
+
            .store(keypair.clone(), "radicle", "radicle".to_owned())
+
            .unwrap();
+

+
        // Ensures that each user has a unique but deterministic public key.
+
        self.users += 1;
+

+
        Profile {
+
            home,
+
            storage,
+
            keystore,
+
            public_key: keypair.pk.into(),
+
        }
+
    }
+
}
+

+
/// A node that can be run.
+
pub struct Node<G> {
+
    pub home: Home,
+
    pub signer: G,
+
    pub storage: Storage,
+
}
+

+
/// Handle to a running node.
+
pub struct NodeHandle<G: Signer + cyphernet::EcSign + 'static> {
+
    pub id: NodeId,
+
    pub storage: Storage,
+
    pub signer: G,
+
    pub addr: net::SocketAddr,
+
    pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
+
    pub handle: ManuallyDrop<Handle<G>>,
+
}
+

+
impl<G: Signer + cyphernet::EcSign + 'static> Drop for NodeHandle<G> {
+
    fn drop(&mut self) {
+
        log::debug!(target: "test", "Node {} shutting down..", self.id);
+

+
        unsafe { ManuallyDrop::take(&mut self.handle) }
+
            .shutdown()
+
            .unwrap();
+
        unsafe { ManuallyDrop::take(&mut self.thread) }
+
            .join()
+
            .unwrap()
+
            .unwrap();
+
    }
+
}
+

+
impl<G: Signer + cyphernet::EcSign> NodeHandle<G> {
+
    /// Connect this node to another node, and wait for the connection to be established both ways.
+
    pub fn connect(&mut self, remote: &NodeHandle<G>) {
+
        self.handle.connect(remote.id, remote.addr.into()).unwrap();
+

+
        loop {
+
            let local_sessions = self.handle.sessions().unwrap();
+
            let remote_sessions = remote.handle.sessions().unwrap();
+

+
            let local_sessions = local_sessions
+
                .negotiated()
+
                .map(|(id, _)| id)
+
                .collect::<BTreeSet<_>>();
+
            let remote_sessions = remote_sessions
+
                .negotiated()
+
                .map(|(id, _)| id)
+
                .collect::<BTreeSet<_>>();
+

+
            if local_sessions.contains(&remote.id) && remote_sessions.contains(&self.id) {
+
                log::debug!(target: "test", "Connection between {} and {} established", self.id, remote.id);
+
                break;
+
            }
+
            thread::sleep(Duration::from_millis(100));
+
        }
+
    }
+
}
+

+
impl Node<MockSigner> {
+
    /// Create a new node.
+
    pub fn init(base: &Path) -> Self {
+
        let home = base.join(
+
            iter::repeat_with(fastrand::alphanumeric)
+
                .take(8)
+
                .collect::<String>(),
+
        );
+
        let home = Home::new(home).init().unwrap();
+
        let signer = MockSigner::default();
+
        let storage = Storage::open(home.storage()).unwrap();
+

+
        Self {
+
            home,
+
            signer,
+
            storage,
+
        }
+
    }
+
}
+

+
impl<G: cyphernet::EcSign<Pk = NodeId, Sig = Signature> + Signer + Clone> Node<G> {
+
    /// Spawn a node in its own thread.
+
    pub fn spawn(self, config: service::Config) -> NodeHandle<G> {
+
        let listen = vec![([0, 0, 0, 0], 0).into()];
+
        let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
+
        let daemon = ([0, 0, 0, 0], fastrand::u16(1025..)).into();
+
        let rt = Runtime::init(
+
            self.home,
+
            config,
+
            listen,
+
            proxy,
+
            daemon,
+
            self.signer.clone(),
+
        )
+
        .unwrap();
+
        let addr = *rt.local_addrs.first().unwrap();
+
        let id = *self.signer.public_key();
+
        let handle = ManuallyDrop::new(rt.handle.clone());
+
        let thread = ManuallyDrop::new(
+
            thread::Builder::new()
+
                .name(id.to_string())
+
                .spawn(move || rt.run())
+
                .unwrap(),
+
        );
+

+
        NodeHandle {
+
            id,
+
            storage: self.storage,
+
            signer: self.signer,
+
            addr,
+
            handle,
+
            thread,
+
        }
+
    }
+

+
    /// Populate a storage instance with a project.
+
    pub fn project(&mut self, name: &str) -> Id {
+
        transport::local::register(self.storage.clone());
+

+
        let tmp = tempfile::tempdir().unwrap();
+
        let (repo, _) = fixtures::gen::repository(tmp.path());
+
        let description = iter::repeat_with(fastrand::alphabetic)
+
            .take(12)
+
            .collect::<String>();
+
        let id = rad::init(
+
            &repo,
+
            name,
+
            &description,
+
            refname!("master"),
+
            &self.signer,
+
            &self.storage,
+
        )
+
        .map(|(id, _, _)| id)
+
        .unwrap();
+

+
        log::debug!(
+
            target: "test",
+
            "Initialized project {id} for node {}", self.signer.public_key()
+
        );
+

+
        id
+
    }
+
}
+

+
/// Checks whether the nodes have converged in their routing tables.
+
#[track_caller]
+
pub fn converge<'a, G: Signer + cyphernet::EcSign + 'static>(
+
    nodes: impl IntoIterator<Item = &'a NodeHandle<G>>,
+
) -> BTreeSet<(Id, NodeId)> {
+
    let nodes = nodes.into_iter().collect::<Vec<_>>();
+

+
    let mut all_routes = BTreeSet::<(Id, NodeId)>::new();
+
    let mut remaining = BTreeMap::from_iter(nodes.iter().map(|node| (node.id, node)));
+

+
    // First build the set of all routes.
+
    for node in &nodes {
+
        let inv = node.storage.inventory().unwrap();
+

+
        for rid in inv {
+
            all_routes.insert((rid, node.id));
+
        }
+
    }
+

+
    // Then, while there are nodes remaining to converge, check each node to see if
+
    // its routing table has all routes. If so, remove it from the remaining nodes.
+
    while !remaining.is_empty() {
+
        remaining.retain(|_, node| {
+
            let routing = node.handle.routing().unwrap();
+
            let routes = BTreeSet::from_iter(routing.try_iter());
+

+
            if routes == all_routes {
+
                log::debug!(target: "test", "Node {} has converged", node.id);
+
                return false;
+
            } else {
+
                log::debug!(target: "test", "Node {} has {:?}", node.id, routes);
+
            }
+
            true
+
        });
+
        thread::sleep(Duration::from_millis(100));
+
    }
+
    all_routes
+
}
modified radicle-node/src/tests/e2e.rs
@@ -1,203 +1,12 @@
-
use std::mem::ManuallyDrop;
-
use std::path::Path;
-
use std::{
-
    collections::{BTreeMap, BTreeSet},
-
    iter, net, thread,
-
    time::Duration,
-
};
-

-
use radicle::crypto::test::signer::MockSigner;
use radicle::crypto::Signer;
-
use radicle::git::refname;
-
use radicle::identity::Id;
use radicle::node::{FetchResult, Handle as _};
-
use radicle::profile::Home;
-
use radicle::storage::{ReadRepository, ReadStorage, WriteStorage};
-
use radicle::test::fixtures;
-
use radicle::Storage;
+
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::{assert_matches, rad};

-
use crate::node::NodeId;
+
use crate::service;
use crate::storage::git::transport;
+
use crate::test::environment::{converge, Node};
use crate::test::logger;
-
use crate::{runtime, runtime::Handle, service, Runtime};
-

-
/// A node that can be run.
-
struct Node {
-
    home: Home,
-
    signer: MockSigner,
-
    storage: Storage,
-
}
-

-
/// Handle to a running node.
-
struct NodeHandle {
-
    id: NodeId,
-
    storage: Storage,
-
    signer: MockSigner,
-
    addr: net::SocketAddr,
-
    thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
-
    handle: ManuallyDrop<Handle<MockSigner>>,
-
}
-

-
impl Drop for NodeHandle {
-
    fn drop(&mut self) {
-
        log::debug!(target: "test", "Node {} shutting down..", self.id);
-

-
        unsafe { ManuallyDrop::take(&mut self.handle) }
-
            .shutdown()
-
            .unwrap();
-
        unsafe { ManuallyDrop::take(&mut self.thread) }
-
            .join()
-
            .unwrap()
-
            .unwrap();
-
    }
-
}
-

-
impl NodeHandle {
-
    /// Connect this node to another node, and wait for the connection to be established both ways.
-
    fn connect(&mut self, remote: &NodeHandle) {
-
        self.handle.connect(remote.id, remote.addr.into()).unwrap();
-

-
        loop {
-
            let local_sessions = self.handle.sessions().unwrap();
-
            let remote_sessions = remote.handle.sessions().unwrap();
-

-
            let local_sessions = local_sessions
-
                .negotiated()
-
                .map(|(id, _)| id)
-
                .collect::<BTreeSet<_>>();
-
            let remote_sessions = remote_sessions
-
                .negotiated()
-
                .map(|(id, _)| id)
-
                .collect::<BTreeSet<_>>();
-

-
            if local_sessions.contains(&remote.id) && remote_sessions.contains(&self.id) {
-
                log::debug!(target: "test", "Connection between {} and {} established", self.id, remote.id);
-
                break;
-
            }
-
            thread::sleep(Duration::from_millis(100));
-
        }
-
    }
-
}
-

-
impl Node {
-
    /// Create a new node.
-
    fn new(base: &Path) -> Self {
-
        let home = base.join(
-
            iter::repeat_with(fastrand::alphanumeric)
-
                .take(8)
-
                .collect::<String>(),
-
        );
-
        let home = Home::new(home).init().unwrap();
-
        let signer = MockSigner::default();
-
        let storage = Storage::open(home.storage()).unwrap();
-

-
        Self {
-
            home,
-
            signer,
-
            storage,
-
        }
-
    }
-

-
    /// Spawn a node in its own thread.
-
    fn spawn(self, config: service::Config) -> NodeHandle {
-
        let listen = vec![([0, 0, 0, 0], 0).into()];
-
        let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
-
        let daemon = ([0, 0, 0, 0], fastrand::u16(1025..)).into();
-
        let rt = Runtime::init(
-
            self.home,
-
            config,
-
            listen,
-
            proxy,
-
            daemon,
-
            self.signer.clone(),
-
        )
-
        .unwrap();
-
        let addr = *rt.local_addrs.first().unwrap();
-
        let id = *self.signer.public_key();
-
        let handle = ManuallyDrop::new(rt.handle.clone());
-
        let thread = ManuallyDrop::new(
-
            thread::Builder::new()
-
                .name(id.to_string())
-
                .spawn(move || rt.run())
-
                .unwrap(),
-
        );
-

-
        NodeHandle {
-
            id,
-
            storage: self.storage,
-
            signer: self.signer,
-
            addr,
-
            handle,
-
            thread,
-
        }
-
    }
-

-
    /// Populate a storage instance with a project.
-
    fn project(&mut self, name: &str) -> Id {
-
        transport::local::register(self.storage.clone());
-

-
        let tmp = tempfile::tempdir().unwrap();
-
        let (repo, _) = fixtures::gen::repository(tmp.path());
-
        let description = iter::repeat_with(fastrand::alphabetic)
-
            .take(12)
-
            .collect::<String>();
-
        let id = rad::init(
-
            &repo,
-
            name,
-
            &description,
-
            refname!("master"),
-
            &self.signer,
-
            &self.storage,
-
        )
-
        .map(|(id, _, _)| id)
-
        .unwrap();
-

-
        log::debug!(
-
            target: "test",
-
            "Initialized project {id} for node {}", self.signer.public_key()
-
        );
-

-
        id
-
    }
-
}
-

-
/// Checks whether the nodes have converged in their routing tables.
-
#[track_caller]
-
fn converge<'a>(nodes: impl IntoIterator<Item = &'a NodeHandle>) -> BTreeSet<(Id, NodeId)> {
-
    let nodes = nodes.into_iter().collect::<Vec<_>>();
-

-
    let mut all_routes = BTreeSet::<(Id, NodeId)>::new();
-
    let mut remaining = BTreeMap::from_iter(nodes.iter().map(|node| (node.id, node)));
-

-
    // First build the set of all routes.
-
    for node in &nodes {
-
        let inv = node.storage.inventory().unwrap();
-

-
        for rid in inv {
-
            all_routes.insert((rid, node.id));
-
        }
-
    }
-

-
    // Then, while there are nodes remaining to converge, check each node to see if
-
    // its routing table has all routes. If so, remove it from the remaining nodes.
-
    while !remaining.is_empty() {
-
        remaining.retain(|_, node| {
-
            let routing = node.handle.routing().unwrap();
-
            let routes = BTreeSet::from_iter(routing.try_iter());
-

-
            if routes == all_routes {
-
                log::debug!(target: "test", "Node {} has converged", node.id);
-
                return false;
-
            } else {
-
                log::debug!(target: "test", "Node {} has {:?}", node.id, routes);
-
            }
-
            true
-
        });
-
        thread::sleep(Duration::from_millis(100));
-
    }
-
    all_routes
-
}

#[test]
//
@@ -208,8 +17,8 @@ fn test_inventory_sync_basic() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
+
    let mut alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());

    alice.project("alice");
    bob.project("bob");
@@ -232,9 +41,9 @@ fn test_inventory_sync_bridge() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
-
    let mut eve = Node::new(tmp.path());
+
    let mut alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
+
    let mut eve = Node::init(tmp.path());

    alice.project("alice");
    bob.project("bob");
@@ -262,10 +71,10 @@ fn test_inventory_sync_ring() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
-
    let mut eve = Node::new(tmp.path());
-
    let mut carol = Node::new(tmp.path());
+
    let mut alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
+
    let mut eve = Node::init(tmp.path());
+
    let mut carol = Node::init(tmp.path());

    alice.project("alice");
    bob.project("bob");
@@ -299,11 +108,11 @@ fn test_inventory_sync_star() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
-
    let mut eve = Node::new(tmp.path());
-
    let mut carol = Node::new(tmp.path());
-
    let mut dave = Node::new(tmp.path());
+
    let mut alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
+
    let mut eve = Node::init(tmp.path());
+
    let mut carol = Node::init(tmp.path());
+
    let mut dave = Node::init(tmp.path());

    alice.project("alice");
    bob.project("bob");
@@ -331,8 +140,8 @@ fn test_replication() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
+
    let alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
    let acme = bob.project("acme");

    let mut alice = alice.spawn(service::Config::default());
@@ -388,8 +197,8 @@ fn test_clone() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
+
    let alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
    let acme = bob.project("acme");

    let mut alice = alice.spawn(service::Config::default());
@@ -438,8 +247,8 @@ fn test_fetch_up_to_date() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::new(tmp.path());
-
    let mut bob = Node::new(tmp.path());
+
    let alice = Node::init(tmp.path());
+
    let mut bob = Node::init(tmp.path());
    let acme = bob.project("acme");

    let mut alice = alice.spawn(service::Config::default());