magellan 3.3.5

Deterministic codebase mapping tool for local development
Documentation
#![cfg(feature = "geometric-backend")]
//! Integration tests for condense and paths commands on geometric backend
//!
//! These tests verify:
//! - condense works on .geo backend and returns meaningful supernodes
//! - paths works on .geo backend and finds real execution paths
//! - Both commands respect bounds and survive database reopen

use std::path::PathBuf;

/// Create a test fixture with call chains and branching
fn create_test_fixture(temp_dir: &std::path::Path) -> PathBuf {
    let src_dir = temp_dir.join("src");
    std::fs::create_dir(&src_dir).unwrap();

    // Create lib.rs with a chain: add -> internal_add
    std::fs::write(
        src_dir.join("lib.rs"),
        r#"
pub fn add(a: i32, b: i32) -> i32 {
    internal_add(a, b)
}

fn internal_add(x: i32, y: i32) -> i32 {
    x + y
}

pub struct Point {
    pub x: i32,
    pub y: i32,
}

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Point { x, y }
    }
    
    pub fn add(&self, other: &Point) -> Point {
        Point::new(add(self.x, other.x), add(self.y, other.y))
    }
}
"#,
    )
    .unwrap();

    // Create main.rs with branching and deeper chains
    std::fs::write(
        src_dir.join("main.rs"),
        r#"
use test_fixture::add;

fn main() {
    let result = compute(1, 2);
    println!("{}", result);
    helper_a();
    helper_b();
}

fn compute(a: i32, b: i32) -> i32 {
    let intermediate = step1(a);
    step2(intermediate, b)
}

fn step1(x: i32) -> i32 {
    x * 2
}

fn step2(x: i32, y: i32) -> i32 {
    add(x, y)
}

fn helper_a() {
    println!("A");
    nested();
}

fn helper_b() {
    println!("B");
}

fn nested() {
    println!("nested");
}

// Mutual recursion cycle
fn is_even(n: u32) -> bool {
    if n == 0 { true } else { is_odd(n - 1) }
}

fn is_odd(n: u32) -> bool {
    if n == 0 { false } else { is_even(n - 1) }
}
"#,
    )
    .unwrap();

    src_dir
}

/// Test condense command works on geometric backend
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_condense_command_works() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_condense.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    // Create backend and index
    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    let indexed =
        scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    assert!(indexed > 0, "Should have indexed at least one file");
    backend.save_to_disk().unwrap();

    // Run condense
    let result = backend.condense_call_graph();

    // Should have supernodes (one per SCC)
    assert!(!result.supernodes.is_empty(), "Should have supernodes");

    // Each symbol should be in exactly one supernode (SCC)
    let total_members: usize = result.supernodes.iter().map(|s| s.len()).sum();
    assert!(total_members > 0, "Should have symbols in supernodes");

    // Check node_to_supernode mapping is consistent
    assert_eq!(
        result.node_to_supernode.len(),
        total_members,
        "Each symbol should be mapped to exactly one supernode"
    );
}

/// Test condense returns meaningful supernodes with edges
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_condense_returns_meaningful_supernodes() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_condense.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    backend.save_to_disk().unwrap();

    let result = backend.condense_call_graph();

    // With the call graph having chains like main->compute->step1/step2,
    // we should have edges between supernodes
    // (unless everything is in cycles, which it shouldn't be)

    // Find the is_even/is_odd cycle - they should be in the same supernode
    let cycle_supernode = result.supernodes.iter().find(|s| s.len() > 1);

    // The mutual recursion between is_even and is_odd should create an SCC of size 2
    // But if the indexer doesn't extract those edges, we won't see it
    // So this is optional - we mainly care that the algorithm works
    if let Some(cycle) = cycle_supernode {
        assert!(
            cycle.len() >= 2,
            "Cycle supernode should have at least 2 members"
        );
    }

    // Verify edges reference valid supernodes
    for (from, to) in &result.edges {
        assert!(
            *from < result.supernodes.len(),
            "Edge from should reference valid supernode"
        );
        assert!(
            *to < result.supernodes.len(),
            "Edge to should reference valid supernode"
        );
    }
}

/// Test paths command works on geometric backend
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_paths_command_works() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_paths.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    backend.save_to_disk().unwrap();

    // Get symbol IDs
    let all_symbols = backend.get_all_symbols().unwrap();
    let main_sym = all_symbols
        .iter()
        .find(|s| s.name == "main" && s.file_path.ends_with("main.rs"))
        .expect("Should find main function");

    // Find paths from main with no specific end (explore all reachable)
    let result = backend.enumerate_paths(main_sym.id, None, 10, 100);

    // Should enumerate some paths (at least explored edges)
    assert!(
        result.total_enumerated > 0 || !result.paths.is_empty(),
        "Should enumerate at least some paths or edges from main"
    );
}

/// Test paths finds real paths between symbols
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_paths_finds_real_path() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_paths.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    backend.save_to_disk().unwrap();

    let all_symbols = backend.get_all_symbols().unwrap();

    // Find main -> compute -> step2 -> add chain
    let main_sym = all_symbols
        .iter()
        .find(|s| s.name == "main" && s.file_path.ends_with("main.rs"))
        .expect("Should find main");
    let compute_sym = all_symbols
        .iter()
        .find(|s| s.name == "compute")
        .expect("Should find compute");

    // Find path from main to compute (direct edge)
    let result = backend.enumerate_paths(main_sym.id, Some(compute_sym.id), 5, 10);

    // Should find at least one path
    assert!(
        !result.paths.is_empty(),
        "Should find path from main to compute. Symbols: {:?}",
        all_symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
    );

    // The path should start with main and end with compute
    let first_path = &result.paths[0];
    assert_eq!(first_path[0], main_sym.id, "Path should start with main");
    assert_eq!(
        first_path[first_path.len() - 1],
        compute_sym.id,
        "Path should end with compute"
    );
}

/// Test paths respects bounds (max_depth, max_paths)
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_paths_respects_bounds() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_paths.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    backend.save_to_disk().unwrap();

    let all_symbols = backend.get_all_symbols().unwrap();
    let main_sym = all_symbols
        .iter()
        .find(|s| s.name == "main" && s.file_path.ends_with("main.rs"))
        .expect("Should find main");
    let step2_sym = all_symbols
        .iter()
        .find(|s| s.name == "step2")
        .expect("Should find step2");

    // Path main -> compute -> step2 requires depth 2
    // With max_depth=1, should not find it
    let result_shallow = backend.enumerate_paths(main_sym.id, Some(step2_sym.id), 1, 10);
    assert!(
        result_shallow.paths.is_empty() || result_shallow.bounded_hit,
        "Should not find path with insufficient depth"
    );

    // With max_depth=5, should find it
    let result_deep = backend.enumerate_paths(main_sym.id, Some(step2_sym.id), 5, 10);
    assert!(
        !result_deep.paths.is_empty() || result_deep.total_enumerated > 0,
        "Should find or attempt path with sufficient depth"
    );

    // Test max_paths limit by requesting many paths but with low limit
    let result_limited = backend.enumerate_paths(main_sym.id, None, 10, 2);
    assert!(
        result_limited.paths.len() <= 2,
        "Should respect max_paths limit"
    );
}

/// Test condense and paths work after database reopen
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_condense_and_paths_survive_reopen() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_reopen.geo");
    let src_dir = create_test_fixture(temp_dir.path());

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    // Create and index
    {
        let mut backend = GeometricBackend::create(&db_path).unwrap();
        scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
        backend.save_to_disk().unwrap();
    }

    // Reopen and test condense
    {
        let backend = GeometricBackend::open(&db_path).unwrap();
        let result = backend.condense_call_graph();
        assert!(
            !result.supernodes.is_empty(),
            "Condense should work after reopen"
        );

        // Test paths after reopen
        let all_symbols = backend.get_all_symbols().unwrap();
        let main_sym = all_symbols
            .iter()
            .find(|s| s.name == "main" && s.file_path.ends_with("main.rs"))
            .expect("Should find main after reopen");

        let path_result = backend.enumerate_paths(main_sym.id, None, 5, 10);
        // Should be able to enumerate (even if no specific target found)
        // The key is that it doesn't panic or error
        let _ = path_result.total_enumerated; // usize is always >= 0
    }
}

/// Test paths handles cycles without infinite loops
#[test]
#[cfg(feature = "geometric-backend")]
fn geometric_paths_handles_cycles() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test_cycles.geo");
    let src_dir = temp_dir.path().join("src");
    std::fs::create_dir(&src_dir).unwrap();

    // Create mutual recursion
    std::fs::write(
        src_dir.join("main.rs"),
        r#"
fn main() {
    is_even(5);
}

fn is_even(n: u32) -> bool {
    if n == 0 { true } else { is_odd(n - 1) }
}

fn is_odd(n: u32) -> bool {
    if n == 0 { false } else { is_even(n - 1) }
}
"#,
    )
    .unwrap();

    use magellan::graph::geo_index::{scan_directory_with_progress, IndexingMode};
    use magellan::graph::geometric_backend::GeometricBackend;

    let mut backend = GeometricBackend::create(&db_path).unwrap();
    scan_directory_with_progress(&mut backend, &src_dir, None, IndexingMode::CfgFirst).unwrap();
    backend.save_to_disk().unwrap();

    let all_symbols = backend.get_all_symbols().unwrap();
    let is_even_sym = all_symbols
        .iter()
        .find(|s| s.name == "is_even")
        .expect("Should find is_even");

    // This should complete without hanging (test has timeout, but we verify it finishes)
    let result = backend.enumerate_paths(is_even_sym.id, None, 20, 50);

    // Should enumerate some edges but not infinitely many
    assert!(
        result.total_enumerated < 1000,
        "Should not enumerate excessively due to cycle detection. Got: {}",
        result.total_enumerated
    );
}