erpc_analysis/algorithms/
components.rs
use log::info;
use std::sync::Arc;
use crate::db_trait::{AnalysisDatabase, AnalysisError};
use crate::models::partitions::ComponentAnalysisResult;
pub struct ComponentAnalyzer {
db_client: Arc<dyn AnalysisDatabase>,
}
impl ComponentAnalyzer {
pub fn new(db_client: Arc<dyn AnalysisDatabase>) -> Self {
Self { db_client }
}
pub async fn analyze_weakly_connected_components(
&self,
projection_name: &str,
) -> Result<ComponentAnalysisResult, AnalysisError> {
info!("=== Starting Weakly Connected Components Analysis ===");
let result = self
.db_client
.calculate_weakly_connected_components(projection_name)
.await?;
info!("=== WCC Analysis Complete ===");
info!(
"Network has {} weakly connected components",
result.components.len()
);
Ok(result)
}
pub async fn analyze_strongly_connected_components(
&self,
projection_name: &str,
) -> Result<ComponentAnalysisResult, AnalysisError> {
info!("=== Starting Strongly Connected Components Analysis ===");
let result = self
.db_client
.calculate_strongly_connected_components(projection_name)
.await?;
info!("=== SCC Analysis Complete ===");
info!(
"Network has {} strongly connected components",
result.components.len()
);
Ok(result)
}
pub fn display_weak_connectivity_analysis(
&self,
result: &ComponentAnalysisResult,
config: &crate::config::AnalysisSettings,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Weakly Connected Components Analysis:");
info!("Total Components: {}", result.total_components.unwrap_or(0));
info!(
"Largest Component Size: {}",
result.largest_component_size.unwrap_or(0)
);
info!(
"Smallest Component Size: {}",
result.smallest_component_size.unwrap_or(0)
);
let isolation_ratio = result.isolation_ratio.unwrap_or(0.0);
info!("Isolation Ratio: {:.2}%", isolation_ratio);
if isolation_ratio < config.isolation_ratio_threshold {
info!(
"⚠️ Network fragmentation detected: Isolation ratio \
{:.2}% is below threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
} else {
info!(
"✅ Network connectivity is healthy: Isolation ratio \
{:.2}% is above threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
}
if config.calculate_distribution {
if let Some(distribution) = &result.component_size_distribution {
info!("Component Size Distribution:");
let mut sizes: Vec<_> = distribution.iter().collect();
sizes.sort_by(|a, b| b.0.cmp(a.0)); for (size, count) in
sizes.iter().take(config.max_display_components.min(5))
{
info!("{} component(s) with {} relays each", count, size);
}
}
}
if result.components.len() > 1 {
info!(
"Top {} Largest Components:",
config.max_display_components.min(result.components.len())
);
for (i, component) in result
.components
.iter()
.take(config.max_display_components)
.enumerate()
{
info!(
"{}. Component {}: {} relays",
i + 1,
component.component_id,
component.size
);
}
}
Ok(())
}
pub fn display_strong_connectivity_analysis(
&self,
result: &ComponentAnalysisResult,
config: &crate::config::AnalysisSettings,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Strong Connectivity Analysis:");
info!("Total Components: {}", result.total_components.unwrap_or(0));
info!(
"Largest Component Size: {}",
result.largest_component_size.unwrap_or(0)
);
info!(
"Smallest Component Size: {}",
result.smallest_component_size.unwrap_or(0)
);
let isolation_ratio = result.isolation_ratio.unwrap_or(0.0);
info!("Isolation Ratio: {:.2}%", isolation_ratio);
if isolation_ratio < config.isolation_ratio_threshold {
info!(
"⚠️ Network fragmentation detected: Isolation ratio \
{:.2}% is below threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
} else {
info!(
"✅ Network connectivity is healthy: Isolation ratio \
{:.2}% is above threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
}
if config.calculate_distribution {
if let Some(distribution) = &result.component_size_distribution {
info!("Component Size Distribution:");
let mut sizes: Vec<_> = distribution.iter().collect();
sizes.sort_by(|a, b| b.0.cmp(a.0)); for (size, count) in
sizes.iter().take(config.max_display_components.min(5))
{
info!("{} component(s) with {} relays each", count, size);
}
}
}
if result.components.len() > 1 {
info!(
"Top {} Largest Components:",
config.max_display_components.min(result.components.len())
);
for (i, component) in result
.components
.iter()
.take(config.max_display_components)
.enumerate()
{
info!(
"{}. Component {}: {} relays",
i + 1,
component.component_id,
component.size
);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db_trait::mock::MockDatabase;
use crate::models::metrics::NodeMetrics;
#[tokio::test]
async fn test_wcc_algorithm_correctness() {
let nodes = vec![
NodeMetrics {
fingerprint: "RELAY_A".to_string(),
in_degree: 0,
out_degree: 1,
total_degree: 1,
},
NodeMetrics {
fingerprint: "RELAY_B".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_C".to_string(),
in_degree: 1,
out_degree: 0,
total_degree: 1,
},
NodeMetrics {
fingerprint: "RELAY_D".to_string(),
in_degree: 0,
out_degree: 1,
total_degree: 1,
},
NodeMetrics {
fingerprint: "RELAY_E".to_string(),
in_degree: 1,
out_degree: 0,
total_degree: 1,
},
NodeMetrics {
fingerprint: "RELAY_F".to_string(),
in_degree: 0,
out_degree: 0,
total_degree: 0,
},
];
let db =
Arc::new(MockDatabase::new().with_projection("test_wcc", nodes));
let analyzer = ComponentAnalyzer::new(db);
let result = analyzer
.analyze_weakly_connected_components("test_wcc")
.await
.expect("WCC analysis should succeed");
assert_eq!(result.total_components, Some(2));
assert_eq!(result.components.len(), 2); assert!(result.largest_component_size.is_some());
assert!(result.smallest_component_size.is_some());
assert!(result.isolation_ratio.is_some());
let total_fingerprints: usize =
result.components.iter().map(|c| c.size).sum();
assert_eq!(total_fingerprints, 6);
}
#[tokio::test]
async fn test_scc_algorithm_correctness() {
let nodes = vec![
NodeMetrics {
fingerprint: "RELAY_A".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_B".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_C".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_D".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
];
let db =
Arc::new(MockDatabase::new().with_projection("test_scc", nodes));
let analyzer = ComponentAnalyzer::new(db);
let result = analyzer
.analyze_strongly_connected_components("test_scc")
.await
.expect("SCC analysis should succeed");
assert_eq!(result.total_components, Some(2));
assert_eq!(result.components.len(), 2);
assert!(result.largest_component_size.is_some());
assert!(result.smallest_component_size.is_some());
assert!(result.isolation_ratio.is_some());
let total_fingerprints: usize =
result.components.iter().map(|c| c.size).sum();
assert_eq!(total_fingerprints, 4);
}
#[tokio::test]
async fn test_small_graphs_and_errors() {
let db = Arc::new(MockDatabase::new());
let analyzer = ComponentAnalyzer::new(db.clone());
let wcc_result = analyzer
.analyze_weakly_connected_components("nonexistent")
.await;
assert!(
wcc_result.is_err(),
"Should fail for non-existent projection"
);
let scc_result = analyzer
.analyze_strongly_connected_components("nonexistent")
.await;
assert!(
scc_result.is_err(),
"Should fail for non-existent projection"
);
let isolated_nodes = vec![
NodeMetrics {
fingerprint: "ISOLATED_1".to_string(),
in_degree: 0,
out_degree: 0,
total_degree: 0,
},
NodeMetrics {
fingerprint: "ISOLATED_2".to_string(),
in_degree: 0,
out_degree: 0,
total_degree: 0,
},
];
let db_isolated = Arc::new(
MockDatabase::new()
.with_projection("isolated_test", isolated_nodes),
);
let analyzer_isolated = ComponentAnalyzer::new(db_isolated);
let result = analyzer_isolated
.analyze_weakly_connected_components("isolated_test")
.await
.expect("Isolated nodes analysis should succeed");
assert_eq!(result.total_components, Some(1));
assert_eq!(result.components.len(), 1);
assert_eq!(result.largest_component_size, Some(2));
assert_eq!(result.isolation_ratio.unwrap(), 100.0);
}
#[tokio::test]
async fn test_component_analysis_edge_cases() {
let single_node = vec![NodeMetrics {
fingerprint: "SINGLE_NODE".to_string(),
in_degree: 0,
out_degree: 0,
total_degree: 0,
}];
let db_single = Arc::new(
MockDatabase::new().with_projection("single_node", single_node),
);
let analyzer_single = ComponentAnalyzer::new(db_single);
let result = analyzer_single
.analyze_weakly_connected_components("single_node")
.await
.expect("Single node analysis should succeed");
assert_eq!(result.total_components, Some(1));
assert_eq!(result.largest_component_size, Some(1));
assert_eq!(result.smallest_component_size, Some(1));
assert!((result.isolation_ratio.unwrap() - 100.0).abs() < 0.01);
let fail_nodes = vec![NodeMetrics {
fingerprint: "FAIL_RELAY".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
}];
let db_fail = Arc::new(
MockDatabase::new()
.with_projection("fail_test", fail_nodes)
.fail_on("calculate_strongly_connected_components"),
);
let analyzer_fail = ComponentAnalyzer::new(db_fail);
let result = analyzer_fail
.analyze_strongly_connected_components("fail_test")
.await;
assert!(result.is_err(), "Should fail when database operation fails");
}
}