use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use super::compat::{
callgraph_ir_to_old, compare_builders, format_edges_compatible, funcdef_to_functioninfo,
importdef_to_importinfo, project_graph_to_edges, ComparisonResult,
NormalizedEdge,
};
use super::builder_v2::{build_project_call_graph_v2, BuildConfig};
use super::cross_file_types::{
CallGraphIR, CallSite, CallType, CrossFileCallEdge, FileIRBuilder, FuncDef,
ImportDef, ProjectCallGraphV2,
};
#[allow(unused_imports)]
use crate::types::{FunctionInfo, ProjectCallGraph};
fn create_test_funcdef() -> FuncDef {
FuncDef {
name: "my_method".to_string(),
line: 10,
end_line: 25,
is_method: true,
class_name: Some("MyClass".to_string()),
return_type: Some("str".to_string()),
parent_function: None,
}
}
fn create_simple_funcdef() -> FuncDef {
FuncDef::function("simple_func", 1, 5)
}
fn create_test_importdef() -> ImportDef {
ImportDef::from_import(
"mymodule",
vec!["MyClass".to_string(), "helper".to_string()],
)
}
fn create_test_project_graph() -> ProjectCallGraphV2 {
let mut graph = ProjectCallGraphV2::new();
graph.add_edge(CrossFileCallEdge {
src_file: PathBuf::from("main.py"),
src_func: "main".to_string(),
dst_file: PathBuf::from("helper.py"),
dst_func: "process".to_string(),
call_type: CallType::Direct,
via_import: Some("helper".to_string()),
});
graph.add_edge(CrossFileCallEdge {
src_file: PathBuf::from("main.py"),
src_func: "main".to_string(),
dst_file: PathBuf::from("utils.py"),
dst_func: "validate".to_string(),
call_type: CallType::Direct,
via_import: Some("utils".to_string()),
});
graph
}
fn create_comparison_project() -> TempDir {
let dir = TempDir::new().unwrap();
let main_py = r#"
from helper import process
from utils import validate
def main():
process()
validate("test")
if __name__ == "__main__":
main()
"#;
fs::write(dir.path().join("main.py"), main_py).unwrap();
let helper_py = r#"
def process():
print("processing")
"#;
fs::write(dir.path().join("helper.py"), helper_py).unwrap();
let utils_py = r#"
def validate(data):
return len(data) > 0
"#;
fs::write(dir.path().join("utils.py"), utils_py).unwrap();
dir
}
mod type_conversion {
use super::*;
#[test]
fn test_funcdef_to_functioninfo() {
let func = create_test_funcdef();
let file = "module.py";
let info = funcdef_to_functioninfo(&func, file);
assert_eq!(info.name, "my_method");
assert_eq!(info.file, "module.py");
assert_eq!(info.start_line, 10);
assert_eq!(info.end_line, 25);
assert!(info.is_method);
assert_eq!(info.class_name, Some("MyClass".to_string()));
}
#[test]
fn test_funcdef_to_functioninfo_simple() {
let func = create_simple_funcdef();
let file = "simple.py";
let info = funcdef_to_functioninfo(&func, file);
assert_eq!(info.name, "simple_func");
assert_eq!(info.file, "simple.py");
assert!(!info.is_method);
assert_eq!(info.class_name, None);
}
#[test]
fn test_importdef_to_importinfo() {
let import = create_test_importdef();
let info = importdef_to_importinfo(&import);
assert_eq!(info.module, "mymodule");
assert!(info.is_from);
assert_eq!(info.names, vec!["MyClass", "helper"]);
}
#[test]
fn test_importdef_to_importinfo_simple() {
let import = ImportDef::simple_import("json");
let info = importdef_to_importinfo(&import);
assert_eq!(info.module, "json");
assert!(!info.is_from);
}
#[test]
fn test_importdef_to_importinfo_with_alias() {
let import = ImportDef::import_as("numpy", "np");
let info = importdef_to_importinfo(&import);
assert_eq!(info.module, "numpy");
assert_eq!(info.alias, Some("np".to_string()));
}
}
mod graph_conversion {
use super::*;
#[test]
fn test_project_graph_to_edges() {
let graph = create_test_project_graph();
let mut file_irs = std::collections::HashMap::new();
file_irs.insert(
"main.py".to_string(),
FileIRBuilder::new(PathBuf::from("main.py"))
.call(CallSite::direct("main", "process", Some(6)))
.call(CallSite::direct("main", "validate", Some(7)))
.build(),
);
let edges = project_graph_to_edges(&graph, &file_irs);
assert_eq!(edges.len(), 2, "Should have 2 edges");
let edge = &edges[0];
assert!(
edge.caller.contains("main.py"),
"Caller should include file"
);
assert!(
edge.caller.contains("main"),
"Caller should include function"
);
}
#[test]
fn test_edge_format() {
let mut graph = ProjectCallGraphV2::new();
graph.add_edge(CrossFileCallEdge {
src_file: PathBuf::from("src/main.py"),
src_func: "main".to_string(),
dst_file: PathBuf::from("src/helper.py"),
dst_func: "process".to_string(),
call_type: CallType::Direct,
via_import: None,
});
let file_irs = std::collections::HashMap::new();
let edges = project_graph_to_edges(&graph, &file_irs);
let edge = &edges[0];
assert!(
edge.caller.contains(":"),
"Caller should be file:func format"
);
assert!(
edge.callee.contains(":"),
"Callee should be file:func format"
);
}
#[test]
fn test_callgraph_ir_to_old() {
let mut ir = CallGraphIR::new(PathBuf::from("/project"), "python");
let file_ir = FileIRBuilder::new(PathBuf::from("main.py"))
.func(FuncDef::function("main", 1, 10))
.call(CallSite::direct("main", "helper", Some(5)))
.build();
ir.add_file(file_ir);
ir.build_indices();
let old_graph = callgraph_ir_to_old(&ir);
let _ = old_graph;
}
}
mod builder_comparison {
use super::*;
#[test]
fn test_compare_builders_result_structure() {
let dir = create_comparison_project();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = compare_builders(dir.path(), &config);
assert!(result.is_ok(), "Comparison should succeed");
let comparison = result.unwrap();
let _only_old: &HashSet<NormalizedEdge> = &comparison.only_in_old;
let _only_new: &HashSet<NormalizedEdge> = &comparison.only_in_new;
let _in_both: &HashSet<NormalizedEdge> = &comparison.in_both;
}
#[test]
fn test_compare_builders_identical() {
let dir = create_comparison_project();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let comparison = compare_builders(dir.path(), &config).unwrap();
assert!(
comparison.only_in_old.is_empty(),
"V2 should find all edges that V1 finds. Missing: {:?}",
comparison.only_in_old
);
}
#[test]
fn test_compare_builders_v2_may_find_more() {
let dir = create_comparison_project();
let config = BuildConfig {
language: "python".to_string(),
use_type_resolution: true, ..Default::default()
};
let comparison = compare_builders(dir.path(), &config).unwrap();
assert!(
comparison.only_in_old.is_empty(),
"V2 should not miss V1 edges"
);
}
}
mod cli_integration {
use super::*;
#[test]
fn test_experimental_flag_routing() {
let dir = create_comparison_project();
let _config_v1 = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let config_v2 = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let result = build_project_call_graph_v2(dir.path(), config_v2);
assert!(result.is_ok(), "V2 builder should work");
}
}
mod output_format {
use super::*;
#[test]
fn test_format_edges_compatible() {
let edges = vec![
(
"main.py".to_string(),
"main".to_string(),
"helper.py".to_string(),
"process".to_string(),
),
(
"main.py".to_string(),
"main".to_string(),
"utils.py".to_string(),
"validate".to_string(),
),
];
let output = format_edges_compatible(&edges);
assert!(output.contains("main.py:main -> helper.py:process"));
assert!(output.contains("main.py:main -> utils.py:validate"));
}
#[test]
fn test_format_edges_sorted() {
let edges = vec![
(
"z.py".to_string(),
"z".to_string(),
"a.py".to_string(),
"a".to_string(),
),
(
"a.py".to_string(),
"a".to_string(),
"b.py".to_string(),
"b".to_string(),
),
];
let output = format_edges_compatible(&edges);
let lines: Vec<&str> = output.lines().collect();
assert!(
lines[0].starts_with("a.py"),
"Output should be sorted alphabetically"
);
}
#[test]
fn test_format_edges_empty() {
let edges: Vec<(String, String, String, String)> = vec![];
let output = format_edges_compatible(&edges);
assert!(output.is_empty() || output.trim().is_empty());
}
}
mod ab_testing {
use super::*;
#[test]
fn test_comparison_result_structure() {
let result = ComparisonResult {
only_in_old: HashSet::new(),
only_in_new: HashSet::new(),
in_both: HashSet::new(),
};
assert!(result.only_in_old.is_empty());
assert!(result.only_in_new.is_empty());
assert!(result.in_both.is_empty());
}
#[test]
fn test_comparison_result_with_differences() {
let mut only_old = HashSet::new();
only_old.insert(NormalizedEdge::new(
"old.py".to_string(),
"old_func".to_string(),
"target.py".to_string(),
"target".to_string(),
));
let mut only_new = HashSet::new();
only_new.insert(NormalizedEdge::new(
"new.py".to_string(),
"new_func".to_string(),
"target.py".to_string(),
"target".to_string(),
));
let result = ComparisonResult {
only_in_old: only_old,
only_in_new: only_new,
in_both: HashSet::new(),
};
assert_eq!(result.only_in_old.len(), 1);
assert_eq!(result.only_in_new.len(), 1);
}
}
mod integration {
use super::*;
#[test]
#[ignore] fn test_ab_comparison_on_project() {
let dir = create_comparison_project();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let comparison = compare_builders(dir.path(), &config).unwrap();
if !comparison.only_in_old.is_empty() {
eprintln!("Edges only in V1:");
for edge in &comparison.only_in_old {
eprintln!(" {:?}", edge);
}
}
if !comparison.only_in_new.is_empty() {
eprintln!("Edges only in V2:");
for edge in &comparison.only_in_new {
eprintln!(" {:?}", edge);
}
}
assert!(
comparison.only_in_old.is_empty(),
"V2 missing {} edges from V1",
comparison.only_in_old.len()
);
}
#[test]
#[ignore] fn test_full_pipeline() {
let dir = create_comparison_project();
let config = BuildConfig {
language: "python".to_string(),
..Default::default()
};
let ir = build_project_call_graph_v2(dir.path(), config.clone()).unwrap();
let json = ir.to_json().unwrap();
let ir2 = CallGraphIR::from_json(&json).unwrap();
let _old_graph = callgraph_ir_to_old(&ir2);
let comparison = compare_builders(dir.path(), &config).unwrap();
assert!(
comparison.only_in_old.is_empty(),
"Full pipeline should preserve all edges"
);
}
}