use debtmap::analyzers::rust_call_graph::extract_call_graph;
use std::path::PathBuf;
#[test]
fn test_same_file_orchestration_calls() {
let code = r#"
fn take_child_stdio(child: &mut Child) -> Result<(Stdout, Stderr), Error> {
Ok((child.stdout.take().unwrap(), child.stderr.take().unwrap()))
}
fn parse_stream_json(line: &str) -> Option<String> {
serde_json::from_str(line).ok()
}
fn interpret_process_result(result: Result<ExitStatus, Error>, name: &str) -> Result<(), Error> {
match result {
Ok(status) if status.success() => Ok(()),
Ok(status) => Err(Error::new(format!("{} exited with {}", name, status))),
Err(e) => Err(e),
}
}
async fn invoke(child: &mut Child) -> Result<(), Error> {
let (stdout, stderr) = take_child_stdio(child)?;
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Some(content) = parse_stream_json(&line?) {
println!("{}", content);
}
}
let result = child.wait();
interpret_process_result(result, "Claude")
}
"#;
let parsed = syn::parse_file(code).unwrap();
let path = PathBuf::from("claude_runner.rs");
let call_graph = extract_call_graph(&parsed, &path);
let functions = call_graph.find_all_functions();
assert!(
functions.len() >= 4,
"Should find at least 4 functions (invoke, take_child_stdio, parse_stream_json, interpret_process_result)"
);
let invoke_func = functions
.iter()
.find(|f| f.name == "invoke")
.expect("Should find invoke function");
let invoke_callees = call_graph.get_callees(invoke_func);
assert!(
invoke_callees.iter().any(|f| f.name == "take_child_stdio"),
"invoke should call take_child_stdio. Found callees: {:?}",
invoke_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
assert!(
invoke_callees.iter().any(|f| f.name == "parse_stream_json"),
"invoke should call parse_stream_json. Found callees: {:?}",
invoke_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
assert!(
invoke_callees
.iter()
.any(|f| f.name == "interpret_process_result"),
"invoke should call interpret_process_result. Found callees: {:?}",
invoke_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
assert!(
invoke_callees.len() >= 3,
"invoke should have at least 3 callees, found {}",
invoke_callees.len()
);
}
#[test]
fn test_calls_inside_tokio_spawn() {
let code = r#"
fn parse_stream_json(line: &str) -> Option<String> {
serde_json::from_str(line).ok()
}
fn extract_text_block(content: &str) -> String {
content.to_string()
}
async fn invoke() {
let stdout_handle = tokio::spawn(async move {
let line = "test";
if let Some(content) = parse_stream_json(&line) {
let text = extract_text_block(&content);
println!("{}", text);
}
});
stdout_handle.await.unwrap();
}
"#;
let parsed = syn::parse_file(code).unwrap();
let path = PathBuf::from("test.rs");
let call_graph = extract_call_graph(&parsed, &path);
let functions = call_graph.find_all_functions();
let invoke_func = functions
.iter()
.find(|f| f.name == "invoke")
.expect("Should find invoke function");
let invoke_callees = call_graph.get_callees(invoke_func);
assert!(
invoke_callees.iter().any(|f| f.name == "parse_stream_json"),
"invoke should detect parse_stream_json inside tokio::spawn. Found: {:?}",
invoke_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
assert!(
invoke_callees
.iter()
.any(|f| f.name == "extract_text_block"),
"invoke should detect extract_text_block inside tokio::spawn. Found: {:?}",
invoke_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
}
#[test]
fn test_bidirectional_caller_callee_tracking() {
let code = r#"
fn helper() -> i32 {
42
}
fn caller() -> i32 {
helper()
}
"#;
let parsed = syn::parse_file(code).unwrap();
let path = PathBuf::from("test.rs");
let call_graph = extract_call_graph(&parsed, &path);
let functions = call_graph.find_all_functions();
let caller_func = functions
.iter()
.find(|f| f.name == "caller")
.expect("Should find caller function");
let caller_callees = call_graph.get_callees(caller_func);
assert!(
caller_callees.iter().any(|f| f.name == "helper"),
"caller should have helper as callee"
);
let helper_func = functions
.iter()
.find(|f| f.name == "helper")
.expect("Should find helper function");
let helper_callers = call_graph.get_callers(helper_func);
assert!(
helper_callers.iter().any(|f| f.name == "caller"),
"helper should have caller as caller"
);
}
#[test]
fn test_multiple_spawn_with_nested_calls() {
let code = r#"
fn process_stdout(line: &str) -> Option<String> {
Some(line.to_string())
}
fn process_stderr(line: &str) -> Option<String> {
Some(line.to_string())
}
fn finalize() {
// cleanup
}
async fn invoke() {
let stdout_handle = tokio::spawn(async move {
process_stdout("test");
});
let stderr_handle = tokio::spawn(async move {
process_stderr("error");
});
stdout_handle.await.unwrap();
stderr_handle.await.unwrap();
finalize();
}
"#;
let parsed = syn::parse_file(code).unwrap();
let path = PathBuf::from("test.rs");
let call_graph = extract_call_graph(&parsed, &path);
let functions = call_graph.find_all_functions();
let invoke_func = functions
.iter()
.find(|f| f.name == "invoke")
.expect("Should find invoke function");
let invoke_callees = call_graph.get_callees(invoke_func);
assert!(
invoke_callees.iter().any(|f| f.name == "process_stdout"),
"Should detect process_stdout in first spawn"
);
assert!(
invoke_callees.iter().any(|f| f.name == "process_stderr"),
"Should detect process_stderr in second spawn"
);
assert!(
invoke_callees.iter().any(|f| f.name == "finalize"),
"Should detect finalize direct call"
);
}
#[test]
fn test_method_calls_in_async_blocks() {
let code = r#"
struct Parser;
impl Parser {
fn parse_json(&self, line: &str) -> Option<String> {
Some(line.to_string())
}
fn validate(&self, content: &str) -> bool {
!content.is_empty()
}
async fn process(&self) {
tokio::spawn(async move {
let parser = Parser;
if let Some(content) = parser.parse_json("test") {
parser.validate(&content);
}
});
}
}
"#;
let parsed = syn::parse_file(code).unwrap();
let path = PathBuf::from("test.rs");
let call_graph = extract_call_graph(&parsed, &path);
let functions = call_graph.find_all_functions();
let process_func = functions
.iter()
.find(|f| f.name.contains("process"))
.expect("Should find process method");
let process_callees = call_graph.get_callees(process_func);
assert!(
process_callees
.iter()
.any(|f| f.name.contains("parse_json")),
"Should detect parse_json method call. Found: {:?}",
process_callees.iter().map(|f| &f.name).collect::<Vec<_>>()
);
}