use super::make_parsed;
use crate::adapters::analyzers::coupling::*;
fn idx(graph: &ModuleGraph, name: &str) -> usize {
graph
.modules
.iter()
.position(|m| m == name)
.unwrap_or_else(|| panic!("module '{name}' not found in graph"))
}
fn has_module_edge(files: &[(&str, &str)], from: &str, to: &str) -> bool {
let parsed = make_parsed(files.to_vec());
let graph = graph::build_module_graph(&parsed);
graph.forward[idx(&graph, from)].contains(&idx(&graph, to))
}
type EdgeCase = (
&'static str,
&'static [(&'static str, &'static str)],
&'static str,
&'static str,
);
#[test]
fn test_build_graph_no_deps() {
let parsed = make_parsed(vec![
("main.rs", "fn main() {}"),
("config.rs", "pub struct Config;"),
]);
let graph = graph::build_module_graph(&parsed);
assert_eq!(graph.modules.len(), 2);
assert!(graph.forward.iter().all(|adj| adj.is_empty()));
}
#[test]
fn test_build_graph_simple_dep() {
let parsed = make_parsed(vec![
("main.rs", "use crate::config::Config; fn main() {}"),
("config.rs", "pub struct Config;"),
]);
let graph = graph::build_module_graph(&parsed);
let main_idx = idx(&graph, "main");
let config_idx = idx(&graph, "config");
assert!(graph.forward[main_idx].contains(&config_idx));
assert!(graph.forward[config_idx].is_empty());
}
#[test]
fn test_build_graph_self_dep_skipped() {
let parsed = make_parsed(vec![
(
"analyzer/mod.rs",
"use crate::analyzer::types::Foo; fn f() {}",
),
("analyzer/types.rs", "pub struct Foo;"),
]);
let graph = graph::build_module_graph(&parsed);
let analyzer_idx = idx(&graph, "analyzer");
assert!(
graph.forward[analyzer_idx].is_empty(),
"Self-dependencies should be skipped"
);
}
#[test]
fn test_build_graph_group_use() {
let parsed = make_parsed(vec![
(
"main.rs",
"use crate::{config::Config, pipeline::run}; fn main() {}",
),
("config.rs", "pub struct Config;"),
("pipeline.rs", "pub fn run() {}"),
]);
let graph = graph::build_module_graph(&parsed);
let main_idx = idx(&graph, "main");
assert_eq!(graph.forward[main_idx].len(), 2);
}
#[test]
fn test_build_graph_external_dep_ignored() {
let parsed = make_parsed(vec![
(
"main.rs",
"use std::collections::HashMap; use serde::config::Opt; fn main() {}",
),
("config.rs", "pub struct Config;"),
]);
let graph = graph::build_module_graph(&parsed);
let main_idx = idx(&graph, "main");
assert!(
graph.forward[main_idx].is_empty(),
"External dependencies must be ignored even when a path segment \
collides with a local module name"
);
}
#[test]
fn build_graph_records_use_edges_across_forms() {
let cases: &[EdgeCase] = &[
(
"multiple files of the same module",
&[
(
"config/mod.rs",
"use crate::analyzer::Foo; pub mod sections;",
),
("config/sections.rs", "pub struct Defaults;"),
("analyzer.rs", "pub struct Foo;"),
],
"config",
"analyzer",
),
(
"glob use",
&[
("main.rs", "use crate::analyzer::*; fn main() {}"),
("analyzer.rs", "pub fn analyze() {}"),
],
"main",
"analyzer",
),
(
"renamed use",
&[
("main.rs", "use crate::config::Config as Cfg; fn main() {}"),
("config.rs", "pub struct Config;"),
],
"main",
"config",
),
];
for (label, files, from, to) in cases {
assert!(
has_module_edge(files, from, to),
"case {label}: expected a {from} → {to} edge"
);
}
}
#[test]
fn test_metrics_empty() {
let graph = ModuleGraph {
modules: vec![],
forward: vec![],
};
let metrics = metrics::compute_coupling_metrics(&graph);
assert!(metrics.is_empty());
}
#[test]
fn test_metrics_simple_dep() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into()],
forward: vec![vec![1], vec![]],
};
let metrics = metrics::compute_coupling_metrics(&graph);
assert_eq!(metrics[0].afferent, 0);
assert_eq!(metrics[0].efferent, 1);
assert_eq!(metrics[1].afferent, 1);
assert_eq!(metrics[1].efferent, 0);
}
#[test]
fn test_metrics_instability_formula() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into(), "c".into()],
forward: vec![vec![1, 2], vec![], vec![]],
};
let metrics = metrics::compute_coupling_metrics(&graph);
assert!((metrics[0].instability - 1.0).abs() < f64::EPSILON);
assert!((metrics[1].instability).abs() < f64::EPSILON);
}
#[test]
fn test_metrics_isolated_module() {
let graph = ModuleGraph {
modules: vec!["isolated".into()],
forward: vec![vec![]],
};
let metrics = metrics::compute_coupling_metrics(&graph);
assert_eq!(metrics[0].afferent, 0);
assert_eq!(metrics[0].efferent, 0);
assert!((metrics[0].instability).abs() < f64::EPSILON);
}
#[test]
fn test_cycles_empty_graph() {
let graph = ModuleGraph {
modules: vec![],
forward: vec![],
};
let cycles = cycles::detect_cycles(&graph);
assert!(cycles.is_empty());
}
#[test]
fn test_cycles_no_cycles() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into(), "c".into()],
forward: vec![vec![1], vec![2], vec![]],
};
let cycles = cycles::detect_cycles(&graph);
assert!(cycles.is_empty());
}
#[test]
fn test_cycles_simple_cycle() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into()],
forward: vec![vec![1], vec![0]],
};
let cycles = cycles::detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
assert!(cycles[0].modules.contains(&"a".to_string()));
assert!(cycles[0].modules.contains(&"b".to_string()));
}
#[test]
fn test_cycles_complex_cycle() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into(), "c".into()],
forward: vec![vec![1], vec![2], vec![0]],
};
let cycles = cycles::detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].modules.len(), 3);
}
#[test]
fn test_cycles_self_loop_not_counted() {
let graph = ModuleGraph {
modules: vec!["a".into()],
forward: vec![vec![0]],
};
let cycles = cycles::detect_cycles(&graph);
assert!(
cycles.is_empty(),
"Self-loops should not be reported as cycles"
);
}
#[test]
fn test_cycles_two_independent_cycles() {
let graph = ModuleGraph {
modules: vec!["a".into(), "b".into(), "c".into(), "d".into()],
forward: vec![vec![1], vec![0], vec![3], vec![2]],
};
let cycles = cycles::detect_cycles(&graph);
assert_eq!(cycles.len(), 2);
}