#![cfg_attr(coverage_nightly, coverage(off))]
use proptest::prelude::*;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct ModuleInfo {
pub name: String,
pub path: String,
pub size: usize,
pub complexity: u32,
}
#[derive(Debug, Clone)]
pub struct DagEdge {
pub from: String,
pub to: String,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub nodes: HashMap<String, ModuleInfo>,
pub edges: Vec<DagEdge>,
pub ranks: HashMap<String, f64>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
ranks: HashMap::new(),
}
}
pub fn detect_cycles(&self) -> Vec<Vec<String>> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
let mut cycles = Vec::new();
let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
for edge in &self.edges {
adj_list.entry(edge.from.clone())
.or_default()
.push(edge.to.clone());
}
for node in self.nodes.keys() {
if !visited.contains(node) {
let mut path = Vec::new();
self.dfs_cycle_detect(
node,
&adj_list,
&mut visited,
&mut rec_stack,
&mut path,
&mut cycles,
);
}
}
cycles
}
fn dfs_cycle_detect(
&self,
node: &str,
adj_list: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
path: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
path.push(node.to_string());
if let Some(neighbors) = adj_list.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
self.dfs_cycle_detect(
neighbor,
adj_list,
visited,
rec_stack,
path,
cycles,
);
} else if rec_stack.contains(neighbor) {
if let Some(start_idx) = path.iter().position(|n| n == neighbor) {
cycles.push(path[start_idx..].to_vec());
}
}
}
}
path.pop();
rec_stack.remove(node);
}
pub fn compute_pagerank(&mut self, iterations: usize) -> &HashMap<String, f64> {
let n = self.nodes.len() as f64;
if n == 0.0 {
return &self.ranks;
}
for node in self.nodes.keys() {
self.ranks.insert(node.clone(), 1.0 / n);
}
let mut incoming: HashMap<String, Vec<String>> = HashMap::new();
let mut outgoing_count: HashMap<String, usize> = HashMap::new();
for edge in &self.edges {
incoming.entry(edge.to.clone())
.or_default()
.push(edge.from.clone());
*outgoing_count.entry(edge.from.clone()).or_default() += 1;
}
let damping = 0.85;
for _ in 0..iterations {
let mut new_ranks = HashMap::new();
for node in self.nodes.keys() {
let mut rank = (1.0 - damping) / n;
if let Some(incomers) = incoming.get(node) {
for incomer in incomers {
let incomer_rank = self.ranks.get(incomer).unwrap_or(&0.0);
let outgoing = outgoing_count.get(incomer).unwrap_or(&1) as f64;
rank += damping * incomer_rank / outgoing;
}
}
new_ranks.insert(node.clone(), rank);
}
self.ranks = new_ranks;
}
&self.ranks
}
}
pub struct DagBuilder {
modules: Vec<ModuleInfo>,
edges: Vec<(usize, usize)>,
}
impl DagBuilder {
pub fn new() -> Self {
Self {
modules: Vec::new(),
edges: Vec::new(),
}
}
pub fn add_modules(mut self, modules: Vec<ModuleInfo>) -> Self {
self.modules = modules;
self
}
pub fn add_edges(mut self, edges: Vec<(usize, usize)>) -> Self {
self.edges = edges;
self
}
pub fn build(self) -> DependencyGraph {
let mut graph = DependencyGraph::new();
for module in self.modules {
graph.nodes.insert(module.name.clone(), module);
}
for (from_idx, to_idx) in self.edges {
if let (Some(from_module), Some(to_module)) =
(self.modules.get(from_idx), self.modules.get(to_idx)) {
graph.edges.push(DagEdge {
from: from_module.name.clone(),
to: to_module.name.clone(),
weight: 1.0,
});
}
}
graph
}
}
fn generate_random_dag(num_modules: usize, edge_probability: f64) -> (Vec<ModuleInfo>, Vec<(usize, usize)>) {
use rand::Rng;
let mut rng = rand::thread_rng();
let modules: Vec<ModuleInfo> = (0..num_modules)
.map(|i| ModuleInfo {
name: format!("module_{}", i),
path: format!("src/module_{}.rs", i),
size: rng.gen_range(100..10000),
complexity: rng.gen_range(1..100),
})
.collect();
let mut edges = Vec::new();
for i in 0..num_modules {
for j in (i + 1)..num_modules {
if rng.gen::<f64>() < edge_probability {
edges.push((i, j));
}
}
}
(modules, edges)
}
impl Arbitrary for ModuleInfo {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
"[a-z][a-z0-9_]{2,20}",
"[a-z0-9_/]+\\.rs",
100usize..10000,
1u32..100,
).prop_map(|(name, path, size, complexity)| {
ModuleInfo {
name,
path,
size,
complexity,
}
}).boxed()
}
}
prop_compose! {
fn arb_module_graph()
(num_modules in 1..50usize)
(
num_modules in Just(num_modules),
edge_probability in 0.0..0.3f64
) -> (Vec<ModuleInfo>, Vec<(usize, usize)>)
{
generate_random_dag(num_modules, edge_probability)
}
}
proptest! {
#[test]
fn dag_construction_preserves_acyclicity(
(modules, edges) in arb_module_graph()
) {
let graph = DagBuilder::new()
.add_modules(modules)
.add_edges(edges)
.build();
let cycles = graph.detect_cycles();
prop_assert!(
cycles.is_empty(),
"DAG construction introduced cycles: {:?}",
cycles
);
}
#[test]
fn dag_edges_reference_valid_nodes(
(modules, edges) in arb_module_graph()
) {
let graph = DagBuilder::new()
.add_modules(modules)
.add_edges(edges)
.build();
for edge in &graph.edges {
prop_assert!(
graph.nodes.contains_key(&edge.from),
"Edge references non-existent source: {}",
edge.from
);
prop_assert!(
graph.nodes.contains_key(&edge.to),
"Edge references non-existent target: {}",
edge.to
);
}
}
#[test]
fn pagerank_convergence(
(modules, edges) in arb_module_graph()
) {
let mut graph = DagBuilder::new()
.add_modules(modules)
.add_edges(edges)
.build();
if graph.nodes.is_empty() {
return Ok(());
}
let ranks = graph.compute_pagerank(100);
let sum: f64 = ranks.values().sum();
prop_assert!(
(sum - 1.0).abs() < 1e-6,
"PageRank sum {} != 1.0",
sum
);
for (&rank) in ranks.values() {
prop_assert!(
rank >= 0.0,
"Negative PageRank detected: {}",
rank
);
}
}
#[test]
fn empty_graph_no_cycles(_dummy in Just(())) {
let graph = DependencyGraph::new();
let cycles = graph.detect_cycles();
prop_assert!(cycles.is_empty());
}
#[test]
fn single_node_no_cycles(module in any::<ModuleInfo>()) {
let mut graph = DependencyGraph::new();
graph.nodes.insert(module.name.clone(), module);
let cycles = graph.detect_cycles();
prop_assert!(cycles.is_empty());
}
#[test]
fn linear_chain_no_cycles(num_modules in 2..20usize) {
let modules: Vec<ModuleInfo> = (0..num_modules)
.map(|i| ModuleInfo {
name: format!("module_{}", i),
path: format!("src/module_{}.rs", i),
size: 1000,
complexity: 10,
})
.collect();
let edges: Vec<(usize, usize)> = (0..num_modules - 1)
.map(|i| (i, i + 1))
.collect();
let graph = DagBuilder::new()
.add_modules(modules)
.add_edges(edges)
.build();
let cycles = graph.detect_cycles();
prop_assert!(
cycles.is_empty(),
"Linear chain should have no cycles"
);
}
#[test]
fn reverse_edge_creates_cycle(num_modules in 3..10usize) {
let modules: Vec<ModuleInfo> = (0..num_modules)
.map(|i| ModuleInfo {
name: format!("module_{}", i),
path: format!("src/module_{}.rs", i),
size: 1000,
complexity: 10,
})
.collect();
let mut edges: Vec<(usize, usize)> = (0..num_modules - 1)
.map(|i| (i, i + 1))
.collect();
edges.push((num_modules - 1, 0));
let mut graph = DependencyGraph::new();
for module in &modules {
graph.nodes.insert(module.name.clone(), module.clone());
}
for (from_idx, to_idx) in &edges {
if let (Some(from_module), Some(to_module)) =
(modules.get(*from_idx), modules.get(*to_idx)) {
graph.edges.push(DagEdge {
from: from_module.name.clone(),
to: to_module.name.clone(),
weight: 1.0,
});
}
}
let cycles = graph.detect_cycles();
prop_assert!(
!cycles.is_empty(),
"Should detect cycle with reverse edge"
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dag_builder() {
let modules = vec![
ModuleInfo {
name: "module_a".to_string(),
path: "src/a.rs".to_string(),
size: 1000,
complexity: 10,
},
ModuleInfo {
name: "module_b".to_string(),
path: "src/b.rs".to_string(),
size: 2000,
complexity: 20,
},
];
let graph = DagBuilder::new()
.add_modules(modules)
.add_edges(vec![(0, 1)])
.build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].from, "module_a");
assert_eq!(graph.edges[0].to, "module_b");
}
#[test]
fn test_cycle_detection() {
let mut graph = DependencyGraph::new();
graph.nodes.insert("a".to_string(), ModuleInfo {
name: "a".to_string(),
path: "a.rs".to_string(),
size: 100,
complexity: 5,
});
graph.nodes.insert("b".to_string(), ModuleInfo {
name: "b".to_string(),
path: "b.rs".to_string(),
size: 100,
complexity: 5,
});
graph.edges.push(DagEdge {
from: "a".to_string(),
to: "b".to_string(),
weight: 1.0,
});
graph.edges.push(DagEdge {
from: "b".to_string(),
to: "a".to_string(),
weight: 1.0,
});
let cycles = graph.detect_cycles();
assert!(!cycles.is_empty(), "Should detect cycle");
}
#[test]
fn test_pagerank() {
let modules = vec![
ModuleInfo {
name: "a".to_string(),
path: "a.rs".to_string(),
size: 100,
complexity: 5,
},
ModuleInfo {
name: "b".to_string(),
path: "b.rs".to_string(),
size: 100,
complexity: 5,
},
ModuleInfo {
name: "c".to_string(),
path: "c.rs".to_string(),
size: 100,
complexity: 5,
},
];
let mut graph = DagBuilder::new()
.add_modules(modules)
.add_edges(vec![(0, 1), (0, 2), (1, 2)])
.build();
let ranks = graph.compute_pagerank(100);
let sum: f64 = ranks.values().sum();
assert!((sum - 1.0).abs() < 1e-6);
let rank_c = ranks.get("c").unwrap();
let rank_a = ranks.get("a").unwrap();
assert!(rank_c > rank_a);
}
}