#![allow(clippy::pedantic)]
use srcwalk::cache::OutlineCache;
use std::path::{Path, PathBuf};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn fixture_scope() -> PathBuf {
repo_root().join("src")
}
fn run_callers(target: &str, scope: &Path, depth: Option<usize>, json: bool) -> String {
run_callers_capped(target, scope, depth, json, Some(20_000))
}
fn run_callers_capped(
target: &str,
scope: &Path,
depth: Option<usize>,
json: bool,
max_edges: Option<usize>,
) -> String {
let cache = OutlineCache::new();
srcwalk::run_callers(
target, scope, 0, None, None,
0, None, &cache, depth, None,
max_edges, None, None, None,
json,
)
.expect("run_callers should succeed on fixture")
}
fn strip_timing(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b',' && i + 1 < bytes.len() && bytes[i + 1] == b' ' {
let mut j = i + 2;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j > i + 2 && j + 3 <= bytes.len() && &bytes[j..j + 3] == b" ms" {
i = j + 3;
continue;
}
}
if bytes[i] == b'(' && i + 2 < bytes.len() && &bytes[i + 1..i + 2] == b"~" {
let mut j = i + 2;
while j < bytes.len()
&& (bytes[j].is_ascii_digit() || bytes[j] == b'.' || bytes[j] == b'k')
{
j += 1;
}
if j > i + 2 && j + 8 <= bytes.len() && &bytes[j..j + 8] == b" tokens)" {
i = j + 8;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
#[test]
fn legacy_equals_depth_1() {
let scope = fixture_scope();
let legacy = run_callers("find_callers_batch", &scope, None, false);
let depth_1 = run_callers("find_callers_batch", &scope, Some(1), false);
let lines_eq = |a: &str, b: &str| {
let mut la: Vec<&str> = a.lines().collect();
let mut lb: Vec<&str> = b.lines().collect();
la.sort();
lb.sort();
la == lb
};
assert!(
lines_eq(&legacy, &depth_1),
"legacy (no --depth) and --depth 1 must produce the same line-set\n---\nLEGACY:\n{legacy}\n---\nDEPTH_1:\n{depth_1}"
);
}
#[test]
fn bfs_deterministic_across_runs() {
let scope = fixture_scope();
let out1 = strip_timing(&run_callers("find_callers_batch", &scope, Some(3), false));
let out2 = strip_timing(&run_callers("find_callers_batch", &scope, Some(3), false));
let out3 = strip_timing(&run_callers("find_callers_batch", &scope, Some(3), false));
assert_eq!(out1, out2, "BFS run 1 vs 2 must be deterministic");
assert_eq!(out2, out3, "BFS run 2 vs 3 must be deterministic");
}
fn extract_edges(json: &str) -> Vec<(u32, String, String, u32, String)> {
let v: serde_json::Value = serde_json::from_str(json).expect("valid json");
v["edges"]
.as_array()
.expect("edges array")
.iter()
.map(|e| {
(
e["hop"].as_u64().unwrap() as u32,
e["from"].as_str().unwrap().to_string(),
e["from_file"].as_str().unwrap().to_string(),
e["from_line"].as_u64().unwrap() as u32,
e["to"].as_str().unwrap().to_string(),
)
})
.collect()
}
#[test]
fn deeper_bfs_superset_of_shallower() {
let scope = fixture_scope();
let e2 = extract_edges(&run_callers("find_callers_batch", &scope, Some(2), true));
let e3 = extract_edges(&run_callers("find_callers_batch", &scope, Some(3), true));
let hop1_from_d2: std::collections::HashSet<_> = e2.iter().filter(|e| e.0 == 1).collect();
let hop1_from_d3: std::collections::HashSet<_> = e3.iter().filter(|e| e.0 == 1).collect();
assert_eq!(
hop1_from_d2, hop1_from_d3,
"hop-1 edges must be identical regardless of max_depth"
);
}
#[test]
fn auto_hub_promotes_new_on_self() {
let scope = repo_root(); let json = run_callers("Session", &scope, Some(4), true);
let v: serde_json::Value = serde_json::from_str(&json).expect("valid json");
let promoted = v["elided"]["auto_hubs_promoted"]
.as_array()
.expect("auto_hubs_promoted array");
assert!(
!promoted.is_empty(),
"expected at least one auto-promoted hub on self-BFS; got empty"
);
let new_entry = promoted
.iter()
.find(|e| e["symbol"].as_str() == Some("new"));
assert!(
new_entry.is_some(),
"expected `new` to be auto-promoted; got: {promoted:?}"
);
let edges = new_entry.unwrap()["edges"].as_u64().unwrap();
assert!(
edges >= 200,
"auto-promoted `new` must have >=200 edges (threshold); got {edges}"
);
}
#[test]
fn json_schema_top_level_keys() {
let scope = fixture_scope();
let json = run_callers("find_callers_batch", &scope, Some(2), true);
let v: serde_json::Value = serde_json::from_str(&json).expect("valid json");
for key in [
"root",
"scope",
"depth_reached",
"max_depth",
"edges_total",
"elapsed_ms",
"edges",
"stats",
"elided",
"disclaimer",
] {
assert!(
v.get(key).is_some(),
"JSON missing top-level key `{key}`; schema changed"
);
}
for key in ["per_hop", "top_level_terminal", "unresolved_symbols"] {
assert!(
v["stats"].get(key).is_some(),
"JSON missing stats.{key}; schema changed"
);
}
for key in [
"edges_cut_at_hop",
"frontier_cuts",
"hubs_skipped",
"auto_hubs_promoted",
"auto_hub_threshold",
] {
assert!(
v["elided"].get(key).is_some(),
"JSON missing elided.{key}; schema changed"
);
}
}
#[test]
fn bfs_deterministic_under_edge_cap() {
let scope = repo_root();
let j1 = run_callers_capped("Session", &scope, Some(4), true, Some(50));
let j2 = run_callers_capped("Session", &scope, Some(4), true, Some(50));
let j3 = run_callers_capped("Session", &scope, Some(4), true, Some(50));
let e1 = extract_edges(&j1);
let e2 = extract_edges(&j2);
let e3 = extract_edges(&j3);
assert_eq!(e1, e2, "cap-truncated BFS run 1 vs 2 must be deterministic");
assert_eq!(e2, e3, "cap-truncated BFS run 2 vs 3 must be deterministic");
let v: serde_json::Value = serde_json::from_str(&j1).unwrap();
assert!(
v["elided"]["edges_cut_at_hop"].is_number(),
"test fixture no longer trips the edge cap; pick a heavier target or lower the cap"
);
}
#[test]
fn bfs_hop2_finds_edges_after_qualified_frontier() {
let scope = fixture_scope();
let json = run_callers("search_callers_expanded", &scope, Some(3), true);
let v: serde_json::Value = serde_json::from_str(&json).expect("valid json");
let per_hop = v["stats"]["per_hop"].as_array().expect("per_hop array");
assert!(
per_hop.len() >= 2,
"expected at least 2 hops; got {}",
per_hop.len()
);
let hop2_edges = per_hop[1]["edges"].as_u64().unwrap_or(0);
assert!(
hop2_edges > 0,
"hop 2 must have >0 edges (frontier bare-name extraction); got 0"
);
}