use magellan::CodeGraph;
use tempfile::TempDir;
#[test]
fn test_cfg_extracted_from_rust_function() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source = r#"
fn simple_function() {
let x = 42;
if x > 0 {
println!("positive");
} else {
println!("non-positive");
}
}
fn loop_function() {
for i in 0..10 {
if i == 5 {
break;
}
}
}
fn match_function(x: i32) {
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"),
}
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source.as_bytes());
let _symbols = graph.symbols_in_file(path).unwrap();
let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();
assert!(
!all_cfg.is_empty(),
"CFG should be extracted for Rust files"
);
let total_blocks: usize = all_cfg
.iter()
.map(|(_, blocks): &(_, Vec<_>)| blocks.len())
.sum();
assert!(total_blocks > 0, "Should have at least one CFG block");
let all_blocks: Vec<_> = all_cfg
.iter()
.flat_map(|(_, blocks): &(_, Vec<_>)| blocks.iter())
.collect();
let block_kinds: Vec<_> = all_blocks.iter().map(|b| b.kind.as_str()).collect();
assert!(block_kinds.contains(&"entry"), "Should have entry block");
}
#[test]
fn test_cfg_deleted_on_file_reindex() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source1 = r#"
fn test_function() {
if true {
return;
}
}
"#;
let source2 = r#"
fn test_function() {
loop {
break;
}
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source1.as_bytes());
let cfg1 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
let initial_count: usize = cfg1
.iter()
.map(|(_, blocks): &(_, Vec<_>)| blocks.len())
.sum();
let _ = graph.index_file(path, source2.as_bytes());
let cfg2 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
let after_count: usize = cfg2
.iter()
.map(|(_, blocks): &(_, Vec<_>)| blocks.len())
.sum();
assert!(
initial_count > 0,
"Should have CFG blocks after first index"
);
assert!(after_count > 0, "Should have CFG blocks after re-index");
let blocks1: Vec<_> = cfg1
.iter()
.flat_map(|(_, b): &(_, Vec<_>)| b)
.map(|b| b.kind.as_str())
.collect();
let blocks2: Vec<_> = cfg2
.iter()
.flat_map(|(_, b): &(_, Vec<_>)| b)
.map(|b| b.kind.as_str())
.collect();
assert!(
blocks1.contains(&"if") || blocks1.contains(&"else"),
"First version should have if/else blocks"
);
assert!(
blocks2.contains(&"loop"),
"Second version should have loop block"
);
}
#[test]
fn test_cfg_deleted_on_file_delete() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source = r#"
fn to_be_deleted() {
return 42;
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source.as_bytes());
let cfg1 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
assert!(!cfg1.is_empty(), "CFG should exist after indexing");
let _ = graph.delete_file(path);
let cfg2 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
assert!(cfg2.is_empty(), "CFG should be deleted after file deletion");
}
#[test]
fn test_cfg_multiple_functions_same_file() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source = r#"
fn func_one() {
if true { return; }
}
fn func_two() {
loop { break; }
}
fn func_three() {
match 1 {
1 => {},
_ => {},
}
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source.as_bytes());
let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();
assert_eq!(all_cfg.len(), 3, "Should have CFG for 3 functions");
for (func_id, blocks) in &all_cfg {
assert!(
!blocks.is_empty(),
"Function {} should have CFG blocks",
func_id
);
assert!(
blocks.iter().any(|b| b.kind == "entry"),
"Function {} should have entry block",
func_id
);
}
}
#[test]
fn test_cfg_for_simple_function() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source = r#"
fn simple() {
let x = 1;
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source.as_bytes());
let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();
assert_eq!(all_cfg.len(), 1, "Should have CFG for 1 function");
let (_func_id, blocks) = &all_cfg[0];
assert!(!blocks.is_empty(), "Function should have CFG blocks");
assert!(
blocks.iter().any(|b| b.kind == "entry"),
"Should have entry block"
);
}
#[test]
fn test_direct_call_icfg_edges_stitch_call_block_to_callee_entry() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let callee_source = r#"
fn callee(x: i32) -> i32 {
x + 1
}
"#;
let caller_source = r#"
fn caller() -> i32 {
callee(41)
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
graph
.index_file("callee.rs", callee_source.as_bytes())
.unwrap();
graph
.index_file("caller.rs", caller_source.as_bytes())
.unwrap();
let stitched = graph.direct_call_icfg_edges("caller.rs", "caller").unwrap();
assert_eq!(
stitched.len(),
1,
"caller should stitch exactly one direct call"
);
let edge = &stitched[0];
assert_eq!(edge.call.caller, "caller");
assert_eq!(edge.call.callee, "callee");
let caller_id = graph
.symbol_id_by_name("caller.rs", "caller")
.unwrap()
.expect("caller symbol should exist");
let callee_id = graph
.symbol_id_by_name("callee.rs", "callee")
.unwrap()
.expect("callee symbol should exist");
assert_eq!(edge.caller_symbol_id, caller_id);
assert_eq!(edge.callee_symbol_id, callee_id);
let caller_blocks = graph.cfg_ops.get_cfg_for_function(caller_id).unwrap();
let callee_blocks = graph.cfg_ops.get_cfg_for_function(callee_id).unwrap();
assert_eq!(caller_blocks[edge.caller_block_idx].kind, "call");
assert_eq!(caller_blocks[edge.caller_block_idx].terminator, "call");
assert_eq!(callee_blocks[edge.callee_entry_block_idx].kind, "entry");
}
#[test]
fn test_direct_call_icfg_edges_include_callee_returns_and_caller_resume() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let callee_source = r#"
fn callee(x: i32) -> i32 {
return x + 1;
}
"#;
let caller_source = r#"
fn caller() -> i32 {
let y = callee(41);
return y + 1;
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
graph
.index_file("callee.rs", callee_source.as_bytes())
.unwrap();
graph
.index_file("caller.rs", caller_source.as_bytes())
.unwrap();
let stitched = graph.direct_call_icfg_edges("caller.rs", "caller").unwrap();
assert_eq!(
stitched.len(),
1,
"caller should have one stitched direct call"
);
let edge = &stitched[0];
assert_eq!(
edge.callee_return_block_indices.len(),
1,
"callee should expose its explicit return block"
);
let caller_blocks = graph
.cfg_ops
.get_cfg_for_function(edge.caller_symbol_id)
.unwrap();
let callee_blocks = graph
.cfg_ops
.get_cfg_for_function(edge.callee_symbol_id)
.unwrap();
let resume_idx = edge
.caller_resume_block_idx
.expect("call should resume into the caller continuation");
assert_eq!(caller_blocks[resume_idx].kind, "return");
let return_idx = edge.callee_return_block_indices[0];
assert_eq!(callee_blocks[return_idx].kind, "return");
}
#[test]
fn test_cfg_condition_extracted_from_cfg_attribute() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let source = r#"
#[cfg(feature = "tokio")]
fn tokio_only() {
let x = 1;
}
#[cfg(all(feature = "a", feature = "b"))]
fn complex_cfg() {
let y = 2;
}
fn always_available() {
let z = 3;
}
"#;
let mut graph = CodeGraph::open(&db_path).unwrap();
let path = "/test.rs";
let _ = graph.index_file(path, source.as_bytes());
let conn = rusqlite::Connection::open(&db_path).unwrap();
let mut stmt = conn
.prepare(
"SELECT c.kind, e.name, c.cfg_condition
FROM cfg_blocks c
JOIN graph_entities e ON c.function_id = e.id
ORDER BY e.name, c.byte_start",
)
.unwrap();
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<usize, String>(0)?,
row.get::<usize, String>(1)?,
row.get::<usize, Option<String>>(2)?,
))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut func_cfg: std::collections::HashMap<String, Vec<(String, Option<String>)>> =
std::collections::HashMap::new();
for (kind, name, cfg) in rows {
func_cfg.entry(name).or_default().push((kind, cfg));
}
let tokio_blocks = func_cfg
.get("tokio_only")
.expect("tokio_only should have CFG");
assert!(
!tokio_blocks.is_empty(),
"tokio_only should have at least one block"
);
for (_kind, cfg) in tokio_blocks {
assert_eq!(
cfg.as_deref(),
Some(r#"feature = "tokio""#),
"tokio_only blocks should have cfg condition"
);
}
let complex_blocks = func_cfg
.get("complex_cfg")
.expect("complex_cfg should have CFG");
assert!(
!complex_blocks.is_empty(),
"complex_cfg should have at least one block"
);
for (_kind, cfg) in complex_blocks {
assert_eq!(
cfg.as_deref(),
Some(r#"all(feature = "a", feature = "b")"#),
"complex_cfg blocks should have cfg condition"
);
}
let always_blocks = func_cfg
.get("always_available")
.expect("always_available should have CFG");
assert!(
!always_blocks.is_empty(),
"always_available should have at least one block"
);
for (_kind, cfg) in always_blocks {
assert_eq!(
cfg.as_deref(),
None,
"always_available blocks should have no cfg condition"
);
}
}