use crate::core_context::CoreContext;
use anyhow::{Context as _, Result};
use saorsa_gossip_groups::GroupContext;
use saorsa_gossip_presence::PresenceManager;
use saorsa_gossip_transport::{QuicTransport, TransportConfig};
use saorsa_gossip_types::{PeerId, TopicId};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct LinkPolicy {
pub connected: bool,
pub latency: Duration,
pub jitter: Duration,
pub loss: f32,
}
impl Default for LinkPolicy {
fn default() -> Self {
Self {
connected: true,
latency: Duration::ZERO,
jitter: Duration::ZERO,
loss: 0.0,
}
}
}
impl LinkPolicy {
pub fn perfect() -> Self {
Self::default()
}
pub fn disconnected() -> Self {
Self {
connected: false,
..Default::default()
}
}
pub fn lossy(loss: f32) -> Self {
Self {
loss: loss.clamp(0.0, 1.0),
..Default::default()
}
}
pub fn slow(latency_ms: u64) -> Self {
Self {
latency: Duration::from_millis(latency_ms),
jitter: Duration::from_millis(latency_ms / 10),
..Default::default()
}
}
pub fn should_drop(&self) -> bool {
!self.connected || (self.loss > 0.0 && rand::random::<f32>() < self.loss)
}
pub fn effective_delay(&self) -> Duration {
if self.jitter > Duration::ZERO {
let jitter_ms = rand::random::<u64>() % self.jitter.as_millis() as u64;
self.latency + Duration::from_millis(jitter_ms)
} else {
self.latency
}
}
}
pub struct TestNode {
pub id: usize,
pub four_words: String,
pub peer_id: PeerId,
pub port: u16,
pub addr: SocketAddr,
pub temp_dir: TempDir,
pub core: Option<Arc<CoreContext>>,
pub presence: Option<Arc<RwLock<PresenceManager>>>,
pub groups: Arc<RwLock<HashMap<TopicId, GroupContext>>>,
pub transport: Arc<QuicTransport>,
}
impl TestNode {
pub async fn new(id: usize) -> Result<Self> {
let temp_dir = TempDir::new().context("failed to create temp dir")?;
let four_words = format!("test-node-{:04x}-peer", id);
let peer_id = PeerId::new([(id % 256) as u8; 32]);
let config = TransportConfig::default();
let transport = Arc::new(QuicTransport::new(config));
let port = 10000 + (id as u16); let addr: SocketAddr = format!("127.0.0.1:{}", port)
.parse()
.context("failed to parse address")?;
debug!("TestNode {} created on port {}", id, port);
Ok(TestNode {
id,
four_words,
peer_id,
port,
addr,
temp_dir,
core: None,
presence: None,
groups: Arc::new(RwLock::new(HashMap::new())),
transport,
})
}
pub async fn initialize_core(&mut self) -> Result<()> {
let groups_map = self.groups.clone();
let presence_mgr =
PresenceManager::new(self.peer_id, self.transport.clone(), groups_map.clone());
self.presence = Some(Arc::new(RwLock::new(presence_mgr)));
info!("TestNode {} core initialized", self.id);
Ok(())
}
pub async fn join_group(&self, topic_id: TopicId, group_name: &str) -> Result<()> {
let group_ctx = GroupContext::from_entity(group_name);
let mut groups = self.groups.write().await;
groups.insert(topic_id, group_ctx);
info!("TestNode {} joined group {}", self.id, group_name);
Ok(())
}
pub fn bootstrap_addr(&self) -> String {
format!("127.0.0.1:{}", self.port)
}
pub async fn shutdown(self) -> Result<()> {
info!("TestNode {} shutting down", self.id);
Ok(())
}
}
pub struct NetworkSimulator {
pub nodes: HashMap<usize, Arc<RwLock<TestNode>>>,
pub policies: Arc<RwLock<HashMap<(usize, usize), LinkPolicy>>>,
}
impl Default for NetworkSimulator {
fn default() -> Self {
Self::new()
}
}
impl NetworkSimulator {
pub fn new() -> Self {
NetworkSimulator {
nodes: HashMap::new(),
policies: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn add_node(&mut self, node: TestNode) -> usize {
let id = node.id;
self.nodes.insert(id, Arc::new(RwLock::new(node)));
id
}
pub async fn set_policy(&self, a: usize, b: usize, policy: LinkPolicy) {
let mut policies = self.policies.write().await;
policies.insert((a.min(b), a.max(b)), policy.clone());
debug!("Link {}->{} policy: {:?}", a, b, policy);
}
pub async fn get_policy(&self, a: usize, b: usize) -> LinkPolicy {
let policies = self.policies.read().await;
policies
.get(&(a.min(b), a.max(b)))
.cloned()
.unwrap_or_default()
}
pub async fn are_connected(&self, a: usize, b: usize) -> bool {
self.get_policy(a, b).await.connected
}
pub async fn get_node(&self, id: usize) -> Option<Arc<RwLock<TestNode>>> {
self.nodes.get(&id).cloned()
}
pub async fn connect(&self, a: usize, b: usize) {
self.set_policy(a, b, LinkPolicy::perfect()).await;
}
pub async fn disconnect(&self, a: usize, b: usize) {
self.set_policy(a, b, LinkPolicy::disconnected()).await;
}
pub async fn set_latency(&self, a: usize, b: usize, latency_ms: u64) {
self.set_policy(a, b, LinkPolicy::slow(latency_ms)).await;
}
pub async fn set_loss(&self, a: usize, b: usize, loss: f32) {
self.set_policy(a, b, LinkPolicy::lossy(loss)).await;
}
}
pub struct TestHarness {
pub network: Arc<RwLock<NetworkSimulator>>,
pub temp_dir: TempDir,
}
impl TestHarness {
pub async fn new(node_count: usize) -> Result<Self> {
let temp_dir = TempDir::new().context("failed to create harness temp dir")?;
let network = Arc::new(RwLock::new(NetworkSimulator::new()));
let harness = TestHarness { network, temp_dir };
for i in 0..node_count {
let mut node = TestNode::new(i).await?;
node.initialize_core().await?;
harness.network.write().await.add_node(node);
}
info!("TestHarness created with {} nodes", node_count);
Ok(harness)
}
pub async fn mesh(&self) -> Result<()> {
let node_count = self.network.read().await.nodes.len();
for i in 0..node_count {
for j in (i + 1)..node_count {
self.network.read().await.connect(i, j).await;
}
}
info!("Mesh topology configured");
Ok(())
}
pub async fn line(&self) -> Result<()> {
let node_count = self.network.read().await.nodes.len();
for i in 0..node_count {
for j in (i + 1)..node_count {
self.network.read().await.disconnect(i, j).await;
}
}
for i in 0..node_count.saturating_sub(1) {
self.network.read().await.connect(i, i + 1).await;
}
info!("Line topology configured");
Ok(())
}
pub async fn star(&self, hub: usize) -> Result<()> {
let node_count = self.network.read().await.nodes.len();
for i in 0..node_count {
for j in (i + 1)..node_count {
self.network.read().await.disconnect(i, j).await;
}
}
for i in 0..node_count {
if i != hub {
self.network.read().await.connect(hub, i).await;
}
}
info!("Star topology configured with hub {}", hub);
Ok(())
}
pub async fn partition(&self, group_a: &[usize], group_b: &[usize]) -> Result<()> {
for &a in group_a {
for &b in group_b {
self.network.read().await.disconnect(a, b).await;
}
}
info!("Network partitioned: {:?} | {:?}", group_a, group_b);
Ok(())
}
pub async fn heal(&self) -> Result<()> {
let node_count = self.network.read().await.nodes.len();
for i in 0..node_count {
for j in (i + 1)..node_count {
self.network.read().await.connect(i, j).await;
}
}
info!("Network healed");
Ok(())
}
pub async fn set_latency(&self, a: usize, b: usize, latency_ms: u64) {
self.network
.read()
.await
.set_latency(a, b, latency_ms)
.await;
}
pub async fn set_loss(&self, a: usize, b: usize, loss: f32) {
self.network.read().await.set_loss(a, b, loss).await;
}
pub async fn wait_until_connected(&self, count: usize, timeout: Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
let network = self.network.read().await;
let node_count = network.nodes.len();
let mut connected = 0;
for i in 0..node_count {
for j in (i + 1)..node_count {
if network.are_connected(i, j).await {
connected += 1;
}
}
}
if connected >= count {
info!("Connected threshold reached: {}/{}", connected, count);
return Ok(());
}
if start.elapsed() > timeout {
warn!("Timeout waiting for connections: {}/{}", connected, count);
return Err(anyhow::anyhow!(
"Timeout waiting for {} connections (got {})",
count,
connected
));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
pub async fn get_node(&self, id: usize) -> Option<Arc<RwLock<TestNode>>> {
self.network.read().await.get_node(id).await
}
pub async fn get_bootstrap_addrs(&self) -> Vec<String> {
let network = self.network.read().await;
let mut addrs = Vec::new();
for node_lock in network.nodes.values() {
let node = node_lock.read().await;
addrs.push(node.bootstrap_addr());
}
addrs
}
pub async fn cleanup(self) -> Result<()> {
info!("TestHarness cleanup started");
let network = Arc::try_unwrap(self.network)
.map_err(|_| anyhow::anyhow!("Failed to unwrap network"))?
.into_inner();
for (id, node_lock) in network.nodes {
let node = Arc::try_unwrap(node_lock)
.map_err(|_| anyhow::anyhow!("Failed to unwrap node {}", id))?
.into_inner();
node.shutdown().await?;
}
info!("TestHarness cleanup complete");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_harness_creation() {
let harness = TestHarness::new(3).await.expect("harness creation failed");
let network = harness.network.read().await;
assert_eq!(network.nodes.len(), 3);
drop(network);
harness.cleanup().await.expect("cleanup failed");
}
#[tokio::test]
async fn test_mesh_topology() {
let harness = TestHarness::new(4).await.expect("harness creation failed");
harness.mesh().await.expect("mesh setup failed");
let network = harness.network.read().await;
for i in 0..4 {
for j in (i + 1)..4 {
assert!(
network.are_connected(i, j).await,
"Nodes {} and {} should be connected",
i,
j
);
}
}
}
#[tokio::test]
async fn test_partition_and_heal() {
let harness = TestHarness::new(4).await.expect("harness creation failed");
harness.mesh().await.expect("mesh failed");
harness
.partition(&[0, 1], &[2, 3])
.await
.expect("partition failed");
let network = harness.network.read().await;
assert!(network.are_connected(0, 1).await);
assert!(network.are_connected(2, 3).await);
assert!(!network.are_connected(0, 2).await);
assert!(!network.are_connected(0, 3).await);
assert!(!network.are_connected(1, 2).await);
assert!(!network.are_connected(1, 3).await);
drop(network);
harness.heal().await.expect("heal failed");
let network = harness.network.read().await;
for i in 0..4 {
for j in (i + 1)..4 {
assert!(network.are_connected(i, j).await);
}
}
}
#[tokio::test]
async fn test_link_policies() {
let harness = TestHarness::new(2).await.expect("harness creation failed");
harness.set_latency(0, 1, 100).await;
let policy = harness.network.read().await.get_policy(0, 1).await;
assert_eq!(policy.latency, Duration::from_millis(100));
harness.set_loss(0, 1, 0.3).await;
let policy = harness.network.read().await.get_policy(0, 1).await;
assert_eq!(policy.loss, 0.3);
}
#[tokio::test]
async fn test_star_topology() {
let harness = TestHarness::new(5).await.expect("harness creation failed");
harness.star(0).await.expect("star failed");
let network = harness.network.read().await;
for i in 1..5 {
assert!(network.are_connected(0, i).await);
}
assert!(!network.are_connected(1, 2).await);
assert!(!network.are_connected(2, 3).await);
}
#[tokio::test]
async fn test_node_initialization() {
let node = TestNode::new(42).await.expect("node creation failed");
assert_eq!(node.id, 42);
assert!(node.port > 0, "Should have ephemeral port");
assert!(node.four_words.contains("test-node"));
node.shutdown().await.expect("shutdown failed");
}
}