use std::collections::{HashMap, HashSet};
use std::fs;
use tempfile::TempDir;
use tokensave::db::Database;
use tokensave::graph::git::file_churn;
use tokensave::graph::queries::GraphQueryManager;
use tokensave::graph::traversal::GraphTraverser;
use tokensave::tokensave::TokenSave;
use tokensave::types::*;
async fn setup_db() -> (Database, TempDir) {
let dir = TempDir::new().expect("failed to create temp dir");
let db_path = dir.path().join("test.db");
let (db, _) = Database::initialize(&db_path)
.await
.expect("failed to initialize database");
(db, dir)
}
fn make_node(id: &str, name: &str, file_path: &str, visibility: Visibility) -> Node {
Node {
id: id.to_string(),
kind: NodeKind::Function,
name: name.to_string(),
qualified_name: format!("crate::{name}"),
file_path: file_path.to_string(),
start_line: 1,
attrs_start_line: 1,
end_line: 10,
start_column: 0,
end_column: 1,
signature: Some(format!("fn {name}()")),
docstring: None,
visibility,
is_async: false,
branches: 0,
loops: 0,
returns: 0,
max_nesting: 0,
unsafe_blocks: 0,
unchecked_calls: 0,
assertions: 0,
updated_at: 1000,
}
}
async fn setup_call_chain() -> (Database, TempDir) {
let (db, dir) = setup_db().await;
let main_node = make_node("n-main", "main", "src/main.rs", Visibility::Pub);
let process_node = make_node("n-process", "process", "src/main.rs", Visibility::Pub);
let validate_node = make_node("n-validate", "validate", "src/lib.rs", Visibility::Pub);
let check_node = make_node("n-check", "check", "src/lib.rs", Visibility::Pub);
db.insert_nodes(&[main_node, process_node, validate_node, check_node])
.await
.expect("failed to insert nodes");
let edges = vec![
Edge {
source: "n-main".to_string(),
target: "n-process".to_string(),
kind: EdgeKind::Calls,
line: Some(5),
},
Edge {
source: "n-process".to_string(),
target: "n-validate".to_string(),
kind: EdgeKind::Calls,
line: Some(10),
},
Edge {
source: "n-validate".to_string(),
target: "n-check".to_string(),
kind: EdgeKind::Calls,
line: Some(15),
},
];
db.insert_edges(&edges)
.await
.expect("failed to insert edges");
(db, dir)
}
#[tokio::test]
async fn test_get_callers() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let callers = traverser
.get_callers("n-process", 5)
.await
.expect("get_callers failed");
assert!(
!callers.is_empty(),
"process should have at least one caller"
);
let caller_names: Vec<&str> = callers.iter().map(|(n, _)| n.name.as_str()).collect();
assert!(
caller_names.contains(&"main"),
"callers of process should include main, got: {caller_names:?}"
);
}
#[tokio::test]
async fn test_get_callees() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let callees = traverser
.get_callees("n-process", 5)
.await
.expect("get_callees failed");
let callee_names: Vec<&str> = callees.iter().map(|(n, _)| n.name.as_str()).collect();
assert!(
callee_names.contains(&"validate"),
"callees of process should include validate, got: {callee_names:?}"
);
}
#[tokio::test]
async fn test_get_callees_transitive() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let callees = traverser
.get_callees("n-process", 5)
.await
.expect("get_callees failed");
let callee_names: Vec<&str> = callees.iter().map(|(n, _)| n.name.as_str()).collect();
assert!(
callee_names.contains(&"validate"),
"callees should include validate"
);
assert!(
callee_names.contains(&"check"),
"callees should transitively include check"
);
}
#[tokio::test]
async fn test_impact_radius() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let subgraph = traverser
.get_impact_radius("n-check", 10)
.await
.expect("get_impact_radius failed");
let node_names: Vec<&str> = subgraph.nodes.iter().map(|n| n.name.as_str()).collect();
assert!(
node_names.contains(&"validate"),
"impact of check should include validate, got: {node_names:?}"
);
assert!(
node_names.contains(&"process"),
"impact of check should include process, got: {node_names:?}"
);
assert!(
node_names.contains(&"main"),
"impact of check should include main, got: {node_names:?}"
);
}
#[tokio::test]
async fn test_call_graph_bidirectional() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let subgraph = traverser
.get_call_graph("n-process", 5)
.await
.expect("get_call_graph failed");
let node_names: Vec<&str> = subgraph.nodes.iter().map(|n| n.name.as_str()).collect();
assert!(
node_names.contains(&"main"),
"call graph of process should include caller 'main', got: {node_names:?}"
);
assert!(
node_names.contains(&"validate"),
"call graph of process should include callee 'validate', got: {node_names:?}"
);
assert!(
node_names.contains(&"process"),
"call graph should include the center node 'process', got: {node_names:?}"
);
}
#[tokio::test]
async fn test_bfs_traversal_with_depth_limit() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let opts = TraversalOptions {
max_depth: 1,
edge_kinds: Some(vec![EdgeKind::Calls]),
node_kinds: None,
direction: TraversalDirection::Outgoing,
limit: 100,
include_start: true,
};
let subgraph = traverser
.traverse_bfs("n-main", &opts)
.await
.expect("traverse_bfs failed");
let node_names: Vec<&str> = subgraph.nodes.iter().map(|n| n.name.as_str()).collect();
assert!(
node_names.contains(&"main"),
"depth-1 from main should include main itself"
);
assert!(
node_names.contains(&"process"),
"depth-1 from main should include process"
);
assert!(
!node_names.contains(&"validate"),
"depth-1 from main should NOT include validate (that is depth 2)"
);
assert!(
!node_names.contains(&"check"),
"depth-1 from main should NOT include check (that is depth 3)"
);
}
#[tokio::test]
async fn test_bfs_traversal_full_depth() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let opts = TraversalOptions {
max_depth: 10,
edge_kinds: Some(vec![EdgeKind::Calls]),
node_kinds: None,
direction: TraversalDirection::Outgoing,
limit: 100,
include_start: true,
};
let subgraph = traverser
.traverse_bfs("n-main", &opts)
.await
.expect("traverse_bfs failed");
assert_eq!(
subgraph.nodes.len(),
4,
"full-depth BFS from main should include all 4 nodes"
);
}
#[tokio::test]
async fn test_dfs_traversal() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let opts = TraversalOptions {
max_depth: 10,
edge_kinds: Some(vec![EdgeKind::Calls]),
node_kinds: None,
direction: TraversalDirection::Outgoing,
limit: 100,
include_start: true,
};
let subgraph = traverser
.traverse_dfs("n-main", &opts)
.await
.expect("traverse_dfs failed");
assert_eq!(
subgraph.nodes.len(),
4,
"full-depth DFS from main should include all 4 nodes"
);
}
#[tokio::test]
async fn test_find_path() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let path = traverser
.find_path("n-main", "n-check", &[EdgeKind::Calls])
.await
.expect("find_path failed")
.expect("path should exist from main to check");
assert!(
path.len() >= 2,
"path from main to check should have at least 2 entries"
);
assert_eq!(path[0].0.name, "main", "path should start with main");
assert_eq!(
path.last().unwrap().0.name,
"check",
"path should end with check"
);
}
#[tokio::test]
async fn test_find_path_no_route() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let orphan = make_node("n-orphan", "orphan", "src/orphan.rs", Visibility::Private);
db.insert_node(&orphan).await.expect("insert orphan failed");
let path = traverser
.find_path("n-main", "n-orphan", &[EdgeKind::Calls])
.await
.expect("find_path failed");
assert!(
path.is_none(),
"there should be no path from main to an orphan node"
);
}
#[tokio::test]
async fn test_find_path_same_node() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let path = traverser
.find_path("n-main", "n-main", &[])
.await
.expect("find_path failed")
.expect("path from a node to itself should exist");
assert_eq!(path.len(), 1, "path from main to main should have 1 entry");
assert_eq!(path[0].0.name, "main");
}
#[tokio::test]
async fn test_find_dead_code() {
let (db, _dir) = setup_call_chain().await;
let orphan = make_node(
"n-orphan",
"unused_helper",
"src/util.rs",
Visibility::Private,
);
db.insert_node(&orphan).await.expect("insert orphan failed");
let qm = GraphQueryManager::new(&db);
let dead = qm.find_dead_code(&[]).await.expect("find_dead_code failed");
let dead_names: Vec<&str> = dead.iter().map(|n| n.name.as_str()).collect();
assert!(
dead_names.contains(&"unused_helper"),
"orphan private function should be detected as dead code, got: {dead_names:?}"
);
assert!(
!dead_names.contains(&"main"),
"main should not be reported as dead code"
);
}
#[tokio::test]
async fn test_find_dead_code_excludes_pub() {
let (db, _dir) = setup_db().await;
let pub_node = make_node("n-pub", "public_api", "src/api.rs", Visibility::Pub);
db.insert_node(&pub_node)
.await
.expect("insert pub_node failed");
let qm = GraphQueryManager::new(&db);
let dead = qm.find_dead_code(&[]).await.expect("find_dead_code failed");
let dead_names: Vec<&str> = dead.iter().map(|n| n.name.as_str()).collect();
assert!(
!dead_names.contains(&"public_api"),
"pub functions should not be reported as dead code"
);
}
#[tokio::test]
async fn test_find_dead_code_with_kind_filter() {
let (db, _dir) = setup_db().await;
let func_node = make_node("n-func", "private_func", "src/lib.rs", Visibility::Private);
let mut struct_node = make_node("n-struct", "MyStruct", "src/lib.rs", Visibility::Private);
struct_node.kind = NodeKind::Struct;
db.insert_nodes(&[func_node, struct_node])
.await
.expect("insert nodes failed");
let qm = GraphQueryManager::new(&db);
let dead = qm
.find_dead_code(&[NodeKind::Function])
.await
.expect("find_dead_code failed");
let dead_names: Vec<&str> = dead.iter().map(|n| n.name.as_str()).collect();
assert!(
dead_names.contains(&"private_func"),
"private_func should be dead code"
);
assert!(
!dead_names.contains(&"MyStruct"),
"MyStruct should not appear when filtering by Function kind"
);
}
#[tokio::test]
async fn test_get_node_metrics() {
let (db, _dir) = setup_call_chain().await;
let qm = GraphQueryManager::new(&db);
let metrics = qm
.get_node_metrics("n-process")
.await
.expect("get_node_metrics failed");
assert_eq!(metrics.caller_count, 1, "process should have 1 caller");
assert_eq!(metrics.call_count, 1, "process should have 1 callee");
assert_eq!(
metrics.incoming_edge_count, 1,
"process should have 1 incoming edge total"
);
assert_eq!(
metrics.outgoing_edge_count, 1,
"process should have 1 outgoing edge total"
);
}
#[tokio::test]
async fn test_get_file_dependencies() {
let (db, _dir) = setup_call_chain().await;
let qm = GraphQueryManager::new(&db);
let deps = qm
.get_file_dependencies("src/main.rs")
.await
.expect("get_file_dependencies failed");
assert!(
deps.contains(&"src/lib.rs".to_string()),
"src/main.rs should depend on src/lib.rs, got: {deps:?}"
);
}
#[tokio::test]
async fn test_get_file_dependents() {
let (db, _dir) = setup_call_chain().await;
let qm = GraphQueryManager::new(&db);
let dependents = qm
.get_file_dependents("src/lib.rs")
.await
.expect("get_file_dependents failed");
assert!(
dependents.contains(&"src/main.rs".to_string()),
"src/lib.rs should be depended on by src/main.rs, got: {dependents:?}"
);
}
#[tokio::test]
async fn test_find_circular_dependencies() {
let (db, _dir) = setup_db().await;
let node_a = make_node("n-a", "func_a", "src/a.rs", Visibility::Pub);
let node_b = make_node("n-b", "func_b", "src/b.rs", Visibility::Pub);
db.insert_nodes(&[node_a, node_b])
.await
.expect("insert nodes failed");
let edges = vec![
Edge {
source: "n-a".to_string(),
target: "n-b".to_string(),
kind: EdgeKind::Calls,
line: Some(1),
},
Edge {
source: "n-b".to_string(),
target: "n-a".to_string(),
kind: EdgeKind::Calls,
line: Some(1),
},
];
db.insert_edges(&edges).await.expect("insert edges failed");
let file_a = tokensave::types::FileRecord {
path: "src/a.rs".to_string(),
content_hash: "hash_a".to_string(),
size: 100,
modified_at: 1000,
indexed_at: 2000,
node_count: 1,
};
let file_b = tokensave::types::FileRecord {
path: "src/b.rs".to_string(),
content_hash: "hash_b".to_string(),
size: 100,
modified_at: 1000,
indexed_at: 2000,
node_count: 1,
};
db.upsert_file(&file_a).await.expect("upsert file_a failed");
db.upsert_file(&file_b).await.expect("upsert file_b failed");
let qm = GraphQueryManager::new(&db);
let cycles = qm
.find_circular_dependencies()
.await
.expect("find_circular_dependencies failed");
assert!(
!cycles.is_empty(),
"should detect at least one circular dependency"
);
let cycle_files: Vec<&str> = cycles[0].iter().map(|s| s.as_str()).collect();
assert!(
cycle_files.contains(&"src/a.rs") && cycle_files.contains(&"src/b.rs"),
"cycle should contain both src/a.rs and src/b.rs, got: {cycle_files:?}"
);
}
#[tokio::test]
async fn test_type_hierarchy() {
let (db, _dir) = setup_db().await;
let mut trait_node = make_node("n-trait", "MyTrait", "src/lib.rs", Visibility::Pub);
trait_node.kind = NodeKind::Trait;
let mut struct_node = make_node("n-struct", "MyStruct", "src/lib.rs", Visibility::Pub);
struct_node.kind = NodeKind::Struct;
let mut impl_node = make_node("n-impl", "impl_block", "src/lib.rs", Visibility::Private);
impl_node.kind = NodeKind::Impl;
db.insert_nodes(&[trait_node, struct_node, impl_node])
.await
.expect("insert nodes failed");
let edge = Edge {
source: "n-impl".to_string(),
target: "n-trait".to_string(),
kind: EdgeKind::Implements,
line: None,
};
db.insert_edge(&edge).await.expect("insert edge failed");
let traverser = GraphTraverser::new(&db);
let subgraph = traverser
.get_type_hierarchy("n-trait")
.await
.expect("get_type_hierarchy failed");
let node_names: Vec<&str> = subgraph.nodes.iter().map(|n| n.name.as_str()).collect();
assert!(
node_names.contains(&"MyTrait"),
"hierarchy should contain the trait"
);
assert!(
node_names.contains(&"impl_block"),
"hierarchy should contain the impl that implements the trait"
);
}
#[tokio::test]
async fn test_traversal_with_limit() {
let (db, _dir) = setup_call_chain().await;
let traverser = GraphTraverser::new(&db);
let opts = TraversalOptions {
max_depth: 10,
edge_kinds: Some(vec![EdgeKind::Calls]),
node_kinds: None,
direction: TraversalDirection::Outgoing,
limit: 2,
include_start: true,
};
let subgraph = traverser
.traverse_bfs("n-main", &opts)
.await
.expect("traverse_bfs with limit failed");
assert!(
subgraph.nodes.len() <= 2,
"limit=2 should cap the result to at most 2 nodes, got: {}",
subgraph.nodes.len()
);
}
#[tokio::test]
async fn test_traversal_nonexistent_start() {
let (db, _dir) = setup_db().await;
let traverser = GraphTraverser::new(&db);
let opts = TraversalOptions::default();
let subgraph = traverser
.traverse_bfs("nonexistent", &opts)
.await
.expect("traverse_bfs should not error on missing start");
assert!(
subgraph.nodes.is_empty(),
"traversal from nonexistent node should return empty subgraph"
);
}
#[tokio::test]
async fn test_node_metrics_depth() {
let (db, _dir) = setup_db().await;
let mut file_node = make_node("n-file", "main.rs", "src/main.rs", Visibility::Pub);
file_node.kind = NodeKind::File;
let mut module_node = make_node("n-module", "utils", "src/main.rs", Visibility::Pub);
module_node.kind = NodeKind::Module;
let func_node = make_node("n-func", "helper", "src/main.rs", Visibility::Private);
db.insert_nodes(&[file_node, module_node, func_node])
.await
.expect("insert nodes failed");
let edges = vec![
Edge {
source: "n-file".to_string(),
target: "n-module".to_string(),
kind: EdgeKind::Contains,
line: None,
},
Edge {
source: "n-module".to_string(),
target: "n-func".to_string(),
kind: EdgeKind::Contains,
line: None,
},
];
db.insert_edges(&edges).await.expect("insert edges failed");
let qm = GraphQueryManager::new(&db);
let file_metrics = qm.get_node_metrics("n-file").await.expect("metrics failed");
assert_eq!(file_metrics.depth, 0, "file should be at depth 0");
assert_eq!(
file_metrics.child_count, 1,
"file should have 1 child (module)"
);
let module_metrics = qm
.get_node_metrics("n-module")
.await
.expect("metrics failed");
assert_eq!(module_metrics.depth, 1, "module should be at depth 1");
let func_metrics = qm.get_node_metrics("n-func").await.expect("metrics failed");
assert_eq!(func_metrics.depth, 2, "function should be at depth 2");
}
async fn setup_project() -> (TokenSave, TempDir) {
let dir = TempDir::new().expect("failed to create temp dir");
let project = dir.path();
fs::create_dir_all(project.join("src")).expect("failed to create src dir");
fs::write(
project.join("src/main.rs"),
r#"
use crate::utils::helper;
mod utils;
fn main() {
let result = helper();
println!("{}", result);
}
"#,
)
.expect("failed to write main.rs");
fs::write(
project.join("src/utils.rs"),
r#"
/// Returns a greeting string.
pub fn helper() -> String {
format!("Hello, world!")
}
"#,
)
.expect("failed to write utils.rs");
let cg = TokenSave::init(project)
.await
.expect("failed to init TokenSave");
cg.index_all().await.expect("failed to index project");
(cg, dir)
}
#[tokio::test]
async fn test_build_file_adjacency() {
let (cg, _dir) = setup_project().await;
let qm = GraphQueryManager::new(cg.db());
let adj = qm.build_file_adjacency(None).await.unwrap();
assert!(
adj.get("src/main.rs")
.is_some_and(|deps| deps.contains("src/utils.rs")),
"main.rs should depend on utils.rs"
);
for (file, deps) in &adj {
assert!(
!deps.contains(file),
"file {file} should not have a self-edge"
);
}
}
use tokensave::graph::health::{
acyclicity_score, compute_composite_health, dependency_depth, gini_coefficient, gini_label,
modularity_score, HealthDimensions,
};
#[test]
fn test_gini_perfect_equality() {
let values = vec![5.0, 5.0, 5.0, 5.0];
let g = gini_coefficient(&values);
assert!(
g.abs() < 1e-9,
"all-equal values should give Gini ~0.0, got {g}"
);
}
#[test]
fn test_gini_perfect_inequality() {
let values = vec![0.0, 0.0, 0.0, 1000.0];
let g = gini_coefficient(&values);
assert!(
g > 0.7,
"extreme inequality should give Gini > 0.7, got {g}"
);
}
#[test]
fn test_gini_moderate() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let g = gini_coefficient(&values);
assert!(
(0.1..0.5).contains(&g),
"moderate distribution should give Gini between 0.1 and 0.5, got {g}"
);
}
#[test]
fn test_gini_empty() {
let g = gini_coefficient(&[]);
assert_eq!(g, 0.0, "empty slice should give Gini 0.0");
}
#[test]
fn test_gini_single() {
let g = gini_coefficient(&[42.0]);
assert_eq!(g, 0.0, "single-element slice should give Gini 0.0");
}
#[test]
fn test_gini_label_thresholds() {
assert_eq!(gini_label(0.10), "low inequality (healthy)");
assert_eq!(gini_label(0.30), "moderate inequality");
assert_eq!(gini_label(0.50), "high inequality");
assert_eq!(gini_label(0.70), "extreme inequality (god files likely)");
}
fn make_adj(edges: &[(&str, &str)]) -> HashMap<String, HashSet<String>> {
let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
for &(src, tgt) in edges {
adj.entry(src.to_string())
.or_default()
.insert(tgt.to_string());
adj.entry(tgt.to_string()).or_default();
}
adj
}
#[test]
fn test_acyclicity_no_cycles() {
let adj = make_adj(&[("a", "b"), ("b", "c")]);
let (score, cycles) = acyclicity_score(&adj);
assert_eq!(score, 1.0, "DAG should have acyclicity score 1.0");
assert_eq!(cycles, 0, "DAG should have 0 cycle edges");
}
#[test]
fn test_acyclicity_with_cycle() {
let adj = make_adj(&[("a", "b"), ("b", "a")]);
let (score, cycles) = acyclicity_score(&adj);
assert!(
score < 1.0,
"graph with cycle should have score < 1.0, got {score}"
);
assert!(
cycles > 0,
"graph with cycle should have > 0 cycle edges, got {cycles}"
);
}
#[test]
fn test_acyclicity_empty() {
let adj: HashMap<String, HashSet<String>> = HashMap::new();
let (score, cycles) = acyclicity_score(&adj);
assert_eq!(score, 1.0, "empty graph should have acyclicity score 1.0");
assert_eq!(cycles, 0);
}
#[test]
fn test_depth_linear_chain() {
let adj = make_adj(&[("a", "b"), ("b", "c"), ("c", "d")]);
let result = dependency_depth(&adj, 10);
assert_eq!(
result.max_depth, 3,
"linear chain a→b→c→d should have max_depth=3"
);
let deepest = result.chains.iter().find(|ch| ch.depth == 3);
assert!(deepest.is_some(), "should find a chain with depth 3");
assert_eq!(
deepest.unwrap().chain.len(),
4,
"chain for depth-3 path should have 4 nodes"
);
}
#[test]
fn test_depth_empty() {
let adj: HashMap<String, HashSet<String>> = HashMap::new();
let result = dependency_depth(&adj, 10);
assert_eq!(result.max_depth, 0, "empty graph should have max_depth=0");
}
#[test]
fn test_depth_with_cycle_breaks() {
let adj = make_adj(&[("a", "b"), ("b", "a"), ("b", "c")]);
let result = dependency_depth(&adj, 10);
assert!(
result.max_depth >= 1,
"should find depth >= 1 even when cycle is present, got {}",
result.max_depth
);
}
#[test]
fn test_modularity_independent_clusters() {
let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
adj.entry("a".to_string())
.or_default()
.insert("b".to_string());
adj.entry("b".to_string()).or_default();
adj.entry("c".to_string())
.or_default()
.insert("d".to_string());
adj.entry("d".to_string()).or_default();
let (score, components) = modularity_score(&adj);
assert!(
components >= 2,
"two disconnected clusters should give >= 2 components, got {components}"
);
assert!(
score > 0.0,
"two-cluster graph should have modularity > 0, got {score}"
);
}
#[test]
fn test_modularity_single_blob() {
let adj = make_adj(&[("a", "b"), ("b", "c"), ("c", "a")]);
let (score, components) = modularity_score(&adj);
assert_eq!(
components, 1,
"fully connected cycle should have 1 component"
);
assert!(
score < 0.5,
"single blob should have low modularity score, got {score}"
);
}
#[test]
fn test_modularity_empty() {
let adj: HashMap<String, HashSet<String>> = HashMap::new();
let (score, _components) = modularity_score(&adj);
assert_eq!(score, 1.0, "empty graph should have modularity score 1.0");
}
#[test]
fn test_composite_health_all_perfect() {
let dims = HealthDimensions {
acyclicity: 1.0,
depth: 1.0,
equality: 1.0,
redundancy: 1.0,
modularity: 1.0,
};
assert_eq!(compute_composite_health(&dims), 10000);
}
#[test]
fn test_composite_health_one_zero() {
let dims = HealthDimensions {
acyclicity: 0.0,
depth: 1.0,
equality: 1.0,
redundancy: 1.0,
modularity: 1.0,
};
assert_eq!(compute_composite_health(&dims), 0);
}
#[test]
fn test_composite_health_mixed() {
let dims = HealthDimensions {
acyclicity: 0.8,
depth: 0.7,
equality: 0.9,
redundancy: 0.6,
modularity: 0.5,
};
let score = compute_composite_health(&dims);
assert!(
score > 0 && score < 10000,
"mixed health should give score between 0 and 10000, got {score}"
);
}
#[tokio::test]
async fn test_file_churn() {
let dir = TempDir::new().expect("failed to create temp dir");
let project = dir.path();
std::process::Command::new("git")
.args(["init"])
.current_dir(project)
.output()
.expect("git init failed");
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(project)
.output()
.expect("git config email failed");
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(project)
.output()
.expect("git config name failed");
fs::write(project.join("file.rs"), "fn foo() {}").expect("write failed");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.expect("git add failed");
std::process::Command::new("git")
.args(["commit", "-m", "first"])
.current_dir(project)
.output()
.expect("git commit 1 failed");
fs::write(project.join("file.rs"), "fn foo() {} fn bar() {}").expect("write failed");
std::process::Command::new("git")
.args(["add", "."])
.current_dir(project)
.output()
.expect("git add 2 failed");
std::process::Command::new("git")
.args(["commit", "-m", "second"])
.current_dir(project)
.output()
.expect("git commit 2 failed");
let churn = file_churn(project, 90).await.expect("file_churn failed");
let count = churn.get("file.rs").copied().unwrap_or(0);
assert!(count >= 2, "file.rs should have churn >= 2, got {count}");
}
#[tokio::test]
async fn test_file_churn_nonexistent_dir() {
let churn = file_churn(
std::path::Path::new("/nonexistent/path/that/does/not/exist"),
90,
)
.await
.expect("file_churn should not error for nonexistent dir");
assert!(
churn.is_empty(),
"should return empty map for nonexistent dir"
);
}