use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConnectedComponent {
pub component_id: i64,
pub relay_fingerprints: Vec<String>,
pub size: usize,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComponentAnalysisResult {
pub components: Vec<ConnectedComponent>,
pub total_components: Option<usize>,
pub largest_component_size: Option<usize>,
pub smallest_component_size: Option<usize>,
pub component_size_distribution: Option<HashMap<usize, usize>>,
pub isolation_ratio: Option<f64>,
pub modularity: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub enum ClassificationType {
#[default]
Geographic,
ASN,
Family,
Flags,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassificationGroup {
pub identifier: String,
pub relay_fingerprints: Vec<String>,
pub component_mapping: HashMap<i64, usize>, pub isolation_score: f64,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ClassificationMetrics {
pub total_groups: usize,
pub groups_with_partitions: usize,
pub classification_coverage: f64,
pub largest_group_size: usize,
pub average_group_size: f64,
pub diversity_score: f64, pub partition_correlation: f64,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct PartitionClassificationResult {
pub classification_type: ClassificationType,
pub groups: Vec<ClassificationGroup>,
pub metrics: ClassificationMetrics,
pub unclassified_relays: Vec<String>, }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_analysis_result_comprehensive() {
let component1 = ConnectedComponent {
component_id: 1,
relay_fingerprints: vec![
"RELAY001".to_string(),
"RELAY002".to_string(),
"RELAY003".to_string(),
],
size: 3,
};
let component2 = ConnectedComponent {
component_id: 2,
relay_fingerprints: vec!["RELAY004".to_string()],
size: 1,
};
let mut size_distribution = HashMap::new();
size_distribution.insert(1, 1); size_distribution.insert(3, 1); let result = ComponentAnalysisResult {
components: vec![component1, component2],
total_components: Some(2),
largest_component_size: Some(3),
smallest_component_size: Some(1),
component_size_distribution: Some(size_distribution.clone()),
isolation_ratio: Some(75.0),
modularity: None,
};
assert_eq!(result.components.len(), 2);
assert_eq!(result.total_components.unwrap(), 2);
assert_eq!(result.largest_component_size.unwrap(), 3);
assert_eq!(result.smallest_component_size.unwrap(), 1);
assert_eq!(result.isolation_ratio.unwrap(), 75.0);
assert_eq!(
result.component_size_distribution.unwrap(),
size_distribution
);
assert_eq!(result.components[0].size, 3);
assert_eq!(result.components[1].size, 1);
assert_eq!(result.components[0].relay_fingerprints.len(), 3);
}
#[test]
fn test_isolation_ratio_edge_cases() {
let perfect_isolation = ComponentAnalysisResult {
components: vec![],
total_components: Some(1),
largest_component_size: Some(100),
smallest_component_size: Some(100),
component_size_distribution: None,
isolation_ratio: Some(100.0),
modularity: None,
};
assert_eq!(perfect_isolation.isolation_ratio.unwrap(), 100.0);
let complete_fragmentation = ComponentAnalysisResult {
components: vec![],
total_components: Some(50),
largest_component_size: Some(1),
smallest_component_size: Some(1),
component_size_distribution: None,
isolation_ratio: Some(2.0), modularity: None,
};
assert_eq!(complete_fragmentation.isolation_ratio.unwrap(), 2.0);
}
#[test]
fn test_classification_group_structure() {
let mut component_mapping = HashMap::new();
component_mapping.insert(1, 5); component_mapping.insert(2, 2); let group = ClassificationGroup {
identifier: "US".to_string(),
relay_fingerprints: vec![
"RELAY001".to_string(),
"RELAY002".to_string(),
"RELAY003".to_string(),
"RELAY004".to_string(),
"RELAY005".to_string(),
"RELAY006".to_string(),
"RELAY007".to_string(),
],
component_mapping: component_mapping.clone(),
isolation_score: 28.6, };
assert_eq!(group.identifier, "US");
assert_eq!(group.relay_fingerprints.len(), 7);
assert_eq!(group.component_mapping, component_mapping);
assert!((group.isolation_score - 28.6).abs() < 0.1);
}
#[test]
fn test_isolation_score_calculation_logic() {
let mut component_mapping = HashMap::new();
component_mapping.insert(1, 10);
let total_relays = 10;
let largest_component_size =
component_mapping.values().max().unwrap_or(&0);
let non_largest_component_relays =
total_relays - largest_component_size;
let isolation_score = (non_largest_component_relays as f64
/ total_relays as f64)
* 100.0;
assert_eq!(isolation_score, 0.0);
let mut fragmented_mapping = HashMap::new();
fragmented_mapping.insert(1, 2); fragmented_mapping.insert(2, 1); fragmented_mapping.insert(3, 1); fragmented_mapping.insert(4, 1); fragmented_mapping.insert(5, 1); fragmented_mapping.insert(6, 1); fragmented_mapping.insert(7, 1); let total_relays = 8;
let largest_component_size =
fragmented_mapping.values().max().unwrap_or(&0);
let non_largest_component_relays =
total_relays - largest_component_size;
let isolation_score = (non_largest_component_relays as f64
/ total_relays as f64)
* 100.0;
assert_eq!(isolation_score, 75.0);
let mut moderate_mapping = HashMap::new();
moderate_mapping.insert(1, 4); moderate_mapping.insert(2, 3); moderate_mapping.insert(3, 2); moderate_mapping.insert(4, 1); let total_relays = 10;
let largest_component_size =
moderate_mapping.values().max().unwrap_or(&0);
let non_largest_component_relays =
total_relays - largest_component_size;
let isolation_score = (non_largest_component_relays as f64
/ total_relays as f64)
* 100.0;
assert_eq!(isolation_score, 60.0);
}
#[test]
fn test_classification_metrics() {
let metrics = ClassificationMetrics {
total_groups: 10,
groups_with_partitions: 3,
classification_coverage: 95.5,
largest_group_size: 150,
average_group_size: 45.7,
diversity_score: 0.85,
partition_correlation: 0.72,
};
assert_eq!(metrics.total_groups, 10);
assert_eq!(metrics.groups_with_partitions, 3);
assert!((metrics.classification_coverage - 95.5).abs() < 0.1);
assert_eq!(metrics.largest_group_size, 150);
assert!((metrics.average_group_size - 45.7).abs() < 0.1);
assert!((metrics.diversity_score - 0.85).abs() < 0.1);
assert!((metrics.partition_correlation - 0.72).abs() < 0.1);
}
#[test]
fn test_partition_classification_result() {
let group = ClassificationGroup {
identifier: "DE".to_string(),
relay_fingerprints: vec!["RELAY001".to_string()],
component_mapping: HashMap::new(),
isolation_score: 100.0,
};
let metrics = ClassificationMetrics {
total_groups: 1,
groups_with_partitions: 0,
classification_coverage: 90.0,
largest_group_size: 1,
average_group_size: 1.0,
diversity_score: 1.0,
partition_correlation: 0.0,
};
let result = PartitionClassificationResult {
classification_type: ClassificationType::Geographic,
groups: vec![group],
metrics,
unclassified_relays: vec!["RELAY_UNKNOWN".to_string()],
};
assert_eq!(result.classification_type, ClassificationType::Geographic);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.unclassified_relays.len(), 1);
assert_eq!(result.metrics.total_groups, 1);
}
}