use debtmap::priority::call_graph::{CallGraph, CallType, FunctionId};
use debtmap::risk::context::{Context, ContextDetails};
use debtmap::risk::lcov::parse_lcov_file;
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;
#[test]
fn test_lcov_parsing_determinism() {
let lcov_content = r#"SF:src/lib.rs
FN:10,func1
FN:10,func2
FNDA:5,func1
FNDA:10,func2
DA:10,1
end_of_record
SF:src/lib.rs
FN:20,func3
FNDA:0,func3
DA:20,0
end_of_record
SF:src/other.rs
FN:5,other_func
FNDA:1,other_func
DA:5,1
end_of_record
"#;
let mut results = Vec::new();
for _ in 0..5 {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(lcov_content.as_bytes()).unwrap();
let data = parse_lcov_file(temp_file.path()).unwrap();
let mut file_data = Vec::new();
let mut sorted_files: Vec<_> = data.functions.keys().collect();
sorted_files.sort();
for file in sorted_files {
let funcs = &data.functions[file];
let func_names: Vec<_> = funcs
.iter()
.map(|f| (f.name.clone(), f.start_line, f.coverage_percentage))
.collect();
file_data.push((file.clone(), func_names));
}
results.push(file_data);
}
let first = &results[0];
for (i, other) in results.iter().enumerate().skip(1) {
assert_eq!(
first, other,
"LCOV parsing non-deterministic at iteration {}",
i
);
}
}
#[test]
fn test_call_graph_determinism() {
let mut results = Vec::new();
for _ in 0..5 {
let mut graph = CallGraph::new();
let f1 = FunctionId::new(PathBuf::from("a.rs"), "common".to_string(), 1);
let f2 = FunctionId::new(PathBuf::from("b.rs"), "common".to_string(), 1);
let f3 = FunctionId::new(PathBuf::from("c.rs"), "other".to_string(), 1);
graph.add_function(f1.clone(), true, false, 10, 100);
graph.add_function(f2.clone(), false, false, 10, 100);
graph.add_function(f3.clone(), false, false, 10, 100);
graph.add_call_parts(f1.clone(), f2.clone(), CallType::Direct);
graph.add_call_parts(f2.clone(), f3.clone(), CallType::Direct);
let topo = graph.topological_sort().unwrap();
let all: Vec<_> = graph.get_all_functions().cloned().collect();
let callers = graph.get_callers_by_name("common");
let callees = graph.get_callees_by_name("common");
results.push((topo, all, callers, callees));
}
let (first_topo, first_all, first_callers, first_callees) = &results[0];
for (i, (other_topo, other_all, other_callers, other_callees)) in
results.iter().enumerate().skip(1)
{
assert_eq!(
first_topo, other_topo,
"Topo sort non-deterministic at iteration {}",
i
);
assert_eq!(
first_all, other_all,
"get_all_functions non-deterministic at iteration {}",
i
);
assert_eq!(
first_callers, other_callers,
"get_callers_by_name non-deterministic at iteration {}",
i
);
assert_eq!(
first_callees, other_callees,
"get_callees_by_name non-deterministic at iteration {}",
i
);
}
}
#[test]
fn test_context_summation_determinism() {
let mut context_map = HashMap::new();
for i in 0..100 {
context_map.insert(
format!("provider_{}", i),
Context {
provider: format!("provider_{}", i),
weight: 0.123456789,
contribution: (i as f64) * 0.0000001,
details: ContextDetails::Historical {
change_frequency: 0.0,
bug_density: 0.0,
age_days: 0,
author_count: 0,
total_commits: 0,
bug_fix_count: 0,
},
},
);
}
let mut results = Vec::new();
for _ in 0..10 {
let mut values: Vec<_> = context_map.values().collect();
values.sort_by(|a, b| a.provider.cmp(&b.provider));
let sum: f64 = values.iter().map(|c| c.contribution * c.weight).sum();
results.push(sum);
}
let first = results[0];
for (i, &other) in results.iter().enumerate().skip(1) {
assert_eq!(
first.to_bits(),
other.to_bits(),
"Float summation non-deterministic at iteration {}",
i
);
}
}
#[test]
fn test_typescript_extraction_determinism() {
use debtmap::analyzers::typescript::call_graph::extract_call_graph;
use debtmap::analyzers::typescript::parser::parse_source;
use debtmap::analyzers::typescript::visitor::function_analysis::extract_functions;
use debtmap::core::ast::JsLanguageVariant;
use std::path::PathBuf;
let source = r#"
function common() { return 1; }
class Test {
common() { return 2; }
}
const arrow = () => common();
"#;
let path = PathBuf::from("test.ts");
let mut results = Vec::new();
for _ in 0..5 {
let ast = parse_source(source, &path, JsLanguageVariant::TypeScript).unwrap();
let funcs = extract_functions(&ast, false);
let graph = extract_call_graph(&ast);
let func_data: Vec<_> = funcs.iter().map(|f| (f.name.clone(), f.line)).collect();
let graph_nodes: Vec<_> = graph.get_all_functions().cloned().collect();
results.push((func_data, graph_nodes));
}
let (first_funcs, first_nodes) = &results[0];
for (i, (other_funcs, other_nodes)) in results.iter().enumerate().skip(1) {
assert_eq!(
first_funcs, other_funcs,
"TS function extraction non-deterministic at iteration {}",
i
);
assert_eq!(
first_nodes, other_nodes,
"TS call graph nodes non-deterministic at iteration {}",
i
);
}
}
#[test]
fn test_python_extraction_determinism() {
use debtmap::analyzers::python::parser::parse_source;
use debtmap::extraction::python::PythonExtractor;
use std::path::PathBuf;
let source = r#"
def common():
return 1
class Test:
def common(self):
return 2
def other():
common()
t = Test()
t.common()
"#;
let path = PathBuf::from("test.py");
let mut results = Vec::new();
for _ in 0..10 {
let ast = parse_source(source, &path).unwrap();
let data = PythonExtractor::extract(&ast).unwrap();
let func_data: Vec<_> = data
.functions
.iter()
.map(|f| (f.name.clone(), f.line, f.cyclomatic))
.collect();
let import_data: Vec<_> = data.imports.iter().map(|i| i.path.clone()).collect();
results.push((func_data, import_data));
}
let (first_funcs, first_imports) = &results[0];
for (i, (other_funcs, other_imports)) in results.iter().enumerate().skip(1) {
assert_eq!(
first_funcs, other_funcs,
"Python function extraction non-deterministic at iteration {}",
i
);
assert_eq!(
first_imports, other_imports,
"Python import extraction non-deterministic at iteration {}",
i
);
}
}