use trueno_graph::{bfs, find_callers, find_patterns, pagerank, CsrGraph, NodeId, Pattern};
fn build_test_graph() -> CsrGraph {
let edges = vec![
(NodeId(0), NodeId(1), 1.0),
(NodeId(0), NodeId(2), 1.0),
(NodeId(1), NodeId(3), 1.0),
(NodeId(1), NodeId(4), 1.0),
(NodeId(2), NodeId(4), 1.0),
];
CsrGraph::from_edge_list(&edges).unwrap()
}
fn build_large_test_graph(num_nodes: usize) -> CsrGraph {
let mut edges = Vec::new();
for i in 0..(num_nodes - 1) {
edges.push((NodeId(i as u32), NodeId((i + 1) as u32), 1.0));
}
for i in 0..(num_nodes / 10) {
let src = i * 10;
let dst = (i * 10 + 5) % num_nodes;
if src != dst {
edges.push((NodeId(src as u32), NodeId(dst as u32), 0.5));
}
}
CsrGraph::from_edge_list(&edges).unwrap()
}
#[test]
fn test_csr_graph_construction() {
let graph = build_test_graph();
assert!(graph.num_nodes() >= 5, "Graph should have at least 5 nodes");
assert_eq!(graph.num_edges(), 5, "Graph should have 5 edges");
}
#[test]
fn test_csr_outgoing_neighbors() {
let graph = build_test_graph();
let neighbors = graph.outgoing_neighbors(NodeId(0)).unwrap();
assert_eq!(neighbors.len(), 2);
let neighbors = graph.outgoing_neighbors(NodeId(1)).unwrap();
assert_eq!(neighbors.len(), 2);
let neighbors = graph.outgoing_neighbors(NodeId(3)).unwrap();
assert_eq!(neighbors.len(), 0);
}
#[test]
fn test_csr_incoming_neighbors() {
let graph = build_test_graph();
let callers = graph.incoming_neighbors(NodeId(4)).unwrap();
assert_eq!(callers.len(), 2, "Node 4 should have 2 incoming edges");
let callers = graph.incoming_neighbors(NodeId(0)).unwrap();
assert_eq!(callers.len(), 0, "Node 0 should have no incoming edges");
}
#[test]
fn test_bfs_reachability() {
let graph = build_test_graph();
let reachable = bfs(&graph, NodeId(0)).expect("BFS should succeed");
assert_eq!(reachable.len(), 5, "All 5 nodes should be reachable");
assert!(reachable.contains(&0), "Source should be in result");
assert!(reachable.contains(&1));
assert!(reachable.contains(&2));
assert!(reachable.contains(&3));
assert!(reachable.contains(&4));
}
#[test]
fn test_bfs_disconnected() {
let edges = vec![
(NodeId(0), NodeId(1), 1.0),
(NodeId(2), NodeId(3), 1.0), ];
let graph = CsrGraph::from_edge_list(&edges).unwrap();
let reachable = bfs(&graph, NodeId(0)).expect("BFS should succeed");
assert_eq!(reachable.len(), 2, "Only component 0-1 should be reachable");
assert!(reachable.contains(&0));
assert!(reachable.contains(&1));
assert!(!reachable.contains(&2), "Node 2 should NOT be reachable");
assert!(!reachable.contains(&3), "Node 3 should NOT be reachable");
}
#[test]
fn test_find_callers() {
let graph = build_test_graph();
let callers = find_callers(&graph, NodeId(4), 10).expect("find_callers should succeed");
assert!(callers.contains(&1), "Node 1 should be a caller of 4");
assert!(callers.contains(&2), "Node 2 should be a caller of 4");
assert!(callers.contains(&0), "Node 0 should be a caller of 4 (indirectly)");
}
#[test]
fn test_find_callers_depth_limit() {
let graph = build_test_graph();
let callers = find_callers(&graph, NodeId(4), 1).expect("find_callers should succeed");
assert!(callers.contains(&1), "Node 1 directly calls 4");
assert!(callers.contains(&2), "Node 2 directly calls 4");
assert!(!callers.contains(&0), "Node 0 should NOT be included at depth 1");
}
#[test]
fn test_pagerank_basic() {
let graph = build_test_graph();
let scores = pagerank(&graph, 20, 1e-6).expect("PageRank should succeed");
for score in &scores {
assert!(*score >= 0.0, "PageRank scores should be non-negative");
}
let sum: f32 = scores.iter().sum();
assert!((sum - 1.0).abs() < 0.1, "PageRank scores should sum to ~1.0, got {sum}");
}
#[test]
fn test_pagerank_larger_graph() {
let graph = build_large_test_graph(100);
let scores = pagerank(&graph, 50, 1e-6).expect("PageRank should succeed");
assert!(scores.len() >= 100, "Should have scores for all 100 nodes");
for score in &scores {
assert!(*score >= 0.0, "All PageRank scores should be non-negative");
}
}
#[test]
fn test_louvain_basic() {
use trueno_graph::louvain;
let edges = vec![
(NodeId(0), NodeId(1), 1.0),
(NodeId(1), NodeId(0), 1.0),
(NodeId(1), NodeId(2), 1.0),
(NodeId(2), NodeId(1), 1.0),
(NodeId(0), NodeId(2), 1.0),
(NodeId(2), NodeId(0), 1.0),
(NodeId(3), NodeId(4), 1.0),
(NodeId(4), NodeId(3), 1.0),
(NodeId(4), NodeId(5), 1.0),
(NodeId(5), NodeId(4), 1.0),
(NodeId(3), NodeId(5), 1.0),
(NodeId(5), NodeId(3), 1.0),
(NodeId(2), NodeId(3), 0.1),
];
let graph = CsrGraph::from_edge_list(&edges).unwrap();
let result = louvain(&graph).expect("Louvain should succeed");
assert!(
result.num_communities >= 2,
"Should detect at least 2 communities, got {}",
result.num_communities
);
assert!(result.modularity > 0.0, "Modularity should be positive, got {}", result.modularity);
}
#[test]
fn test_pattern_detection_circular() {
let edges = vec![
(NodeId(0), NodeId(1), 1.0),
(NodeId(1), NodeId(2), 1.0),
(NodeId(2), NodeId(0), 1.0), ];
let graph = CsrGraph::from_edge_list(&edges).unwrap();
let patterns = find_patterns(&graph, &Pattern::circular_dependency(3))
.expect("Pattern detection should succeed");
assert!(!patterns.is_empty(), "Should detect the circular dependency (3-cycle)");
}
#[test]
fn test_pattern_detection_god_class() {
let mut edges = Vec::new();
for i in 1..=10 {
edges.push((NodeId(0), NodeId(i), 1.0));
}
let graph = CsrGraph::from_edge_list(&edges).unwrap();
let patterns =
find_patterns(&graph, &Pattern::god_class(5)).expect("Pattern detection should succeed");
assert!(!patterns.is_empty(), "Should detect god class (node 0 has 10 callees)");
}
#[cfg(feature = "gpu")]
mod gpu_tests {
use super::*;
use trueno_graph::{gpu_bfs, GpuDevice};
#[tokio::test]
async fn test_gpu_device_init() {
match GpuDevice::new().await {
Ok(_device) => {
}
Err(e) => {
eprintln!("GPU initialization failed (expected on CI): {e}");
}
}
}
#[tokio::test]
async fn test_gpu_cpu_bfs_equivalence() {
let Ok(_device) = GpuDevice::new().await else {
eprintln!("Skipping GPU test (no GPU available)");
return;
};
let graph = build_test_graph();
let cpu_result = bfs(&graph, NodeId(0)).expect("CPU BFS should succeed");
let gpu_result = gpu_bfs(&graph, NodeId(0)).await.expect("GPU BFS should succeed");
let cpu_set: std::collections::HashSet<_> = cpu_result.into_iter().collect();
let gpu_set: std::collections::HashSet<_> = gpu_result.reachable.into_iter().collect();
assert_eq!(cpu_set, gpu_set, "CPU and GPU BFS should produce same reachable nodes");
}
#[tokio::test]
async fn test_gpu_bfs_large_graph() {
let Ok(_device) = GpuDevice::new().await else {
eprintln!("Skipping GPU test (no GPU available)");
return;
};
let graph = build_large_test_graph(1000);
let result = gpu_bfs(&graph, NodeId(0)).await.expect("GPU BFS should succeed");
assert!(!result.reachable.is_empty(), "GPU BFS should find reachable nodes");
}
}
#[cfg(test)]
mod backend_completeness {
use trueno_graph::{
bfs, find_callers, find_patterns, louvain, pagerank, CsrGraph, NodeId, Pattern,
};
#[test]
fn test_algorithm_functions_exist() {
let _: fn(&CsrGraph, NodeId) -> anyhow::Result<Vec<u32>> = bfs;
let _: fn(&CsrGraph, NodeId, usize) -> anyhow::Result<Vec<u32>> = find_callers;
let _: fn(&CsrGraph, usize, f32) -> anyhow::Result<Vec<f32>> = pagerank;
let _: fn(&CsrGraph, &Pattern) -> anyhow::Result<_> = find_patterns;
let _: fn(&CsrGraph) -> anyhow::Result<_> = louvain;
}
#[test]
fn test_csr_graph_methods_exist() {
let edges = vec![(NodeId(0), NodeId(1), 1.0)];
let graph = CsrGraph::from_edge_list(&edges).unwrap();
let _ = graph.num_nodes();
let _ = graph.num_edges();
let _ = graph.outgoing_neighbors(NodeId(0));
let _ = graph.incoming_neighbors(NodeId(0));
}
#[test]
fn test_pattern_constructors_exist() {
let _ = Pattern::god_class(5);
let _ = Pattern::circular_dependency(3);
let _ = Pattern::dead_code();
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_functions_exist() {
use trueno_graph::{gpu_bfs, GpuDevice};
let _ = gpu_bfs;
let _ = GpuDevice::new;
}
}