use crate::constants::*;
use crate::relay::{RelayInfo, RelayManager, RelayRole, RelayTransition};
use offline_protocol_core::Message;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct NeighborInfo {
pub peer_id: String,
pub rssi: i16,
pub hops_to_destination: Option<u8>,
pub link_quality: u8,
pub relay_info: Option<RelayInfo>,
}
#[derive(Debug, Clone)]
pub struct GossipConfig {
pub enabled: bool,
pub target_fanout: usize,
pub min_probability: f32,
pub small_network_threshold: usize,
pub priority_bypass: bool,
}
impl Default for GossipConfig {
fn default() -> Self {
Self {
enabled: true,
target_fanout: 4,
min_probability: 0.15,
small_network_threshold: 10,
priority_bypass: true,
}
}
}
#[derive(Debug, Clone)]
pub struct GradientRoutingConfig {
pub enabled: bool,
pub max_routes_per_destination: usize,
pub route_ttl_secs: u64,
pub max_routing_table_size: usize,
}
impl Default for GradientRoutingConfig {
fn default() -> Self {
Self {
enabled: true,
max_routes_per_destination: 3,
route_ttl_secs: 300, max_routing_table_size: 1000,
}
}
}
#[derive(Debug, Clone)]
pub struct RouteEntry {
pub next_hop: String,
pub hop_count: u8,
pub last_seen: Instant,
pub quality: f32,
pub sequence_number: u32,
}
#[derive(Debug)]
pub struct GradientRoutingTable {
config: GradientRoutingConfig,
routes: HashMap<String, Vec<RouteEntry>>,
neighbor_destinations: HashMap<String, Vec<String>>,
}
impl GradientRoutingTable {
pub fn new() -> Self {
Self::with_config(GradientRoutingConfig::default())
}
pub fn with_config(config: GradientRoutingConfig) -> Self {
Self {
config,
routes: HashMap::new(),
neighbor_destinations: HashMap::new(),
}
}
pub fn learn_route(
&mut self,
destination: &str,
next_hop: &str,
hop_count: u8,
quality: f32,
sequence_number: u32,
) {
if !self.config.enabled {
return;
}
let now = Instant::now();
let routes = self.routes.entry(destination.to_string()).or_default();
if let Some(existing) = routes.iter_mut().find(|r| r.next_hop == next_hop) {
let accept = seq_is_newer(sequence_number, existing.sequence_number)
|| (sequence_number == existing.sequence_number && hop_count <= existing.hop_count);
if accept {
existing.hop_count = hop_count;
existing.last_seen = now;
existing.quality = quality;
existing.sequence_number = sequence_number;
}
return;
}
if routes.len() >= self.config.max_routes_per_destination {
routes.sort_by(|a, b| {
seq_cmp(b.sequence_number, a.sequence_number)
.then_with(|| {
b.quality
.partial_cmp(&a.quality)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| a.hop_count.cmp(&b.hop_count))
});
if let Some(evicted) = routes.pop() {
if let Some(dests) = self.neighbor_destinations.get_mut(&evicted.next_hop) {
dests.retain(|d| d != destination);
if dests.is_empty() {
self.neighbor_destinations.remove(&evicted.next_hop);
}
}
}
}
routes.push(RouteEntry {
next_hop: next_hop.to_string(),
hop_count,
last_seen: now,
quality,
sequence_number,
});
let dests = self
.neighbor_destinations
.entry(next_hop.to_string())
.or_default();
if !dests.contains(&destination.to_string()) {
dests.push(destination.to_string());
}
self.enforce_size_limit();
}
pub fn get_route(&self, destination: &str) -> Option<&RouteEntry> {
if !self.config.enabled {
return None;
}
let routes = self.routes.get(destination)?;
let ttl = Duration::from_secs(self.config.route_ttl_secs);
let now = Instant::now();
routes
.iter()
.filter(|r| now.duration_since(r.last_seen) < ttl)
.max_by(|a, b| {
seq_cmp(a.sequence_number, b.sequence_number)
.then_with(|| {
a.quality
.partial_cmp(&b.quality)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| b.hop_count.cmp(&a.hop_count))
})
}
pub fn get_routes(&self, destination: &str) -> Vec<&RouteEntry> {
if !self.config.enabled {
return Vec::new();
}
let Some(routes) = self.routes.get(destination) else {
return Vec::new();
};
let ttl = Duration::from_secs(self.config.route_ttl_secs);
let now = Instant::now();
routes
.iter()
.filter(|r| now.duration_since(r.last_seen) < ttl)
.collect()
}
pub fn has_route(&self, destination: &str) -> bool {
self.get_route(destination).is_some()
}
pub fn remove_neighbor(&mut self, neighbor: &str) {
for routes in self.routes.values_mut() {
routes.retain(|r| r.next_hop != neighbor);
}
self.routes.retain(|_, v| !v.is_empty());
self.neighbor_destinations.remove(neighbor);
}
pub fn cleanup_expired(&mut self) {
let ttl = Duration::from_secs(self.config.route_ttl_secs);
let now = Instant::now();
for routes in self.routes.values_mut() {
routes.retain(|r| now.duration_since(r.last_seen) < ttl);
}
self.routes.retain(|_, v| !v.is_empty());
self.neighbor_destinations.clear();
for (dest, routes) in &self.routes {
for route in routes {
self.neighbor_destinations
.entry(route.next_hop.clone())
.or_default()
.push(dest.clone());
}
}
}
pub fn destination_count(&self) -> usize {
self.routes.len()
}
pub fn route_count(&self) -> usize {
self.routes.values().map(|v| v.len()).sum()
}
fn enforce_size_limit(&mut self) {
let route_ttl_secs = self.config.route_ttl_secs as f64;
while self.route_count() > self.config.max_routing_table_size {
let worst = self
.routes
.iter()
.flat_map(|(dest, routes)| routes.iter().map(move |r| (dest.clone(), r)))
.min_by(|(_, a), (_, b)| {
let score_a = composite_eviction_score(a, route_ttl_secs);
let score_b = composite_eviction_score(b, route_ttl_secs);
score_a
.partial_cmp(&score_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some((dest, route)) = worst {
let next_hop = route.next_hop.clone();
if let Some(routes) = self.routes.get_mut(&dest) {
routes.retain(|r| r.next_hop != next_hop);
if routes.is_empty() {
self.routes.remove(&dest);
}
}
if let Some(dests) = self.neighbor_destinations.get_mut(&next_hop) {
dests.retain(|d| d != &dest);
if dests.is_empty() {
self.neighbor_destinations.remove(&next_hop);
}
}
} else {
break;
}
}
}
}
fn seq_is_newer(new: u32, old: u32) -> bool {
new != old && new.wrapping_sub(old) < (1u32 << 31)
}
fn seq_cmp(a: u32, b: u32) -> std::cmp::Ordering {
if a == b {
std::cmp::Ordering::Equal
} else if seq_is_newer(a, b) {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
}
}
fn composite_eviction_score(route: &RouteEntry, route_ttl_secs: f64) -> f64 {
let age_secs = route.last_seen.elapsed().as_secs_f64();
let recency_factor = 1.0 - (age_secs / route_ttl_secs).min(1.0);
route.quality as f64 * recency_factor
}
impl Default for GradientRoutingTable {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PathConfig {
pub forward_to_top_k: usize,
pub max_congestion_level: f32,
pub gossip: GossipConfig,
pub gradient_routing: GradientRoutingConfig,
}
impl Default for PathConfig {
fn default() -> Self {
Self {
forward_to_top_k: DEFAULT_FORWARD_TO_TOP_K,
max_congestion_level: DEFAULT_MAX_CONGESTION_LEVEL,
gossip: GossipConfig::default(),
gradient_routing: GradientRoutingConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct PathScore {
pub signal: f32,
pub proximity: f32,
pub capacity: f32,
pub energy: f32,
pub total: f32,
}
#[derive(Debug, Clone)]
pub struct ForwardingDecision {
pub peers: Vec<String>,
pub gossip_applied: bool,
pub probability: f32,
pub dropped_count: usize,
}
pub struct PathSelector {
config: PathConfig,
relay_manager: RelayManager,
local_device_id: String,
routing_table: GradientRoutingTable,
}
impl PathSelector {
pub fn new() -> Self {
Self::with_config(PathConfig::default(), RelayManager::new())
}
pub fn with_config(config: PathConfig, relay_manager: RelayManager) -> Self {
let routing_table = GradientRoutingTable::with_config(config.gradient_routing.clone());
Self {
config,
relay_manager,
local_device_id: String::new(),
routing_table,
}
}
pub fn set_local_device_id(&mut self, device_id: impl Into<String>) {
self.local_device_id = device_id.into();
}
pub fn current_relay_role(&self) -> RelayRole {
self.relay_manager.current_role()
}
pub fn evaluate_relay_transition(
&mut self,
connection_count: usize,
battery_level: u8,
is_charging: bool,
) -> Option<RelayTransition> {
self.relay_manager
.evaluate_transition(connection_count, battery_level, is_charging)
}
pub fn compute_forwarding_probability(&self, visible_peer_count: usize) -> f32 {
let gossip = &self.config.gossip;
if !gossip.enabled || visible_peer_count <= gossip.small_network_threshold {
return 1.0;
}
let raw_probability = gossip.target_fanout as f32 / visible_peer_count as f32;
raw_probability.max(gossip.min_probability).min(1.0)
}
fn should_forward_to_peer(&self, message_id: &str, peer_id: &str, probability: f32) -> bool {
if probability >= 1.0 {
return true;
}
if probability <= 0.0 {
return false;
}
let mut hasher = DefaultHasher::new();
message_id.hash(&mut hasher);
peer_id.hash(&mut hasher);
self.local_device_id.hash(&mut hasher);
let hash = hasher.finish();
let hash_probability = (hash as f64 / u64::MAX as f64) as f32;
hash_probability < probability
}
pub fn select_paths_with_gossip(
&self,
message: &Message,
neighbors: &[NeighborInfo],
total_visible_peers: usize,
) -> ForwardingDecision {
if neighbors.is_empty() {
return ForwardingDecision {
peers: Vec::new(),
gossip_applied: false,
probability: 1.0,
dropped_count: 0,
};
}
let bypass_gossip = self.config.gossip.priority_bypass
&& matches!(
message.priority,
offline_protocol_core::MessagePriority::High
| offline_protocol_core::MessagePriority::Critical
);
let probability = if bypass_gossip {
1.0
} else {
self.compute_forwarding_probability(total_visible_peers.max(neighbors.len()))
};
let gossip_applied = probability < 1.0;
let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
.iter()
.map(|neighbor| {
let score = self.calculate_path_score(message, neighbor);
(neighbor.peer_id.clone(), score)
})
.collect();
scored_neighbors.retain(|(peer_id, _)| {
if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
if let Some(relay_info) = &neighbor.relay_info {
return relay_info.congestion_level <= self.config.max_congestion_level;
}
}
true
});
scored_neighbors.sort_by(|a, b| {
b.1.total
.partial_cmp(&a.1.total)
.unwrap_or(std::cmp::Ordering::Equal)
});
let candidates: Vec<String> = scored_neighbors
.into_iter()
.take(self.config.forward_to_top_k)
.map(|(peer_id, _)| peer_id)
.collect();
let initial_count = candidates.len();
let message_id = message.id.as_str();
let peers: Vec<String> = if gossip_applied {
candidates
.into_iter()
.filter(|peer_id| {
self.should_forward_to_peer(&message_id, peer_id.as_str(), probability)
})
.collect()
} else {
candidates
};
let dropped_count = initial_count.saturating_sub(peers.len());
ForwardingDecision {
peers,
gossip_applied,
probability,
dropped_count,
}
}
pub fn select_paths(&self, message: &Message, neighbors: &[NeighborInfo]) -> Vec<String> {
if neighbors.is_empty() {
return Vec::new();
}
let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
.iter()
.map(|neighbor| {
let score = self.calculate_path_score(message, neighbor);
(neighbor.peer_id.clone(), score)
})
.collect();
scored_neighbors.retain(|(peer_id, _)| {
if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
if let Some(relay_info) = &neighbor.relay_info {
return relay_info.congestion_level <= self.config.max_congestion_level;
}
}
true
});
scored_neighbors.sort_by(|a, b| {
b.1.total
.partial_cmp(&a.1.total)
.unwrap_or(std::cmp::Ordering::Equal)
});
scored_neighbors
.into_iter()
.take(self.config.forward_to_top_k)
.map(|(peer_id, _)| peer_id)
.collect()
}
fn calculate_path_score(&self, message: &Message, neighbor: &NeighborInfo) -> PathScore {
let signal_score = self.calculate_signal_score(neighbor);
let proximity_score = self.calculate_proximity_score(message, neighbor);
let capacity_score = self.calculate_capacity_score(neighbor);
let energy_score = self.calculate_energy_score(neighbor);
let total = (signal_score * SIGNAL_WEIGHT)
+ (proximity_score * PROXIMITY_WEIGHT)
+ (capacity_score * CAPACITY_WEIGHT)
+ (energy_score * ENERGY_WEIGHT);
PathScore {
signal: signal_score,
proximity: proximity_score,
capacity: capacity_score,
energy: energy_score,
total,
}
}
fn calculate_signal_score(&self, neighbor: &NeighborInfo) -> f32 {
let rssi = neighbor.rssi;
if rssi >= EXCELLENT_RSSI_THRESHOLD {
EXCELLENT_SIGNAL_SCORE
} else if rssi >= GOOD_RSSI_THRESHOLD {
GOOD_SIGNAL_BASE
+ ((rssi - GOOD_RSSI_THRESHOLD) as f32 * 30.0
/ (EXCELLENT_RSSI_THRESHOLD - GOOD_RSSI_THRESHOLD) as f32)
} else if rssi >= FAIR_RSSI_THRESHOLD {
FAIR_SIGNAL_BASE
+ ((rssi - FAIR_RSSI_THRESHOLD) as f32 * 30.0
/ (GOOD_RSSI_THRESHOLD - FAIR_RSSI_THRESHOLD) as f32)
} else {
((rssi - POOR_RSSI_MAX).max(0) as f32 * FAIR_SIGNAL_BASE
/ (FAIR_RSSI_THRESHOLD - POOR_RSSI_MAX) as f32)
.max(0.0)
}
}
fn calculate_proximity_score(&self, message: &Message, neighbor: &NeighborInfo) -> f32 {
if let Some(hops) = neighbor.hops_to_destination {
let remaining_ttl = message.ttl.value();
if hops == 0 {
100.0
} else {
let score = if hops < remaining_ttl {
100.0 - (hops as f32 / remaining_ttl as f32 * 50.0)
} else {
20.0
};
score.max(0.0)
}
} else {
neighbor.link_quality as f32
}
}
fn calculate_capacity_score(&self, neighbor: &NeighborInfo) -> f32 {
if let Some(relay_info) = &neighbor.relay_info {
let relay_score = self.relay_manager.calculate_relay_score(relay_info);
relay_score.min(EXCELLENT_SIGNAL_SCORE)
} else {
NON_RELAY_BASIC_CAPACITY
}
}
fn calculate_energy_score(&self, neighbor: &NeighborInfo) -> f32 {
if let Some(relay_info) = &neighbor.relay_info {
if relay_info.is_charging {
CHARGING_BATTERY_SCORE
} else {
relay_info.battery_level as f32
}
} else {
NON_RELAY_ASSUMED_BATTERY
}
}
pub fn select_best_path(
&self,
message: &Message,
neighbors: &[NeighborInfo],
) -> Option<String> {
self.select_paths(message, neighbors).into_iter().next()
}
pub fn should_route_around_congestion(&self, neighbor: &NeighborInfo) -> bool {
if let Some(relay_info) = &neighbor.relay_info {
relay_info.congestion_level > self.config.max_congestion_level
} else {
false
}
}
pub fn config(&self) -> &PathConfig {
&self.config
}
pub fn learn_route_from_message(
&mut self,
message: &Message,
from_neighbor: &str,
quality: f32,
) {
let sender = message.sender.as_str();
let hop_count = message.hop_count.value();
self.routing_table.learn_route(
sender,
from_neighbor,
hop_count,
quality,
0, );
}
pub fn get_route_to(&self, destination: &str) -> Option<&RouteEntry> {
self.routing_table.get_route(destination)
}
pub fn has_route_to(&self, destination: &str) -> bool {
self.routing_table.has_route(destination)
}
pub fn select_directed_path(
&self,
message: &Message,
neighbors: &[NeighborInfo],
) -> Option<String> {
let recipient = message.recipient.as_str();
for neighbor in neighbors {
if neighbor.peer_id == recipient {
return Some(neighbor.peer_id.clone());
}
}
if let Some(route) = self.routing_table.get_route(recipient) {
if neighbors.iter().any(|n| n.peer_id == route.next_hop) {
return Some(route.next_hop.clone());
}
}
self.select_best_path(message, neighbors)
}
pub fn remove_neighbor_routes(&mut self, neighbor: &str) {
self.routing_table.remove_neighbor(neighbor);
}
pub fn cleanup_routes(&mut self) {
self.routing_table.cleanup_expired();
}
pub fn routing_stats(&self) -> (usize, usize) {
(
self.routing_table.destination_count(),
self.routing_table.route_count(),
)
}
pub fn routing_table_mut(&mut self) -> &mut GradientRoutingTable {
&mut self.routing_table
}
}
impl Default for PathSelector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relay::{RelayInfo, RelayRole};
use offline_protocol_core::{AppId, UserId};
fn create_test_message() -> Message {
Message::new(
UserId::new("alice").unwrap(),
UserId::new("bob").unwrap(),
AppId::new("test").unwrap(),
"Test message",
)
}
fn create_neighbor(id: &str, rssi: i16, hops: Option<u8>, congestion: f32) -> NeighborInfo {
NeighborInfo {
peer_id: id.to_string(),
rssi,
hops_to_destination: hops,
link_quality: 80,
relay_info: Some(RelayInfo {
connection_count: 5,
battery_level: 70,
is_charging: false,
role: RelayRole::Relay,
link_quality: 80,
queue_depth: 10,
congestion_level: congestion,
}),
}
}
#[test]
fn test_path_selection_basic() {
let selector = PathSelector::new();
let message = create_test_message();
let neighbors = vec![
create_neighbor("peer1", -60, Some(2), 0.2),
create_neighbor("peer2", -70, Some(3), 0.3),
create_neighbor("peer3", -55, Some(1), 0.1),
];
let paths = selector.select_paths(&message, &neighbors);
assert!(!paths.is_empty());
assert!(paths.len() <= 3);
assert_eq!(paths[0], "peer3");
}
#[test]
fn test_congestion_filtering() {
let selector = PathSelector::new();
let message = create_test_message();
let neighbors = vec![
create_neighbor("peer1", -60, Some(2), 0.9), create_neighbor("peer2", -65, Some(3), 0.3),
];
let paths = selector.select_paths(&message, &neighbors);
assert!(!paths.contains(&"peer1".to_string()));
}
#[test]
fn test_direct_destination_preferred() {
let selector = PathSelector::new();
let message = create_test_message();
let neighbors = vec![
create_neighbor("peer1", -70, Some(5), 0.3),
create_neighbor("peer2", -70, Some(0), 0.1), ];
let paths = selector.select_paths(&message, &neighbors);
assert_eq!(paths[0], "peer2");
}
#[test]
fn test_signal_score_calculation() {
let selector = PathSelector::new();
let excellent = create_neighbor("peer1", -40, None, 0.1);
let score = selector.calculate_signal_score(&excellent);
assert_eq!(score, 100.0);
let poor = create_neighbor("peer2", -90, None, 0.1);
let score = selector.calculate_signal_score(&poor);
assert!(score < 40.0);
}
#[test]
fn test_empty_neighbors() {
let selector = PathSelector::new();
let message = create_test_message();
let paths = selector.select_paths(&message, &[]);
assert!(paths.is_empty());
}
#[test]
fn test_select_best_path() {
let selector = PathSelector::new();
let message = create_test_message();
let neighbors = vec![
create_neighbor("peer1", -60, Some(2), 0.2),
create_neighbor("peer2", -55, Some(1), 0.1),
];
let best = selector.select_best_path(&message, &neighbors);
assert_eq!(best, Some("peer2".to_string()));
}
#[test]
fn test_route_around_congestion() {
let selector = PathSelector::new();
let congested = create_neighbor("peer1", -60, Some(2), 0.9);
assert!(selector.should_route_around_congestion(&congested));
let normal = create_neighbor("peer2", -60, Some(2), 0.3);
assert!(!selector.should_route_around_congestion(&normal));
}
#[test]
fn test_gossip_probability_small_network() {
let selector = PathSelector::new();
assert_eq!(selector.compute_forwarding_probability(5), 1.0);
assert_eq!(selector.compute_forwarding_probability(10), 1.0);
}
#[test]
fn test_gossip_probability_large_network() {
let selector = PathSelector::new();
let prob = selector.compute_forwarding_probability(40);
assert!((prob - 0.15).abs() < 0.01);
let prob = selector.compute_forwarding_probability(20);
assert!((prob - 0.2).abs() < 0.01);
let prob = selector.compute_forwarding_probability(100);
assert!((prob - 0.15).abs() < 0.01);
}
#[test]
fn test_gossip_disabled() {
let config = PathConfig {
gossip: GossipConfig {
enabled: false,
..Default::default()
},
..Default::default()
};
let selector = PathSelector::with_config(config, RelayManager::new());
assert_eq!(selector.compute_forwarding_probability(1000), 1.0);
}
#[test]
fn test_gossip_forwarding_deterministic() {
let mut selector = PathSelector::new();
selector.set_local_device_id("device1");
let message = create_test_message();
let neighbors = vec![
create_neighbor("peer1", -60, Some(2), 0.2),
create_neighbor("peer2", -60, Some(2), 0.2),
create_neighbor("peer3", -60, Some(2), 0.2),
];
let decision1 = selector.select_paths_with_gossip(&message, &neighbors, 50);
let decision2 = selector.select_paths_with_gossip(&message, &neighbors, 50);
assert_eq!(decision1.peers, decision2.peers);
assert!(decision1.gossip_applied);
}
#[test]
fn test_gossip_priority_bypass() {
use offline_protocol_core::MessagePriority;
let mut selector = PathSelector::new();
selector.set_local_device_id("device1");
let high_priority_message = Message::builder(
UserId::new("alice").unwrap(),
UserId::new("bob").unwrap(),
AppId::new("test").unwrap(),
)
.content("Test")
.priority(MessagePriority::High)
.build();
let neighbors = vec![
create_neighbor("peer1", -60, Some(2), 0.2),
create_neighbor("peer2", -60, Some(2), 0.2),
];
let decision = selector.select_paths_with_gossip(&high_priority_message, &neighbors, 100);
assert!(!decision.gossip_applied);
assert_eq!(decision.probability, 1.0);
}
#[test]
fn test_gossip_reduces_forwarding_count() {
let mut selector = PathSelector::new();
selector.set_local_device_id("test-device");
let neighbors: Vec<NeighborInfo> = (0..20)
.map(|i| create_neighbor(&format!("peer{}", i), -60, Some(2), 0.2))
.collect();
let message = create_test_message();
let decision = selector.select_paths_with_gossip(&message, &neighbors, 100);
assert!(decision.gossip_applied);
assert!((decision.probability - 0.15).abs() < 0.01);
}
#[test]
fn test_dsdv_prefer_higher_sequence_number() {
let mut table = GradientRoutingTable::new();
let dest = "alice";
table.learn_route(dest, "peer_a", 2, 0.9, 1);
table.learn_route(dest, "peer_b", 3, 0.8, 2);
let route = table.get_route(dest).expect("should have route");
assert_eq!(route.next_hop, "peer_b");
assert_eq!(route.sequence_number, 2);
}
#[test]
fn test_dsdv_reject_stale_update_same_next_hop() {
let mut table = GradientRoutingTable::new();
let dest = "bob";
table.learn_route(dest, "peer_a", 2, 0.9, 2);
table.learn_route(dest, "peer_a", 5, 0.5, 1);
let route = table.get_route(dest).expect("should have route");
assert_eq!(route.next_hop, "peer_a");
assert_eq!(route.hop_count, 2);
assert_eq!(route.sequence_number, 2);
}
#[test]
fn test_dsdv_accept_same_sequence_better_hop_count() {
let mut table = GradientRoutingTable::new();
let dest = "carol";
table.learn_route(dest, "peer_a", 4, 0.7, 2);
table.learn_route(dest, "peer_a", 2, 0.8, 2);
let route = table.get_route(dest).expect("should have route");
assert_eq!(route.hop_count, 2);
assert_eq!(route.sequence_number, 2);
}
#[test]
fn test_global_eviction_prefers_low_quality_routes() {
let config = GradientRoutingConfig {
max_routing_table_size: 3,
max_routes_per_destination: 3,
route_ttl_secs: 300,
..Default::default()
};
let mut table = GradientRoutingTable::with_config(config);
table.learn_route("alice", "hop_good", 1, 0.9, 0);
table.learn_route("bob", "hop_low1", 3, 0.1, 0);
table.learn_route("carol", "hop_low2", 4, 0.1, 0);
assert_eq!(table.route_count(), 3);
table.learn_route("dave", "hop_new", 2, 0.5, 0);
assert_eq!(table.route_count(), 3);
assert!(
table.has_route("alice"),
"High-quality route to alice should survive eviction"
);
assert!(table.has_route("dave"), "Newly inserted route should exist");
}
#[test]
fn test_dsdv_eviction_removes_worst_route() {
let config = GradientRoutingConfig {
max_routes_per_destination: 2,
..Default::default()
};
let mut table = GradientRoutingTable::with_config(config);
let dest = "dest";
table.learn_route(dest, "hop1", 1, 0.9, 10);
table.learn_route(dest, "hop2", 2, 0.8, 10);
table.learn_route(dest, "hop3", 3, 0.7, 5);
let route = table.get_route(dest).expect("should have route");
assert_eq!(route.next_hop, "hop1");
let routes = table.get_routes(dest);
assert_eq!(routes.len(), 2);
assert!(routes.iter().any(|r| r.next_hop == "hop1"));
assert!(routes.iter().any(|r| r.next_hop == "hop3"));
assert!(
!routes.iter().any(|r| r.next_hop == "hop2"),
"hop2 (worst of original two) should be evicted"
);
}
#[test]
fn test_seq_is_newer_basic() {
assert!(seq_is_newer(2, 1));
assert!(seq_is_newer(100, 50));
assert!(!seq_is_newer(1, 2));
assert!(!seq_is_newer(5, 5)); }
#[test]
fn test_seq_is_newer_wrapping() {
assert!(seq_is_newer(0, u32::MAX));
assert!(seq_is_newer(1, u32::MAX));
assert!(seq_is_newer(5, u32::MAX - 2));
assert!(!seq_is_newer(u32::MAX, 0));
assert!(!seq_is_newer(u32::MAX - 2, 5));
}
#[test]
fn test_dsdv_wrapping_sequence_update() {
let mut table = GradientRoutingTable::new();
let dest = "alice";
table.learn_route(dest, "peer_a", 2, 0.9, u32::MAX - 1);
table.learn_route(dest, "peer_a", 2, 0.9, 0);
let route = table.get_route(dest).expect("should have route");
assert_eq!(
route.sequence_number, 0,
"Wrapped sequence should be accepted"
);
}
#[test]
fn test_seq_cmp_basic_and_wrapping() {
use std::cmp::Ordering;
assert_eq!(seq_cmp(5, 5), Ordering::Equal);
assert_eq!(seq_cmp(10, 5), Ordering::Greater);
assert_eq!(seq_cmp(5, 10), Ordering::Less);
assert_eq!(seq_cmp(0, u32::MAX), Ordering::Greater);
assert_eq!(seq_cmp(u32::MAX, 0), Ordering::Less);
}
#[test]
fn test_get_route_prefers_wrapped_sequence() {
let mut table = GradientRoutingTable::new();
let dest = "alice";
table.learn_route(dest, "peer_old", 2, 0.9, u32::MAX - 1);
table.learn_route(dest, "peer_new", 3, 0.7, 1);
let route = table.get_route(dest).expect("should have route");
assert_eq!(
route.next_hop, "peer_new",
"get_route must prefer wrapped (newer) sequence number"
);
assert_eq!(route.sequence_number, 1);
}
#[test]
fn test_per_dest_eviction_respects_wrapping() {
let config = GradientRoutingConfig {
max_routes_per_destination: 2,
..Default::default()
};
let mut table = GradientRoutingTable::with_config(config);
let dest = "bob";
table.learn_route(dest, "hop_old", 2, 0.8, u32::MAX - 5);
table.learn_route(dest, "hop_mid", 2, 0.8, u32::MAX);
table.learn_route(dest, "hop_new", 2, 0.8, 2);
let routes = table.get_routes(dest);
assert_eq!(routes.len(), 2);
assert!(
routes.iter().any(|r| r.next_hop == "hop_new"),
"Wrapped-sequence route must survive"
);
assert!(
routes.iter().any(|r| r.next_hop == "hop_mid"),
"Second-best sequence route must survive"
);
assert!(
!routes.iter().any(|r| r.next_hop == "hop_old"),
"Oldest sequence route should be evicted"
);
}
#[test]
fn test_seq_is_newer_half_space_boundary() {
let half = 1u32 << 31;
assert!(
!seq_is_newer(half, 0),
"Half-space distance is ambiguous, should not be newer"
);
assert!(!seq_is_newer(0, half), "Reverse half-space also not newer");
}
}