magellan 3.3.8

Deterministic codebase mapping tool for local development
Documentation
//! CFG extraction integration tests
//!
//! Tests for AST-based CFG extraction and storage.

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());

    // Get symbols to find function IDs
    let _symbols = graph.symbols_in_file(path).unwrap();

    // Verify CFG was extracted - check via cfg_ops
    let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();

    // Should have CFG blocks for at least one function
    assert!(
        !all_cfg.is_empty(),
        "CFG should be extracted for Rust files"
    );

    // Count total CFG blocks across all functions
    let total_blocks: usize = all_cfg
        .iter()
        .map(|(_, blocks): &(_, Vec<_>)| blocks.len())
        .sum();
    assert!(total_blocks > 0, "Should have at least one CFG block");

    // Verify some expected block kinds exist
    let all_blocks: Vec<_> = all_cfg
        .iter()
        .flat_map(|(_, blocks): &(_, Vec<_>)| blocks.iter())
        .collect();

    // Check for expected block kinds
    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";

    // Index first version
    let _ = graph.index_file(path, source1.as_bytes());

    // Get CFG count after first index
    let cfg1 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
    let initial_count: usize = cfg1
        .iter()
        .map(|(_, blocks): &(_, Vec<_>)| blocks.len())
        .sum();

    // Re-index with different source
    let _ = graph.index_file(path, source2.as_bytes());

    // Get CFG count after re-index
    let cfg2 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
    let after_count: usize = cfg2
        .iter()
        .map(|(_, blocks): &(_, Vec<_>)| blocks.len())
        .sum();

    // CFG should be cleaned up and re-extracted
    assert!(
        initial_count > 0,
        "Should have CFG blocks after first index"
    );
    assert!(after_count > 0, "Should have CFG blocks after re-index");

    // The block kinds should differ between versions
    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();

    // source1 has "if", source2 has "loop"
    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());

    // Verify CFG exists
    let cfg1 = graph.cfg_ops.get_cfg_for_file(path).unwrap();
    assert!(!cfg1.is_empty(), "CFG should exist after indexing");

    // Delete file
    let _ = graph.delete_file(path);

    // Verify CFG is deleted
    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());

    // Get all CFG for the file
    let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();

    // Should have CFG for 3 functions
    assert_eq!(all_cfg.len(), 3, "Should have CFG for 3 functions");

    // Each function should have at least an entry block
    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());

    // Get all CFG for the file
    let all_cfg = graph.cfg_ops.get_cfg_for_file(path).unwrap();

    // Should have CFG for 1 function
    assert_eq!(all_cfg.len(), 1, "Should have CFG for 1 function");

    // Should have an entry block with fallthrough terminator
    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_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());

    // Query cfg_blocks directly to verify cfg_condition
    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();

    // Group by function
    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));
    }

    // tokio_only should have cfg_condition = feature = "tokio"
    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"
        );
    }

    // complex_cfg should have cfg_condition = all(feature = "a", feature = "b")
    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"
        );
    }

    // always_available should have no 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"
        );
    }
}