use tempfile::TempDir;
#[test]
fn test_reachable_symbols_finds_transitive_closure() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
helper_b();
}
fn helper_a() {
shared();
}
fn helper_b() {
shared();
}
fn shared() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let reachable = graph.reachable_symbols(main_fqn, None).unwrap();
assert!(
!reachable.is_empty(),
"Should find reachable symbols from main"
);
let fqn_names: Vec<_> = reachable.iter().filter_map(|s| s.fqn.as_deref()).collect();
assert!(
fqn_names.contains(&"helper_a") || fqn_names.contains(&"test.rs::helper_a"),
"Should find helper_a as reachable from main"
);
}
#[test]
fn test_dead_symbols_finds_unreachable_code() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
}
fn helper_a() {
shared();
}
fn shared() {}
fn unused_function() {
shared();
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let dead = graph.dead_symbols(main_fqn).unwrap();
let dead_fqns: Vec<_> = dead
.iter()
.filter_map(|s| s.symbol.fqn.as_deref())
.collect();
assert!(
dead_fqns.contains(&"unused_function")
|| dead_fqns.iter().any(|f| f.contains("unused_function")),
"unused_function should be detected as dead"
);
}
#[test]
fn test_reverse_reachable_finds_callers() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
helper_b();
}
fn helper_a() {
shared();
}
fn helper_b() {
shared();
}
fn shared() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let shared_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("shared"))
.expect("Should find shared symbol");
let shared_fqn = shared_symbol
.fqn
.as_ref()
.or(shared_symbol.canonical_fqn.as_ref())
.expect("shared should have FQN");
let callers = graph.reverse_reachable_symbols(shared_fqn, None).unwrap();
let caller_fqns: Vec<_> = callers.iter().filter_map(|s| s.fqn.as_deref()).collect();
assert!(
!callers.is_empty(),
"Should have callers for shared function"
);
assert!(
caller_fqns.contains(&"helper_a")
|| caller_fqns.contains(&"helper_b")
|| caller_fqns.iter().any(|f| f.contains("helper")),
"Should find at least one helper function as a caller of shared"
);
}
#[test]
fn test_algorithm_empty_database() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let graph = CodeGraph::open(&db_path).unwrap();
let result = graph.reachable_symbols("nonexistent", None);
assert!(
result.is_err(),
"Should error when symbol not found in database"
);
}
#[test]
fn test_algorithm_nonexistent_symbol() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = "fn main() {}";
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
let result = graph.reachable_symbols("this_symbol_does_not_exist", None);
assert!(result.is_err(), "Should error when symbol ID not found");
}
#[test]
fn test_backward_slice_finds_callers() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
intermediate();
}
fn intermediate() {
leaf();
}
fn leaf() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let leaf_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("leaf"))
.expect("Should find leaf symbol");
let leaf_fqn = leaf_symbol
.fqn
.as_ref()
.or(leaf_symbol.canonical_fqn.as_ref())
.expect("leaf should have FQN");
let slice_result = graph.backward_slice(leaf_fqn).unwrap();
let slice_fqns: Vec<_> = slice_result
.slice
.included_symbols
.iter()
.filter_map(|s| s.fqn.as_deref())
.collect();
assert!(
!slice_fqns.is_empty(),
"Backward slice from leaf should contain callers"
);
assert!(
slice_fqns.contains(&"intermediate")
|| slice_fqns.iter().any(|f| f.contains("intermediate")),
"intermediate should be in backward slice from leaf"
);
}
#[test]
fn test_forward_slice_finds_callees() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
intermediate();
}
fn intermediate() {
leaf();
}
fn leaf() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let slice_result = graph.forward_slice(main_fqn).unwrap();
let slice_fqns: Vec<_> = slice_result
.slice
.included_symbols
.iter()
.filter_map(|s| s.fqn.as_deref())
.collect();
assert!(
!slice_fqns.is_empty(),
"Forward slice from main should contain callees"
);
assert!(
slice_fqns.contains(&"intermediate")
|| slice_fqns.iter().any(|f| f.contains("intermediate")),
"intermediate should be in forward slice from main"
);
}
#[test]
fn test_slice_statistics() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
helper_b();
}
fn helper_a() {
shared();
}
fn helper_b() {
shared();
}
fn shared() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let shared_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("shared"))
.expect("Should find shared symbol");
let shared_fqn = shared_symbol
.fqn
.as_ref()
.or(shared_symbol.canonical_fqn.as_ref())
.expect("shared should have FQN");
let slice_result = graph.backward_slice(shared_fqn).unwrap();
assert_eq!(
slice_result.statistics.total_symbols,
slice_result.slice.included_symbols.len(),
"total_symbols should match included symbols count"
);
assert_eq!(
slice_result.statistics.data_dependencies, 0,
"data_dependencies should be 0 with call-graph fallback"
);
assert!(
slice_result.statistics.control_dependencies > 0,
"control_dependencies should be non-zero"
);
}
#[test]
fn test_slice_is_empty_for_isolated_function() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn isolated_function() {
let x = 42;
}
fn other_function() {
let y = 10;
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let isolated_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("isolated_function"))
.expect("Should find isolated_function symbol");
let isolated_fqn = isolated_symbol
.fqn
.as_ref()
.or(isolated_symbol.canonical_fqn.as_ref())
.expect("isolated_function should have FQN");
let backward_slice = graph.backward_slice(isolated_fqn).unwrap();
assert!(
backward_slice.slice.included_symbols.is_empty(),
"Backward slice of isolated function should be empty"
);
let forward_slice = graph.forward_slice(isolated_fqn).unwrap();
assert!(
forward_slice.slice.included_symbols.is_empty(),
"Forward slice of isolated function should be empty"
);
}
#[test]
fn test_slice_direction_consistency() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn a() {
b();
}
fn b() {
c();
}
fn c() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let b_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("b"))
.expect("Should find b symbol");
let b_fqn = b_symbol
.fqn
.as_ref()
.or(b_symbol.canonical_fqn.as_ref())
.expect("b should have FQN");
let backward = graph.backward_slice(b_fqn).unwrap();
let forward = graph.forward_slice(b_fqn).unwrap();
assert!(
matches!(backward.slice.direction, magellan::SliceDirection::Backward),
"Backward slice should have Backward direction"
);
assert!(
matches!(forward.slice.direction, magellan::SliceDirection::Forward),
"Forward slice should have Forward direction"
);
assert_eq!(
backward.slice.target.kind, forward.slice.target.kind,
"Target should be the same for both slices"
);
}
#[test]
fn test_enumerate_paths_finds_execution_paths() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
helper_b();
}
fn helper_a() {
leaf();
}
fn helper_b() {
leaf();
}
fn leaf() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let result = graph.enumerate_paths(main_fqn, None, 10, 100).unwrap();
if !result.paths.is_empty() {
assert!(
result.statistics.avg_length > 0.0,
"Average path length should be positive when paths exist"
);
assert!(
result.statistics.max_length >= result.statistics.min_length,
"Max length should be >= min length"
);
} else {
assert!(
result.statistics.avg_length >= 0.0,
"Average path length should be non-negative"
);
}
let leaf_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("leaf"))
.expect("Should find leaf symbol");
let leaf_fqn = leaf_symbol
.fqn
.as_ref()
.or(leaf_symbol.canonical_fqn.as_ref())
.expect("leaf should have FQN");
let result_to_leaf = graph
.enumerate_paths(main_fqn, Some(leaf_fqn), 10, 100)
.unwrap();
let _ = result_to_leaf.total_enumerated;
}
#[test]
fn test_enumerate_paths_with_end_symbol() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper();
}
fn helper() {
target();
}
fn target() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let target_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("target"))
.expect("Should find target symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let target_fqn = target_symbol
.fqn
.as_ref()
.or(target_symbol.canonical_fqn.as_ref())
.expect("target should have FQN");
let result = graph
.enumerate_paths(main_fqn, Some(target_fqn), 10, 100)
.unwrap();
assert!(
!result.paths.is_empty(),
"Should find at least one path from main to target"
);
for path in &result.paths {
let last_fqn = path
.symbols
.last()
.and_then(|s| s.fqn.as_deref())
.unwrap_or("");
assert!(
last_fqn.contains("target") || last_fqn == target_fqn,
"Path should end at target symbol"
);
}
}
#[test]
fn test_enumerate_paths_respects_bounds() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
b();
c();
}
fn a() { x(); }
fn b() { x(); }
fn c() { x(); }
fn x() {
y();
}
fn y() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let main_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.expect("Should find main symbol");
let main_fqn = main_symbol
.fqn
.as_ref()
.or(main_symbol.canonical_fqn.as_ref())
.expect("main should have FQN");
let result = graph.enumerate_paths(main_fqn, None, 10, 2).unwrap();
assert!(
result.paths.len() <= 2,
"Should respect max_paths bound (got {} paths)",
result.paths.len()
);
}
#[test]
fn test_detect_cycles_finds_mutual_recursion() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {
a();
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let report = graph.detect_cycles().unwrap();
assert!(
report.total_count > 0,
"Should detect cycles in mutually recursive code"
);
let mutual_cycle = report
.cycles
.iter()
.find(|c| matches!(c.kind, magellan::graph::CycleKind::MutualRecursion));
assert!(mutual_cycle.is_some(), "Should find mutual recursion cycle");
let cycle = mutual_cycle.unwrap();
let cycle_fqns: Vec<_> = cycle
.members
.iter()
.filter_map(|s| s.fqn.as_deref())
.collect();
assert!(
cycle_fqns.iter().any(|f| f.contains("a")),
"Cycle should contain function 'a'"
);
assert!(
cycle_fqns.iter().any(|f| f.contains("b")),
"Cycle should contain function 'b'"
);
}
#[test]
fn test_detect_cycles_no_cycles_in_dag() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {
c();
}
fn c() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let report = graph.detect_cycles().unwrap();
assert!(
report.total_count == 0,
"Should detect no cycles in DAG code (found {})",
report.total_count
);
assert!(report.cycles.is_empty(), "Cycle list should be empty");
}
#[test]
fn test_find_cycles_containing_specific_symbol() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
x();
}
fn a() {
b();
}
fn b() {
a();
}
fn x() {
y();
}
fn y() {
x();
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let a_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("a"))
.expect("Should find 'a' symbol");
let a_fqn = a_symbol
.fqn
.as_ref()
.or(a_symbol.canonical_fqn.as_ref())
.expect("Symbol should have FQN");
let cycles = graph.find_cycles_containing(a_fqn).unwrap();
assert!(!cycles.is_empty(), "Should find cycles containing 'a'");
let cycle_fqns: Vec<_> = cycles[0]
.members
.iter()
.filter_map(|s| s.fqn.as_deref())
.collect();
assert!(
cycle_fqns.iter().any(|f| f.contains("a")),
"Cycle should contain 'a'"
);
assert!(
cycle_fqns.iter().any(|f| f.contains("b")),
"Cycle should contain 'b' (a's cycle partner)"
);
}
#[test]
fn test_find_cycles_containing_non_cyclic_symbol() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {
c();
}
fn c() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let symbols = graph.symbols_in_file(&path_str).unwrap();
let a_symbol = symbols
.iter()
.find(|s| s.name.as_deref() == Some("a"))
.expect("Should find 'a' symbol");
let a_fqn = a_symbol
.fqn
.as_ref()
.or(a_symbol.canonical_fqn.as_ref())
.expect("Symbol should have FQN");
let cycles = graph.find_cycles_containing(a_fqn).unwrap();
assert!(
cycles.is_empty(),
"Should find no cycles for non-cyclic symbol 'a'"
);
}
#[test]
fn test_condense_call_graph_creates_dag() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {
a();
c();
}
fn c() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let result = graph.condense_call_graph().unwrap();
assert!(
!result.graph.supernodes.is_empty(),
"Should create supernodes"
);
let cycle_supernode = result.graph.supernodes.iter().find(|s| {
let fqns: Vec<_> = s.members.iter().filter_map(|m| m.fqn.as_deref()).collect();
fqns.iter().any(|f| f.contains("a")) && fqns.iter().any(|f| f.contains("b"))
});
assert!(
cycle_supernode.is_some(),
"Should have a supernode containing the cycle between a and b"
);
assert!(
result.graph.supernodes.len() <= 10, "Should have a reasonable number of supernodes (got {})",
result.graph.supernodes.len()
);
}
#[test]
fn test_condense_call_graph_single_symbol_supernodes() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {}
fn c() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let result = graph.condense_call_graph().unwrap();
let single_member_supernodes: Vec<_> = result
.graph
.supernodes
.iter()
.filter(|s| s.members.len() == 1)
.collect();
assert!(
single_member_supernodes.len() >= 3,
"Should have at least 3 single-member supernodes (main, b, c)"
);
}
#[test]
fn test_condense_call_graph_symbol_to_supernode_mapping() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
a();
}
fn a() {
b();
}
fn b() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let result = graph.condense_call_graph().unwrap();
let main_supernode = result.graph.supernodes.iter().find(|s| {
s.members.iter().any(|m| {
m.fqn
.as_deref()
.map(|f| f.contains("main"))
.unwrap_or(false)
})
});
assert!(
main_supernode.is_some(),
"Should find a supernode containing 'main'"
);
let mut mapped_count = 0;
for supernode in &result.graph.supernodes {
for member in &supernode.members {
if let Some(ref sym_id) = member.symbol_id {
if let Some(&mapped_id) = result.original_to_supernode.get(sym_id) {
assert_eq!(
mapped_id, supernode.id,
"Symbol {} should map to the correct supernode",
sym_id
);
mapped_count += 1;
}
}
}
}
assert!(
mapped_count > 0,
"At least one symbol should be in the mapping"
);
}
#[test]
fn test_fqn_fallback_lookup() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper();
}
fn helper() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let reachable = graph.reachable_symbols("main", None).unwrap();
assert!(
!reachable.is_empty(),
"FQN fallback should resolve 'main' to the correct symbol"
);
}
#[test]
fn test_single_symbol_graph() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = "fn main() {}\n";
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let reachable = graph.reachable_symbols("main", None).unwrap();
assert!(
reachable.is_empty(),
"Single symbol should have no reachable callees"
);
let cycles = graph.detect_cycles().unwrap();
assert!(
cycles.cycles.is_empty(),
"Single symbol with no self-loop should have no cycles"
);
let condensed = graph.condense_call_graph().unwrap();
assert!(
!condensed.graph.supernodes.is_empty(),
"Even single symbol should create a supernode"
);
}
#[test]
fn test_disconnected_components() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
}
fn helper_a() {}
fn standalone_function() {
helper_b();
}
fn helper_b() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let reachable_from_main = graph.reachable_symbols("main", None).unwrap();
let standalone_found = reachable_from_main.iter().any(|s| {
s.fqn
.as_deref()
.map(|f| f.contains("standalone_function"))
.unwrap_or(false)
});
assert!(
!standalone_found,
"Reachability should not cross disconnected components"
);
let reachable_from_standalone = graph
.reachable_symbols("standalone_function", None)
.unwrap();
let helper_b_found = reachable_from_standalone.iter().any(|s| {
s.fqn
.as_deref()
.map(|f| f.contains("helper_b"))
.unwrap_or(false)
});
assert!(
helper_b_found,
"Standalone component should have its own reachable callees"
);
}
#[test]
fn test_slice_after_condense() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
}
fn helper_a() {
shared();
}
fn shared() {}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let _condensed = graph.condense_call_graph().unwrap();
let slice_result = graph.backward_slice("helper_a").unwrap();
assert!(
!slice_result.slice.included_symbols.is_empty(),
"Slicing should work after condensation"
);
let main_found = slice_result.slice.included_symbols.iter().any(
|s: &magellan::graph::algorithms::SymbolInfo| {
s.fqn
.as_deref()
.map(|f: &str| f.contains("main"))
.unwrap_or(false)
},
);
assert!(
main_found,
"Backward slice from helper_a should include main"
);
}
#[test]
fn test_dead_code_with_cycles() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
helper_a();
}
fn helper_a() {
helper_b();
}
fn helper_b() {
helper_a(); // Cycle with helper_a
}
fn unused_cycle_start() {
unused_cycle_end();
}
fn unused_cycle_end() {
unused_cycle_start(); // Disconnected cycle
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let dead = graph.dead_symbols("main").unwrap();
let unused_found = dead.iter().any(|s| {
s.symbol
.fqn
.as_deref()
.map(|f| f.contains("unused_cycle"))
.unwrap_or(false)
});
assert!(
unused_found,
"Dead code detection should find disconnected cycles"
);
}
#[test]
fn test_cycle_then_slice() {
use magellan::CodeGraph;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.rs");
let file_path = temp_dir.path().join("test.rs");
let source = r#"
fn main() {
cycle_start();
}
fn cycle_start() {
cycle_middle();
}
fn cycle_middle() {
cycle_end();
}
fn cycle_end() {
cycle_start(); // Back edge creates cycle
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path_str = file_path.to_string_lossy().to_string();
graph.index_file(&path_str, source.as_bytes()).unwrap();
graph.index_calls(&path_str, source.as_bytes()).unwrap();
let cycles = graph.detect_cycles().unwrap();
assert!(!cycles.cycles.is_empty(), "Should detect the cycle");
let slice_result = graph.backward_slice("cycle_middle").unwrap();
assert!(
!slice_result.slice.included_symbols.is_empty(),
"Slice within cycle should return results"
);
}