use debtmap::analyzers::batch::{analyze_files_effect, validate_and_analyze_files, validate_files};
use debtmap::config::{BatchAnalysisConfig, DebtmapConfig, ParallelConfig};
use debtmap::effects::{run_effect, run_validation};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_project(files: &[(&str, &str)]) -> (TempDir, Vec<PathBuf>) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let mut paths = Vec::with_capacity(files.len());
for (name, content) in files {
let file_path = temp_dir.path().join(name);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).expect("Failed to create parent directory");
}
fs::write(&file_path, content).expect("Failed to write test file");
paths.push(file_path);
}
(temp_dir, paths)
}
#[test]
fn test_parallel_analysis_multiple_files() {
let files: Vec<(&str, &str)> = vec![
("file_a.rs", "fn a() { let x = 1; }"),
("file_b.rs", "fn b() { let y = 2; }"),
("file_c.rs", "fn c() { let z = 3; }"),
("file_d.rs", "fn d() { let w = 4; }"),
("file_e.rs", "fn e() { let v = 5; }"),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig::default()),
..Default::default()
};
let effect = analyze_files_effect(paths.clone());
let results = run_effect(effect, config).expect("Analysis should succeed");
assert_eq!(results.len(), 5, "Should analyze all 5 files");
for (result, path) in results.iter().zip(paths.iter()) {
assert_eq!(result.path, *path);
}
}
#[test]
fn test_parallel_analysis_with_timing() {
let files: Vec<(&str, &str)> = vec![
("mod1.rs", "fn func1() { let a = 1 + 2; }"),
("mod2.rs", "fn func2() { let b = 3 + 4; }"),
("mod3.rs", "fn func3() { let c = 5 + 6; }"),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig::default().with_timing()),
..Default::default()
};
let effect = analyze_files_effect(paths);
let results = run_effect(effect, config).expect("Analysis should succeed");
for result in &results {
assert!(
result.analysis_time.is_some(),
"Each result should have timing info"
);
}
}
#[test]
fn test_parallel_vs_sequential_produces_same_results() {
let files: Vec<(&str, &str)> = vec![
("src/lib.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }"),
(
"src/util.rs",
"pub fn multiply(a: i32, b: i32) -> i32 { a * b }",
),
("src/helper.rs", "pub fn negate(x: i32) -> i32 { -x }"),
];
let (_temp_dir, paths) = create_test_project(&files);
let parallel_config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig {
parallelism: ParallelConfig::default(),
fail_fast: false,
collect_timing: false,
}),
..Default::default()
};
let parallel_results =
run_effect(analyze_files_effect(paths.clone()), parallel_config).expect("Parallel failed");
let sequential_config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig {
parallelism: ParallelConfig::sequential(),
fail_fast: false,
collect_timing: false,
}),
..Default::default()
};
let sequential_results =
run_effect(analyze_files_effect(paths), sequential_config).expect("Sequential failed");
assert_eq!(
parallel_results.len(),
sequential_results.len(),
"Same number of results"
);
let mut parallel_sorted: Vec<_> = parallel_results.iter().collect();
let mut sequential_sorted: Vec<_> = sequential_results.iter().collect();
parallel_sorted.sort_by_key(|r| &r.path);
sequential_sorted.sort_by_key(|r| &r.path);
for (p, s) in parallel_sorted.iter().zip(sequential_sorted.iter()) {
assert_eq!(p.path, s.path, "Paths should match");
assert_eq!(
p.metrics.complexity.functions.len(),
s.metrics.complexity.functions.len(),
"Function count should match for {}",
p.path.display()
);
}
}
#[test]
fn test_parallel_analysis_large_batch() {
let files: Vec<(String, String)> = (0..25)
.map(|i| {
let name = format!("file_{}.rs", i);
let content = format!("fn func_{}() {{ let x = {}; }}", i, i * 10);
(name, content)
})
.collect();
let files_refs: Vec<(&str, &str)> = files
.iter()
.map(|(n, c)| (n.as_str(), c.as_str()))
.collect();
let (_temp_dir, paths) = create_test_project(&files_refs);
let config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig {
parallelism: ParallelConfig {
enabled: true,
max_concurrency: None,
batch_size: Some(10), },
fail_fast: false,
collect_timing: false,
}),
..Default::default()
};
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
assert_eq!(results.len(), 25, "Should analyze all 25 files");
}
#[test]
fn test_validation_accumulates_all_errors() {
let files: Vec<(&str, &str)> = vec![("valid.rs", "fn valid() {}")];
let (temp_dir, _) = create_test_project(&files);
let paths = vec![
temp_dir.path().join("valid.rs"),
PathBuf::from("/nonexistent/file_1.rs"),
PathBuf::from("/nonexistent/file_2.rs"),
PathBuf::from("/nonexistent/file_3.rs"),
];
let result = validate_files(&paths);
match result {
stillwater::Validation::Failure(errors) => {
let errors_vec: Vec<_> = errors.into_iter().collect();
assert_eq!(
errors_vec.len(),
3,
"Should accumulate all 3 errors, not fail at first"
);
}
stillwater::Validation::Success(_) => {
panic!("Expected failure due to nonexistent files");
}
}
}
#[test]
fn test_validation_syntax_errors_accumulate() {
let files: Vec<(&str, &str)> = vec![
("valid.rs", "fn valid() {}"),
("invalid1.rs", "fn broken( { }"), ("invalid2.rs", "struct { }"), ];
let (_temp_dir, paths) = create_test_project(&files);
let result = validate_files(&paths);
match result {
stillwater::Validation::Failure(errors) => {
let errors_vec: Vec<_> = errors.into_iter().collect();
assert_eq!(errors_vec.len(), 2, "Should accumulate both syntax errors");
}
stillwater::Validation::Success(_) => {
panic!("Expected failure due to syntax errors");
}
}
}
#[test]
fn test_validate_and_analyze_success() {
let files: Vec<(&str, &str)> = vec![
("module_a.rs", "pub fn a() -> i32 { 1 }"),
("module_b.rs", "pub fn b() -> i32 { 2 }"),
];
let (_temp_dir, paths) = create_test_project(&files);
let result = run_validation(validate_and_analyze_files(&paths));
assert!(result.is_ok(), "Should succeed for valid files");
let results = result.unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_validate_and_analyze_mixed() {
let files: Vec<(&str, &str)> = vec![("valid.rs", "fn ok() {}")];
let (temp_dir, _) = create_test_project(&files);
let paths = vec![
temp_dir.path().join("valid.rs"),
PathBuf::from("/nonexistent/missing.rs"),
];
let result = run_validation(validate_and_analyze_files(&paths));
assert!(result.is_err(), "Should fail when any file is invalid");
}
#[test]
fn test_analysis_detects_complexity() {
let content = r#"
pub fn complex_function(data: &[i32], threshold: i32) -> Vec<i32> {
let mut results = Vec::new();
for &value in data {
if value > threshold {
if value % 2 == 0 {
results.push(value * 2);
} else {
results.push(value * 3);
}
} else if value == threshold {
results.push(value);
} else {
results.push(value / 2);
}
}
results
}
"#;
let files: Vec<(&str, &str)> = vec![("complex.rs", content)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
assert_eq!(results.len(), 1);
let result = &results[0];
assert!(
!result.metrics.complexity.functions.is_empty(),
"Should find functions"
);
let complex_fn = result
.metrics
.complexity
.functions
.iter()
.find(|f| f.name == "complex_function");
assert!(complex_fn.is_some(), "Should find complex_function");
let func = complex_fn.unwrap();
assert!(
func.cyclomatic > 1,
"Complex function should have cyclomatic > 1"
);
}
#[test]
fn test_analysis_multiple_languages() {
let files: Vec<(&str, &str)> = vec![
("lib.rs", "pub fn rust_fn() { let x = 1; }"),
("script.py", "def python_fn():\n x = 1"),
("app.js", "function jsFn() { let x = 1; }"),
(
"main.go",
"package main\n\nfunc goFn() { println(\"hello\") }",
),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
assert_eq!(results.len(), 4, "Should analyze all 4 files");
for result in &results {
assert!(
result.metrics.complexity.functions.is_empty()
|| !result.metrics.complexity.functions.is_empty(),
"Should complete analysis without error"
);
}
}
#[test]
fn test_analysis_go_file() {
let files = vec![(
"service.go",
r#"package service
import "context"
func Serve(ctx context.Context, ok bool) error {
if ok {
return nil
}
return nil
}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let metrics = &results[0].metrics;
assert_eq!(metrics.language, debtmap::core::Language::Go);
assert_eq!(metrics.dependencies[0].name, "context");
assert_eq!(metrics.complexity.functions[0].name, "Serve");
assert!(metrics.complexity.functions[0].cyclomatic > 1);
}
#[test]
fn test_go_same_package_cross_file_calls_resolve() {
let files = vec![
(
"service/handler.go",
r#"package service
func Serve() {
helper()
}
"#,
),
(
"service/helper.go",
r#"package service
func helper() {}
"#,
),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = results
.iter()
.flat_map(|result| result.metrics.complexity.functions.iter())
.find(|function| function.name == "Serve")
.unwrap();
let helper = results
.iter()
.flat_map(|result| result.metrics.complexity.functions.iter())
.find(|function| function.name == "helper")
.unwrap();
assert_eq!(serve.downstream_callees, Some(vec!["helper".to_string()]));
assert_eq!(helper.upstream_callers, Some(vec!["Serve".to_string()]));
}
#[test]
fn test_go_external_calls_do_not_resolve_as_internal_edges() {
let files = vec![(
"service/handler.go",
r#"package service
import "fmt"
func Serve() {
fmt.Println("hello")
}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = &results[0].metrics.complexity.functions[0];
assert_eq!(serve.call_dependencies, None);
assert_eq!(serve.downstream_callees, None);
}
#[test]
fn test_go_test_package_does_not_resolve_to_internal_package() {
let files = vec![
(
"service/handler.go",
r#"package service
func helper() {}
"#,
),
(
"service/handler_test.go",
r#"package service_test
func TestServe() {
helper()
}
"#,
),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let test_fn = results
.iter()
.flat_map(|result| result.metrics.complexity.functions.iter())
.find(|function| function.name == "TestServe")
.unwrap();
assert_eq!(test_fn.call_dependencies, None);
assert_eq!(test_fn.downstream_callees, None);
}
#[test]
fn test_go_selector_calls_resolve_to_inferred_receiver_type() {
let files = vec![(
"service/handler.go",
r#"package service
type Service struct{}
type Logger struct{}
func Serve() {
svc := &Service{}
svc.Handle()
}
func (s *Service) Handle() {}
func (l *Logger) Handle() {}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Serve")
.unwrap();
let service_handle = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Service.Handle")
.unwrap();
let logger_handle = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Logger.Handle")
.unwrap();
assert_eq!(
serve.downstream_callees,
Some(vec!["Service.Handle".to_string()])
);
assert_eq!(
service_handle.upstream_callers,
Some(vec!["Serve".to_string()])
);
assert_eq!(logger_handle.upstream_callers, None);
}
#[test]
fn test_go_direct_receiver_method_calls_resolve_to_same_type() {
let files = vec![(
"service/handler.go",
r#"package service
type Handler struct{}
type Other struct{}
func (h *Handler) Serve() {
h.validate()
}
func (h *Handler) validate() {}
func (o *Other) validate() {}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Handler.Serve")
.unwrap();
let other_validate = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Other.validate")
.unwrap();
assert_eq!(
serve.downstream_callees,
Some(vec!["Handler.validate".to_string()])
);
assert_eq!(other_validate.upstream_callers, None);
}
#[test]
fn test_go_constructor_assignment_resolves_receiver_method_call() {
let files = vec![(
"service/handler.go",
r#"package service
type Service struct{}
func Serve() {
svc := NewService()
svc.Handle()
}
func NewService() *Service {
return &Service{}
}
func (s *Service) Handle() {}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Serve")
.unwrap();
let service_handle = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Service.Handle")
.unwrap();
assert_eq!(
serve.downstream_callees,
Some(vec!["NewService".to_string(), "Service.Handle".to_string()])
);
assert_eq!(
service_handle.upstream_callers,
Some(vec!["Serve".to_string()])
);
}
#[test]
fn test_go_unknown_selector_calls_do_not_resolve_by_method_name() {
let files = vec![(
"service/handler.go",
r#"package service
type Service struct{}
func Serve(handler interface{ Handle() }) {
handler.Handle()
}
func (s *Service) Handle() {}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let serve = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Serve")
.unwrap();
let service_handle = results[0]
.metrics
.complexity
.functions
.iter()
.find(|function| function.name == "Service.Handle")
.unwrap();
assert_eq!(serve.downstream_callees, None);
assert_eq!(service_handle.upstream_callers, None);
}
#[test]
fn test_go_module_import_calls_resolve_to_local_package() {
let files = vec![
(
"cmd/app/main.go",
r#"package main
import "example.com/app/internal/mathx"
func main() {
mathx.Add()
}
"#,
),
(
"internal/mathx/math.go",
r#"package mathx
func Add() {}
"#,
),
];
let (temp_dir, paths) = create_test_project(&files);
fs::write(temp_dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let main_fn = find_go_function(&results, "main.go", "main");
let add_fn = find_go_function(&results, "math.go", "Add");
assert_eq!(main_fn.downstream_callees, Some(vec!["Add".to_string()]));
assert_eq!(add_fn.upstream_callers, Some(vec!["main".to_string()]));
}
#[test]
fn test_go_module_alias_import_calls_resolve() {
let files = vec![
(
"cmd/app/main.go",
r#"package main
import util "example.com/app/pkg/mathx"
func main() {
util.Add()
}
"#,
),
(
"pkg/mathx/math.go",
r#"package mathx
func Add() {}
"#,
),
];
let (temp_dir, paths) = create_test_project(&files);
fs::write(temp_dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let main_fn = find_go_function(&results, "main.go", "main");
let add_fn = find_go_function(&results, "math.go", "Add");
assert_eq!(main_fn.downstream_callees, Some(vec!["Add".to_string()]));
assert_eq!(add_fn.upstream_callers, Some(vec!["main".to_string()]));
}
#[test]
fn test_go_external_module_imports_stay_unresolved() {
let files = vec![(
"cmd/app/main.go",
r#"package main
import (
"fmt"
ext "github.com/acme/lib"
)
func main() {
fmt.Println("hello")
ext.Run()
}
"#,
)];
let (temp_dir, paths) = create_test_project(&files);
fs::write(temp_dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let main_fn = find_go_function(&results, "main.go", "main");
assert_eq!(main_fn.call_dependencies, None);
assert_eq!(main_fn.downstream_callees, None);
}
#[test]
fn test_go_duplicate_package_names_resolve_by_import_path() {
let files = vec![
(
"cmd/app/main.go",
r#"package main
import auth "example.com/app/internal/auth"
func main() {
auth.Run()
}
"#,
),
(
"internal/auth/auth.go",
r#"package auth
func Run() {}
"#,
),
(
"pkg/auth/auth.go",
r#"package auth
func Run() {}
"#,
),
];
let (temp_dir, paths) = create_test_project(&files);
fs::write(temp_dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let main_fn = find_go_function(&results, "main.go", "main");
let internal_run = find_go_function(&results, "internal/auth/auth.go", "Run");
let pkg_run = find_go_function(&results, "pkg/auth/auth.go", "Run");
assert_eq!(main_fn.downstream_callees, Some(vec!["Run".to_string()]));
assert_eq!(
internal_run.upstream_callers,
Some(vec!["main".to_string()])
);
assert_eq!(pkg_run.upstream_callers, None);
}
#[test]
fn test_go_dot_import_is_explicitly_ignored() {
let files = vec![
(
"cmd/app/main.go",
r#"package main
import . "example.com/app/internal/mathx"
func main() {
Add()
}
"#,
),
(
"internal/mathx/math.go",
r#"package mathx
func Add() {}
"#,
),
];
let (temp_dir, paths) = create_test_project(&files);
fs::write(temp_dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let main_fn = find_go_function(&results, "main.go", "main");
let add_fn = find_go_function(&results, "math.go", "Add");
assert_eq!(main_fn.downstream_callees, None);
assert_eq!(add_fn.upstream_callers, None);
}
#[test]
fn test_go_generic_instantiated_calls_resolve() {
let files = vec![(
"collections.go",
r#"package collections
func Run(xs []int) []string {
return Map[int, string](xs, format)
}
func Map[T any, U any](items []T, f func(T) U) []U {
return nil
}
func format(item int) string {
return ""
}
"#,
)];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results = run_effect(analyze_files_effect(paths), config).expect("Analysis should succeed");
let run = find_go_function(&results, "collections.go", "Run");
let map = find_go_function(&results, "collections.go", "Map");
assert_eq!(run.downstream_callees, Some(vec!["Map".to_string()]));
assert_eq!(map.upstream_callers, Some(vec!["Run".to_string()]));
}
fn find_go_function<'a>(
results: &'a [debtmap::analyzers::batch::FileAnalysisResult],
file_suffix: &str,
name: &str,
) -> &'a debtmap::core::FunctionMetrics {
results
.iter()
.flat_map(|result| result.metrics.complexity.functions.iter())
.find(|function| function.file.ends_with(file_suffix) && function.name == name)
.unwrap()
}
#[test]
fn test_empty_file_analysis() {
let files: Vec<(&str, &str)> = vec![("empty.rs", "")];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results =
run_effect(analyze_files_effect(paths), config).expect("Should handle empty file");
assert_eq!(results.len(), 1);
assert!(
results[0].metrics.complexity.functions.is_empty(),
"Empty file should have no functions"
);
}
#[test]
fn test_single_file_analysis() {
let files: Vec<(&str, &str)> = vec![("single.rs", "fn single() {}")];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig::default()),
..Default::default()
};
let results = run_effect(analyze_files_effect(paths), config).expect("Single file should work");
assert_eq!(results.len(), 1);
}
#[test]
fn test_deeply_nested_directory_structure() {
let files: Vec<(&str, &str)> = vec![
("src/mod1/submod/file.rs", "fn deep1() {}"),
("src/mod2/submod/deeper/file.rs", "fn deep2() {}"),
("lib/utils/helpers/file.rs", "fn deep3() {}"),
];
let (_temp_dir, paths) = create_test_project(&files);
let config = DebtmapConfig::default();
let results =
run_effect(analyze_files_effect(paths), config).expect("Should handle nested directories");
assert_eq!(results.len(), 3);
}
#[test]
fn test_parallel_analysis_maintains_all_results() {
let files: Vec<(String, String)> = (0..10)
.map(|i| {
let name = format!("module_{}.rs", i);
let content = format!(
r#"
pub fn function_{}(x: i32) -> i32 {{
if x > {} {{
x * 2
}} else {{
x + 1
}}
}}
"#,
i, i
);
(name, content)
})
.collect();
let files_refs: Vec<(&str, &str)> = files
.iter()
.map(|(n, c)| (n.as_str(), c.as_str()))
.collect();
let (_temp_dir, paths) = create_test_project(&files_refs);
let config = DebtmapConfig {
batch_analysis: Some(BatchAnalysisConfig::default()),
..Default::default()
};
let results = run_effect(analyze_files_effect(paths.clone()), config)
.expect("Parallel analysis should succeed");
assert_eq!(results.len(), 10);
let result_paths: std::collections::HashSet<_> =
results.iter().map(|r| r.path.clone()).collect();
assert_eq!(
result_paths.len(),
10,
"All results should have unique paths"
);
for path in &paths {
assert!(
result_paths.contains(path),
"Result should contain path: {}",
path.display()
);
}
}