radicle-heartwood-lfs/node/src/test/tests.rs

337 lines
9.8 KiB
Rust

use std::io;
use std::sync::Arc;
use nakamoto_net as nakamoto;
use nakamoto_net::simulator;
use nakamoto_net::simulator::{Peer as _, Simulation};
use nakamoto_net::Protocol as _;
use crate::collections::{HashMap, HashSet};
use crate::protocol::*;
use crate::storage::ReadStorage;
#[allow(unused)]
use crate::test::logger;
use crate::test::peer::Peer;
use crate::test::storage::MockStorage;
use crate::*;
// NOTE
//
// If you wish to see the logs for a running test, simply add the following line to your test:
//
// logger::init(log::Level::Debug);
//
// You may then run the test with eg. `cargo test -- --nocapture` to always show output.
#[test]
fn test_outbound_connection() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
alice.connect_to(&bob.addr());
alice.connect_to(&eve.addr());
let peers = alice
.protocol
.peers()
.negotiated()
.map(|(ip, _)| *ip)
.collect::<Vec<_>>();
assert!(peers.contains(&eve.ip));
assert!(peers.contains(&bob.ip));
}
#[test]
fn test_inbound_connection() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
alice.connect_from(&bob.addr());
alice.connect_from(&eve.addr());
let peers = alice
.protocol
.peers()
.negotiated()
.map(|(ip, _)| *ip)
.collect::<Vec<_>>();
assert!(peers.contains(&eve.ip));
assert!(peers.contains(&bob.ip));
}
#[test]
fn test_persistent_peer_connect() {
let rng = fastrand::Rng::new();
let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
let config = Config {
connect: vec![bob.addr(), eve.addr()],
..Config::default()
};
let mut alice = Peer::config(
"alice",
config,
[7, 7, 7, 7],
vec![],
MockStorage::empty(),
rng,
);
alice.initialize();
let mut outbox = alice.outbox();
assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == bob.addr());
assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == eve.addr());
assert_matches!(outbox.next(), None);
}
#[test]
#[ignore]
fn test_wrong_peer_version() {
// TODO
}
#[test]
#[ignore]
fn test_wrong_peer_magic() {
// TODO
}
#[test]
fn test_inventory_relay_bad_seq() {
let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
alice.connect_to(&bob.addr());
alice.receive(
&bob.addr(),
Message::Inventory {
seq: 0,
inv: vec![],
origin: None,
},
);
assert_matches!(
alice.outbox().next(),
Some(Io::Disconnect(addr, DisconnectReason::Error(PeerError::InvalidSequenceNumber(seq))))
if addr == bob.addr() && seq == 0
);
}
#[test]
fn test_inventory_relay() {
// Topology is eve <-> alice <-> bob
let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
let inv = vec![];
// Inventory from Bob relayed to Eve.
alice.connect_to(&bob.addr());
alice.connect_from(&eve.addr());
alice.receive(
&bob.addr(),
Message::Inventory {
seq: 1,
inv: inv.clone(),
origin: None,
},
);
assert_matches!(
alice.messages(&eve.addr()).next(),
Some(Message::Inventory { seq, origin, .. })
if origin == Some(bob.ip) && seq == 1
);
assert_matches!(
alice.messages(&bob.addr()).next(),
None,
"The inventory is not sent back to Bob"
);
alice.receive(
&bob.addr(),
Message::Inventory {
seq: 1,
inv: inv.clone(),
origin: None,
},
);
assert_matches!(
alice.messages(&eve.addr()).next(),
None,
"Sending the same inventory again doesn't trigger a relay"
);
alice.receive(
&bob.addr(),
Message::Inventory {
seq: 2,
inv: inv.clone(),
origin: None,
},
);
assert_matches!(
alice.messages(&eve.addr()).next(),
Some(Message::Inventory { seq, origin, .. })
if origin == Some(bob.ip) && seq == 2,
"Sending a new inventory does trigger the relay"
);
// Inventory from Eve relayed to Bob.
alice.receive(
&eve.addr(),
Message::Inventory {
seq: 4,
inv,
origin: None,
},
);
assert_matches!(
alice.messages(&bob.addr()).next(),
Some(Message::Inventory { seq, origin, .. })
if origin == Some(eve.ip) && seq == 4
);
}
#[test]
fn test_persistent_peer_reconnect() {
let mut bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
let mut eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
let mut alice = Peer::config(
"alice",
Config {
connect: vec![bob.addr(), eve.addr()],
..Config::default()
},
[7, 7, 7, 7],
vec![],
MockStorage::empty(),
fastrand::Rng::new(),
);
let mut sim = Simulation::new(
LocalTime::now(),
alice.rng.clone(),
simulator::Options::default(),
)
.initialize([&mut alice, &mut bob, &mut eve]);
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
let ips = alice
.peers()
.negotiated()
.map(|(ip, _)| *ip)
.collect::<Vec<_>>();
assert!(ips.contains(&bob.ip));
assert!(ips.contains(&eve.ip));
// ... Negotiated ...
//
// Now let's disconnect a peer.
// A transient error such as this will cause Alice to attempt a reconnection.
let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset));
// A non-transient disconnect, such as one requested by the user will not trigger
// a reconnection.
alice.disconnected(
&eve.addr(),
nakamoto::DisconnectReason::DialError(error.clone()),
);
assert_matches!(alice.outbox().next(), None);
for _ in 0..MAX_CONNECTION_ATTEMPTS {
alice.disconnected(
&bob.addr(),
nakamoto::DisconnectReason::ConnectionError(error.clone()),
);
assert_matches!(alice.outbox().next(), Some(Io::Connect(a)) if a == bob.addr());
assert_matches!(alice.outbox().next(), None);
alice.attempted(&bob.addr());
}
// After the max connection attempts, a disconnect doesn't trigger a reconnect.
alice.disconnected(
&bob.addr(),
nakamoto::DisconnectReason::ConnectionError(error),
);
assert_matches!(alice.outbox().next(), None);
}
#[test]
fn prop_inventory_exchange_dense() {
fn property(alice_inv: MockStorage, bob_inv: MockStorage, eve_inv: MockStorage) {
let rng = fastrand::Rng::new();
let alice = Peer::new("alice", [7, 7, 7, 7], alice_inv.clone());
let mut bob = Peer::new("bob", [8, 8, 8, 8], bob_inv.clone());
let mut eve = Peer::new("eve", [9, 9, 9, 9], eve_inv.clone());
let mut routing = Routing::with_hasher(rng.clone().into());
for (inv, peer) in &[
(alice_inv.inventory, alice.addr().ip()),
(bob_inv.inventory, bob.addr().ip()),
(eve_inv.inventory, eve.addr().ip()),
] {
for (proj, _) in inv {
routing
.entry(proj.clone())
.or_insert_with(|| HashSet::with_hasher(rng.clone().into()))
.insert(*peer);
}
}
// Fully-connected.
bob.command(Command::Connect(alice.addr()));
bob.command(Command::Connect(eve.addr()));
eve.command(Command::Connect(alice.addr()));
eve.command(Command::Connect(bob.addr()));
let mut peers: HashMap<_, _> = [(alice.ip, alice), (bob.ip, bob), (eve.ip, eve)]
.into_iter()
.collect();
let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default())
.initialize(peers.values_mut());
simulator.run_while(peers.values_mut(), |s| !s.is_settled());
for (proj_id, remotes) in &routing {
for peer in peers.values() {
let lookup = peer.lookup(proj_id);
if lookup.local.is_some() {
peer.storage()
.get(proj_id)
.expect("There are no errors querying storage")
.expect("The project is available locally");
} else {
for remote in &lookup.remote {
peers[remote]
.storage()
.get(proj_id)
.expect("There are no errors querying storage")
.expect("The project is available remotely");
}
assert!(
!lookup.remote.is_empty(),
"There are remote locations for the project"
);
assert_eq!(
&lookup.remote.into_iter().collect::<HashSet<_>>(),
remotes,
"The remotes match the global routing table"
);
}
}
}
}
quickcheck::QuickCheck::new()
.gen(quickcheck::Gen::new(8))
.quickcheck(property as fn(MockStorage, MockStorage, MockStorage));
}