use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use super::builder_v2::{build_import_map, build_project_call_graph_v2, BuildConfig};
use super::cross_file_types::ImportDef;
use super::import_resolver::ImportResolver;
use super::module_index::ModuleIndex;
#[allow(dead_code)]
fn create_relative_import_project() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("pkg")).unwrap();
fs::write(dir.path().join("pkg/__init__.py"), "").unwrap();
let utils_py = r#"
def helper():
"""A helper function."""
return "helped"
"#;
fs::write(dir.path().join("pkg/utils.py"), utils_py).unwrap();
let main_py = r#"
from .utils import helper
def main():
result = helper()
return result
"#;
fs::write(dir.path().join("pkg/main.py"), main_py).unwrap();
dir
}
fn create_aliased_import_project() -> TempDir {
let dir = TempDir::new().unwrap();
let helper_py = r#"
def process():
"""Process something."""
pass
def validate():
"""Validate something."""
pass
"#;
fs::write(dir.path().join("helper.py"), helper_py).unwrap();
let main_py = r#"
from helper import process as proc
from helper import validate as check
def main():
proc() # Should resolve to helper.process
process() # Should ALSO resolve to helper.process (original name)
check() # Should resolve to helper.validate
validate() # Should ALSO resolve to helper.validate
"#;
fs::write(dir.path().join("main.py"), main_py).unwrap();
dir
}
fn create_nested_relative_import_project() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("pkg/sub")).unwrap();
fs::write(dir.path().join("pkg/__init__.py"), "").unwrap();
fs::write(dir.path().join("pkg/sub/__init__.py"), "").unwrap();
let sibling_py = r#"
def sibling_func():
"""Function in sibling module."""
return "sibling"
"#;
fs::write(dir.path().join("pkg/sibling.py"), sibling_py).unwrap();
let deep_py = r#"
from ..sibling import sibling_func
def deep_func():
return sibling_func()
"#;
fs::write(dir.path().join("pkg/sub/deep.py"), deep_py).unwrap();
dir
}
fn create_simple_cross_file_project() -> TempDir {
let dir = TempDir::new().unwrap();
let helper_py = r#"
def process():
"""Process data."""
return "processed"
"#;
fs::write(dir.path().join("helper.py"), helper_py).unwrap();
let main_py = r#"
from helper import process
def main():
result = process()
return result
"#;
fs::write(dir.path().join("main.py"), main_py).unwrap();
dir
}
#[test]
fn test_import_map_populated() {
let dir = create_simple_cross_file_project();
let index = ModuleIndex::build(dir.path(), "python").unwrap();
let import = ImportDef::from_import("helper", vec!["process".to_string()]);
let mut resolver = ImportResolver::new(&index, 100);
let resolved = resolver.resolve(&import, &dir.path().join("main.py"));
let (import_map, _module_imports) = build_import_map(&resolved);
assert!(
import_map.contains_key("process"),
"import_map should contain 'process' key after resolving `from helper import process`. \
Got keys: {:?}",
import_map.keys().collect::<Vec<_>>()
);
let (module, original_name) = import_map
.get("process")
.expect("import_map should contain 'process'");
assert_eq!(
original_name, "process",
"Original name should be 'process', got '{}'",
original_name
);
assert!(
module.contains("helper"),
"Module path should contain 'helper', got '{}'",
module
);
}
#[test]
fn test_aliased_import_both_names() {
let dir = create_aliased_import_project();
let index = ModuleIndex::build(dir.path(), "python").unwrap();
let mut import = ImportDef::from_import("helper", vec!["process".to_string()]);
import.aliases = Some({
let mut aliases = HashMap::new();
aliases.insert("proc".to_string(), "process".to_string());
aliases
});
let mut resolver = ImportResolver::new(&index, 100);
let resolved = resolver.resolve(&import, &dir.path().join("main.py"));
let (import_map, _) = build_import_map(&resolved);
assert!(
import_map.contains_key("proc"),
"import_map should contain alias 'proc'. Got keys: {:?}",
import_map.keys().collect::<Vec<_>>()
);
assert!(
import_map.contains_key("process"),
"import_map should ALSO contain original name 'process' (not just alias 'proc'). \
Per spec section 4.2: aliased imports must map BOTH names. \
Got keys: {:?}",
import_map.keys().collect::<Vec<_>>()
);
if let (Some(proc_mapping), Some(process_mapping)) =
(import_map.get("proc"), import_map.get("process"))
{
assert_eq!(
proc_mapping.1, process_mapping.1,
"Both 'proc' and 'process' should map to the same original name"
);
}
}
#[test]
fn test_relative_import_resolution() {
let dir = create_nested_relative_import_project();
let index = ModuleIndex::build(dir.path(), "python").unwrap();
let import = ImportDef::relative_import("sibling", vec!["sibling_func".to_string()], 2);
let resolver = ImportResolver::new(&index, 100);
let result = resolver.resolve_relative(&import, &dir.path().join("pkg/sub/deep.py"));
assert!(
result.is_some(),
"Relative import `from ..sibling import func` should resolve, not return None"
);
let resolved_module = result.unwrap();
assert_eq!(
resolved_module, "pkg.sibling",
"Relative import from pkg/sub/deep.py with level=2 should resolve to 'pkg.sibling', \
got '{}'. PEP 328: level-1 directories up from package.",
resolved_module
);
}
#[test]
fn test_cross_file_edge_created() {
let dir = create_simple_cross_file_project();
let config = BuildConfig {
language: "python".to_string(),
use_type_resolution: false,
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config).unwrap();
let cross_file_edges: Vec<_> = result
.edges
.iter()
.filter(|e| e.src_file != e.dst_file)
.collect();
assert!(
!cross_file_edges.is_empty(),
"Expected at least 1 cross-file edge (main.py:main -> helper.py:process). \
Found {} cross-file edges. \n\
All edges: {:?}",
cross_file_edges.len(),
result.edges
);
let has_expected_edge = cross_file_edges.iter().any(|e| {
e.src_file.to_string_lossy().contains("main.py")
&& e.dst_file.to_string_lossy().contains("helper.py")
&& e.src_func == "main"
&& e.dst_func == "process"
});
assert!(
has_expected_edge,
"Expected edge main.py:main -> helper.py:process not found. \
Cross-file edges found: {:?}",
cross_file_edges
);
}
#[test]
fn test_function_index_dual_key() {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("pkg")).unwrap();
fs::write(dir.path().join("pkg/__init__.py"), "").unwrap();
let core_py = r#"
def my_function():
pass
"#;
fs::write(dir.path().join("pkg/core.py"), core_py).unwrap();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config.clone()).unwrap();
let core_file = result
.files
.values()
.find(|f| f.path.to_string_lossy().contains("core.py"));
assert!(
core_file.is_some(),
"Should have parsed pkg/core.py. Files found: {:?}",
result.files.keys().collect::<Vec<_>>()
);
let core_file = core_file.unwrap();
assert!(
core_file.funcs.iter().any(|f| f.name == "my_function"),
"pkg/core.py should contain my_function"
);
let caller_py = r#"
from core import my_function
def caller():
my_function()
"#;
fs::write(dir.path().join("caller.py"), caller_py).unwrap();
let result2 = build_project_call_graph_v2(dir.path(), config).unwrap();
let has_edge = result2.edges.iter().any(|e| {
e.dst_func == "my_function"
&& e.src_func == "caller"
&& e.dst_file.to_string_lossy().contains("core.py")
});
assert!(
has_edge,
"Cross-file edge caller.py:caller -> pkg/core.py:my_function should exist. \
This requires func_index to have BOTH ('pkg.core', 'my_function') AND ('core', 'my_function'). \
Per spec section 2.2: Index BOTH forms for each function. \
Edges found: {:?}",
result2.edges
);
}
#[test]
fn test_parity_with_python() {
let test_path = Path::new("/tmp/llm-tldr-test/tldr");
if !test_path.exists() {
eprintln!(
"SKIPPING test_parity_with_python: test codebase not found at {}. \
To run this test, clone the tldr codebase to that location.",
test_path.display()
);
return;
}
let config = BuildConfig {
language: "python".to_string(),
use_type_resolution: false,
..Default::default()
};
let result = build_project_call_graph_v2(test_path, config).unwrap();
let cross_file_edge_count = result
.edges
.iter()
.filter(|e| e.src_file != e.dst_file)
.count();
assert!(
cross_file_edge_count >= 180,
"Expected >= 180 cross-file edges (baseline after bug fixes), found {}. \
This indicates a regression in cross-file resolution. \
See CROSSFILE_SPEC.md for debugging guide.",
cross_file_edge_count
);
let callers_of_build = result
.edges
.iter()
.filter(|e| e.dst_func == "build_project_call_graph")
.collect::<Vec<_>>();
assert!(
!callers_of_build.is_empty(),
"Expected >= 1 caller of build_project_call_graph (Python finds 3), found {}. \
Callers found: {:?}",
callers_of_build.len(),
callers_of_build
);
}
#[test]
fn test_intra_file_calls_work() {
let dir = TempDir::new().unwrap();
let code = r#"
def foo():
bar()
def bar():
baz()
def baz():
pass
"#;
fs::write(dir.path().join("module.py"), code).unwrap();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config).unwrap();
let module_ir = result
.files
.values()
.find(|f| f.path.to_string_lossy().contains("module.py"))
.expect("Should have parsed module.py");
let foo_calls = module_ir.calls.get("foo");
let foo_calls_bar = foo_calls
.map(|calls| calls.iter().any(|c| c.target == "bar"))
.unwrap_or(false);
let bar_calls = module_ir.calls.get("bar");
let bar_calls_baz = bar_calls
.map(|calls| calls.iter().any(|c| c.target == "baz"))
.unwrap_or(false);
assert!(
foo_calls_bar,
"Intra-file call foo->bar should be detected. Calls from foo: {:?}",
foo_calls
);
assert!(
bar_calls_baz,
"Intra-file call bar->baz should be detected. Calls from bar: {:?}",
bar_calls
);
}
#[test]
fn test_no_duplicate_edges() {
let dir = create_simple_cross_file_project();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config).unwrap();
let mut seen = std::collections::HashSet::new();
for edge in &result.edges {
let key = (
edge.src_file.clone(),
edge.src_func.clone(),
edge.dst_file.clone(),
edge.dst_func.clone(),
);
assert!(seen.insert(key.clone()), "Duplicate edge found: {:?}", edge);
}
}
#[test]
fn test_parse_errors_dont_crash() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("good.py"), "def foo(): pass").unwrap();
fs::write(dir.path().join("bad.py"), "def foo( :::").unwrap();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config);
assert!(
result.is_ok(),
"Build should succeed even with parse errors. Error: {:?}",
result.err()
);
let result = result.unwrap();
assert!(
result
.files
.values()
.any(|f| f.path.to_string_lossy().contains("good.py")),
"good.py should be parsed despite bad.py having errors"
);
}