erpc_analysis/algorithms/
path.rs
use log::info;
use std::sync::Arc;
use crate::db_trait::{AnalysisDatabase, AnalysisError};
use crate::models::metrics::PathAnalysisResult;
pub struct PathAnalyzer {
db_client: Arc<dyn AnalysisDatabase>,
}
impl PathAnalyzer {
pub fn new(db_client: Arc<dyn AnalysisDatabase>) -> Self {
Self { db_client }
}
pub async fn analyze_inter_community_paths(
&self,
projection_name: &str,
source_nodes: &[String],
target_nodes: &[String],
) -> Result<PathAnalysisResult, AnalysisError> {
info!("=== Starting Inter-Community Path Analysis ===");
info!(
"Analyzing paths between {} source nodes and {} target nodes",
source_nodes.len(),
target_nodes.len()
);
let result = self
.db_client
.analyze_paths_between_communities(
projection_name,
source_nodes,
target_nodes,
)
.await?;
info!("=== Inter-Community Path Analysis Complete ===");
Ok(result)
}
pub fn display_path_results(&self, result: &PathAnalysisResult) {
info!("=== Path Analysis Results ===");
if let Some(total_analyzed) = result.total_paths_analyzed {
info!("Total paths analyzed: {}", total_analyzed);
}
if let Some(connected_pairs) = result.connected_community_pairs {
info!("Connected community pairs: {}", connected_pairs);
}
if let Some(disconnected_pairs) = result.disconnected_community_pairs {
info!("Disconnected community pairs: {}", disconnected_pairs);
}
if let (Some(connected), Some(total)) = (
result.connected_community_pairs,
result.total_paths_analyzed,
) {
let connectivity_ratio = if total > 0 {
(connected as f64 / total as f64) * 100.0
} else {
0.0
};
info!("Connectivity ratio: {:.2}%", connectivity_ratio);
}
if let Some(avg_length) = result.average_path_length {
info!("Average path length: {:.2}", avg_length);
}
if let Some(max_length) = result.max_path_length {
info!("Maximum path length: {}", max_length);
}
if let Some(min_length) = result.min_path_length {
info!("Minimum path length: {}", min_length);
}
self.display_example_paths(&result.path_results, 5);
}
fn display_example_paths(
&self,
paths: &[crate::models::metrics::PathResult],
max_examples: usize,
) {
if paths.is_empty() {
info!("No paths to display");
return;
}
info!("=== Example Paths ===");
let connected_paths: Vec<_> =
paths.iter().filter(|p| p.path_exists).collect();
if !connected_paths.is_empty() {
info!("Connected Paths (showing up to {}):", max_examples);
for (i, path) in
connected_paths.iter().take(max_examples).enumerate()
{
let length_str = if let Some(length) = path.path_length {
format!("length: {}", length)
} else {
"length: unknown".to_string()
};
let cost_str = if let Some(cost) = path.path_cost {
format!("cost: {:.2}", cost)
} else {
"cost: unknown".to_string()
};
info!(
" {}. {} → {} ({}, {})",
i + 1,
path.source_fingerprint,
path.target_fingerprint,
length_str,
cost_str
);
}
}
let disconnected_paths: Vec<_> =
paths.iter().filter(|p| !p.path_exists).collect();
if !disconnected_paths.is_empty() {
info!("Disconnected Paths (showing up to {}):", max_examples);
for (i, path) in
disconnected_paths.iter().take(max_examples).enumerate()
{
info!(
" {}. {} → {} (no path)",
i + 1,
path.source_fingerprint,
path.target_fingerprint
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db_trait::mock::MockDatabase;
use crate::models::metrics::NodeMetrics;
#[tokio::test]
async fn test_analyze_inter_community_paths_with_connections() {
let nodes = vec![
NodeMetrics {
fingerprint: "RELAY_A".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_B".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_C".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_D".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
];
let mock_db = Arc::new(
MockDatabase::new().with_projection("test_projection", nodes),
);
let analyzer = PathAnalyzer::new(mock_db);
let source_nodes = vec!["RELAY_A".to_string(), "RELAY_B".to_string()];
let target_nodes = vec!["RELAY_C".to_string(), "RELAY_D".to_string()];
let result = analyzer
.analyze_inter_community_paths(
"test_projection",
&source_nodes,
&target_nodes,
)
.await
.expect("Path analysis should succeed");
assert_eq!(result.total_paths_analyzed, Some(4)); assert_eq!(result.connected_community_pairs, Some(4));
assert_eq!(result.disconnected_community_pairs, Some(0));
assert!(result.average_path_length.is_some());
assert_eq!(result.path_results.len(), 4);
for path in &result.path_results {
assert!(path.path_exists);
assert!(path.path_length.is_some());
assert!(path.path_cost.is_some());
}
}
}