use std::path::PathBuf;
use tldr_core::analysis::{
architecture_analysis, dead_code_analysis, find_importers, impact_analysis,
};
use tldr_core::callgraph::build_project_call_graph;
use tldr_core::{FunctionRef, Language, TldrError};
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
mod calls_tests {
use super::*;
#[test]
fn calls_builds_cross_file_call_graph() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true);
assert!(
graph.is_ok(),
"Failed to build call graph: {:?}",
graph.err()
);
let graph = graph.unwrap();
assert!(graph.edge_count() > 0, "Expected edges in call graph");
}
#[test]
fn calls_includes_self_edges_for_recursion() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true);
assert!(graph.is_ok());
let graph = graph.unwrap();
assert!(graph.edge_count() >= 2, "Expected at least 2 edges");
}
#[test]
fn calls_skips_unresolved_calls() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true);
assert!(graph.is_ok(), "Should handle unresolved calls gracefully");
}
#[test]
fn calls_respects_ignore_patterns() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true);
assert!(graph.is_ok());
}
#[test]
fn calls_works_with_typescript() {
let project = fixtures_dir().join("typescript-project");
let graph = build_project_call_graph(&project, Language::TypeScript, None, true);
assert!(
graph.is_ok(),
"TypeScript call graph should build: {:?}",
graph.err()
);
}
}
mod impact_tests {
use super::*;
#[test]
fn impact_finds_direct_callers() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = impact_analysis(&graph, "add_to_total", 2, None);
assert!(report.is_ok(), "Impact analysis failed: {:?}", report.err());
let report = report.unwrap();
assert!(report.total_targets > 0, "Expected to find the function");
}
#[test]
fn impact_respects_max_depth() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = impact_analysis(&graph, "add_to_total", 1, None);
assert!(report.is_ok());
}
#[test]
fn impact_handles_function_not_found() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = impact_analysis(&graph, "nonexistent_function_xyz", 3, None);
assert!(report.is_err());
if let Err(TldrError::FunctionNotFound { name, .. }) = report {
assert_eq!(name, "nonexistent_function_xyz");
} else {
panic!("Expected FunctionNotFound error, got {:?}", report);
}
}
#[test]
fn impact_handles_entry_points() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = impact_analysis(&graph, "main", 3, None);
assert!(
report.is_ok(),
"Should handle entry point: {:?}",
report.err()
);
}
}
mod dead_tests {
use super::*;
#[test]
fn dead_finds_uncalled_functions() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("main.py"), "main"),
FunctionRef::new(project.join("main.py"), "process_data"),
FunctionRef::new(project.join("main.py"), "add_to_total"),
FunctionRef::new(project.join("main.py"), "unused_function"),
];
let report = dead_code_analysis(&graph, &functions, None);
assert!(
report.is_ok(),
"Dead code analysis failed: {:?}",
report.err()
);
let report = report.unwrap();
assert!(
report
.dead_functions
.iter()
.any(|f| f.name == "unused_function"),
"Expected to find unused_function as dead"
);
}
#[test]
fn dead_excludes_entry_points() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("main.py"), "main"),
FunctionRef::new(project.join("test_main.py"), "test_something"),
];
let report = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
!report.dead_functions.iter().any(|f| f.name == "main"),
"main should not be marked as dead"
);
assert!(
!report
.dead_functions
.iter()
.any(|f| f.name == "test_something"),
"test functions should not be marked as dead"
);
}
#[test]
fn dead_excludes_dunder_methods() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("utils.py"), "__init__"),
FunctionRef::new(project.join("utils.py"), "__str__"),
];
let report = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
report.dead_functions.is_empty(),
"Dunder methods should be excluded"
);
}
#[test]
fn dead_respects_custom_entry_points() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("main.py"), "custom_entry"),
FunctionRef::new(project.join("main.py"), "helper_func"),
];
let custom = vec!["custom_entry".to_string()];
let report = dead_code_analysis(&graph, &functions, Some(&custom)).unwrap();
assert!(
!report
.dead_functions
.iter()
.any(|f| f.name == "custom_entry"),
"custom_entry should be excluded"
);
}
#[test]
fn dead_calculates_percentage() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("main.py"), "main"), FunctionRef::new(project.join("main.py"), "dead1"), FunctionRef::new(project.join("main.py"), "dead2"), FunctionRef::new(project.join("main.py"), "test_x"), ];
let report = dead_code_analysis(&graph, &functions, None).unwrap();
assert_eq!(report.total_dead, 2, "Expected 2 dead functions");
assert_eq!(report.total_functions, 4, "Expected 4 total functions");
assert!(
(report.dead_percentage - 50.0).abs() < 0.01,
"Expected ~50% dead"
);
}
#[test]
fn dead_groups_by_file() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let functions = vec![
FunctionRef::new(project.join("a.py"), "dead_a"),
FunctionRef::new(project.join("b.py"), "dead_b"),
];
let report = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(!report.by_file.is_empty() || report.dead_functions.len() == 2);
}
}
mod importers_tests {
use super::*;
#[test]
fn importers_finds_files_importing_module() {
let project = fixtures_dir().join("python-project");
let report = find_importers(&project, "typing", Language::Python);
assert!(report.is_ok(), "find_importers failed: {:?}", report.err());
let report = report.unwrap();
assert!(report.total > 0, "Expected to find files importing typing");
}
#[test]
fn importers_captures_line_numbers() {
let project = fixtures_dir().join("python-project");
let report = find_importers(&project, "typing", Language::Python).unwrap();
for importer in &report.importers {
assert!(importer.line > 0, "Line number should be positive");
}
}
#[test]
fn importers_captures_import_statement() {
let project = fixtures_dir().join("python-project");
let report = find_importers(&project, "typing", Language::Python).unwrap();
for importer in &report.importers {
assert!(
!importer.import_statement.is_empty(),
"Import statement should be captured"
);
}
}
#[test]
fn importers_handles_no_importers() {
let project = fixtures_dir().join("simple-project");
let report = find_importers(&project, "nonexistent_module_xyz", Language::Python);
assert!(report.is_ok());
let report = report.unwrap();
assert_eq!(report.total, 0, "Expected no importers");
}
#[test]
fn importers_works_with_typescript() {
let project = fixtures_dir().join("typescript-project");
let report = find_importers(&project, "./processor", Language::TypeScript);
assert!(report.is_ok(), "TypeScript importers should work");
}
}
mod arch_tests {
use super::*;
#[test]
fn arch_identifies_entry_layer() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph);
assert!(
report.is_ok(),
"Architecture analysis failed: {:?}",
report.err()
);
let report = report.unwrap();
assert!(
!report.entry_layer.is_empty() || !report.middle_layer.is_empty(),
"Should identify some layers"
);
}
#[test]
fn arch_identifies_middle_layer() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph).unwrap();
assert!(
report.middle_layer.iter().any(|f| f.name == "process_data")
|| report
.entry_layer
.iter()
.any(|f| f.name.contains("process"))
|| report.leaf_layer.iter().any(|f| f.name.contains("process")),
"Should classify process_data"
);
}
#[test]
fn arch_identifies_leaf_layer() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph).unwrap();
assert!(
report.leaf_layer.iter().any(|f| f.name == "add_to_total")
|| !report.leaf_layer.is_empty()
|| report.middle_layer.iter().any(|f| f.name.contains("add")),
"Should have leaf functions or classify add_to_total"
);
}
#[test]
fn arch_detects_circular_dependencies() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph);
assert!(
report.is_ok(),
"Should handle circular dependency detection"
);
}
#[test]
fn arch_infers_layer_types() {
let project = fixtures_dir().join("simple-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph).unwrap();
assert!(
report.inferred_layers.is_empty() || !report.directories.is_empty(),
"Should process directories"
);
}
#[test]
fn arch_calculates_directory_stats() {
let project = fixtures_dir().join("python-project");
let graph = build_project_call_graph(&project, Language::Python, None, true).unwrap();
let report = architecture_analysis(&graph).unwrap();
for stats in report.directories.values() {
assert!(
!stats.functions.is_empty() || stats.calls_in > 0 || stats.calls_out > 0,
"DirStats should have valid values"
);
}
}
}