#![cfg(feature = "rebuild-internals")]
#![allow(clippy::too_many_lines)]
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Instant;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::file::FileId;
use sqry_core::graph::unified::node::{NodeId, NodeKind};
use sqry_core::graph::unified::publish::assert_publish_bijection;
use sqry_core::graph::unified::rebuild::RebuildGraph;
use sqry_core::graph::unified::storage::NodeEntry;
const SCALE_FILES: usize = if cfg!(debug_assertions) { 100 } else { 1000 };
const SCALE_NODES_PER_FILE: usize = 200;
const SCALE_FANOUT_PER_NODE: usize = 10;
fn build_synthetic_rebuild() -> (RebuildGraph, Vec<FileId>, Vec<Vec<NodeId>>) {
let mut graph = CodeGraph::new();
let sym = graph.strings_mut().intern("sym").expect("intern");
let mut file_ids = Vec::with_capacity(SCALE_FILES);
let mut file_nodes: Vec<Vec<NodeId>> = Vec::with_capacity(SCALE_FILES);
for i in 0..SCALE_FILES {
let path = PathBuf::from(format!("/tmp/sqryd_scale_fixture/file_{i:05}.rs"));
let fid = graph.files_mut().register(&path).expect("register file");
file_ids.push(fid);
let mut nodes = Vec::with_capacity(SCALE_NODES_PER_FILE);
let mut first_slot: Option<u32> = None;
let mut last_slot: u32 = 0;
for _ in 0..SCALE_NODES_PER_FILE {
let nid = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, sym, fid))
.expect("alloc node");
if first_slot.is_none() {
first_slot = Some(nid.index());
}
last_slot = nid.index();
nodes.push(nid);
graph.files_mut().record_node(fid, nid);
graph
.indices_mut()
.add(nid, NodeKind::Function, sym, None, fid);
}
let start_slot = first_slot.expect("SCALE_NODES_PER_FILE > 0");
let slot_count = last_slot - start_slot + 1;
graph.test_only_record_file_segment(fid, start_slot, slot_count);
file_nodes.push(nodes);
}
for (file_idx, nodes) in file_nodes.iter().enumerate() {
let fid = file_ids[file_idx];
let n = nodes.len();
for i in 0..n {
for k in 1..=SCALE_FANOUT_PER_NODE {
let target = nodes[(i + k) % n];
graph.edges_mut().add_edge(
nodes[i],
target,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
fid,
);
}
}
}
for i in 0..SCALE_FILES.saturating_sub(1) {
graph.edges_mut().add_edge(
file_nodes[i][0],
file_nodes[i + 1][0],
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_ids[i],
);
}
let rebuild = graph.clone_for_rebuild();
(rebuild, file_ids, file_nodes)
}
#[test]
fn incremental_remove_file_scale_all_buckets_drain_and_invariants_hold() {
let build_start = Instant::now();
let (mut rebuild, file_ids, file_nodes) = build_synthetic_rebuild();
let build_elapsed = build_start.elapsed();
eprintln!(
"[scale] built {SCALE_FILES}×{SCALE_NODES_PER_FILE} synthetic rebuild in {:.2?}",
build_elapsed
);
for &fid in &file_ids {
assert!(
rebuild.file_segments().get(fid).is_some(),
"every file must have a FileSegmentTable entry before remove_file; {fid:?} missing"
);
}
let expected_dead: HashSet<NodeId> = file_nodes.iter().flatten().copied().collect();
assert_eq!(
expected_dead.len(),
SCALE_FILES * SCALE_NODES_PER_FILE,
"expected every seeded node to be distinct"
);
let remove_start = Instant::now();
let mut returned_union: HashSet<NodeId> = HashSet::with_capacity(expected_dead.len());
for &fid in &file_ids {
let returned = rebuild.remove_file(fid);
returned_union.extend(returned);
}
let remove_elapsed = remove_start.elapsed();
eprintln!(
"[scale] removed {} files in {:.2?}",
file_ids.len(),
remove_elapsed
);
assert!(
remove_elapsed.as_secs() < 60,
"mass removal must complete in <60s (release profile); took {remove_elapsed:.2?}"
);
assert_eq!(
returned_union, expected_dead,
"remove_file's returned NodeIds (unioned over every file) must equal \
the seeded per-file bucket membership"
);
for &fid in &file_ids {
assert!(
rebuild.files().nodes_for_file(fid).is_empty(),
"per-file bucket for {fid:?} must be drained after remove_file"
);
assert!(
rebuild.files().resolve(fid).is_none(),
"FileRegistry::resolve({fid:?}) must return None after remove_file"
);
assert!(
rebuild.file_segments().get(fid).is_none(),
"FileSegmentTable entry for {fid:?} must be cleared after remove_file"
);
}
assert_eq!(
rebuild.file_segments().segment_count(),
0,
"every FileSegmentTable entry must be cleared after removing every file"
);
assert_eq!(
rebuild.nodes().len(),
0,
"every arena slot must be tombstoned after removing every file"
);
assert_eq!(
rebuild.pending_tombstone_count(),
expected_dead.len(),
"finalize's K.A/K.B sweep must see the union of every remove_file call"
);
let finalize_start = Instant::now();
let finalized = rebuild.finalize().expect("finalize must succeed");
let finalize_elapsed = finalize_start.elapsed();
eprintln!("[scale] finalize completed in {:.2?}", finalize_elapsed);
assert_publish_bijection(&finalized);
assert_eq!(
finalized.nodes().len(),
0,
"finalized arena must have zero live nodes"
);
assert_eq!(
finalized.file_segments().segment_count(),
0,
"finalized FileSegmentTable must be empty after mass removal"
);
let forward_stats = finalized.edges().stats().forward;
assert_eq!(
forward_stats.delta_edge_count, 0,
"finalized forward delta must be empty — finalize absorbs delta \
into CSR and the CSR must contain no edges pointing at dead slots"
);
for (nid, _entry) in finalized.nodes().iter() {
let out = finalized.edges().edges_from(nid);
assert!(
out.is_empty(),
"no live node should have outgoing edges after mass removal; \
{nid:?} has {} edges",
out.len()
);
let inc = finalized.edges().edges_to(nid);
assert!(
inc.is_empty(),
"no live node should have incoming edges after mass removal; \
{nid:?} has {} edges",
inc.len()
);
}
assert_publish_bijection(&finalized);
sqry_core::graph::unified::publish::assert_publish_invariants(&finalized, &expected_dead);
let total = remove_elapsed + finalize_elapsed;
eprintln!(
"[scale] remove+finalize total {:.2?} for {SCALE_FILES} files",
total
);
}
#[test]
fn incremental_remove_file_scale_half_removal_preserves_remainder() {
let (mut rebuild, file_ids, file_nodes) = build_synthetic_rebuild();
let mut expected_dead: HashSet<NodeId> = HashSet::new();
let mut expected_live: HashSet<NodeId> = HashSet::new();
for (i, nodes) in file_nodes.iter().enumerate() {
if i % 2 == 0 {
expected_dead.extend(nodes.iter().copied());
} else {
expected_live.extend(nodes.iter().copied());
}
}
for (i, &fid) in file_ids.iter().enumerate() {
if i % 2 == 0 {
let _ = rebuild.remove_file(fid);
}
}
for (i, &fid) in file_ids.iter().enumerate() {
if i % 2 == 0 {
assert!(
rebuild.file_segments().get(fid).is_none(),
"even-indexed file {fid:?} segment must be cleared"
);
} else {
assert!(
rebuild.file_segments().get(fid).is_some(),
"odd-indexed file {fid:?} segment must survive partial removal"
);
}
}
let finalized = rebuild.finalize().expect("finalize must succeed");
for nid in &expected_dead {
assert!(
finalized.nodes().get(*nid).is_none(),
"node {nid:?} from an even-indexed (removed) file must be tombstoned"
);
}
for nid in &expected_live {
assert!(
finalized.nodes().get(*nid).is_some(),
"node {nid:?} from an odd-indexed (live) file must survive"
);
}
let expected_live_segments = SCALE_FILES / 2;
assert_eq!(
finalized.file_segments().segment_count(),
expected_live_segments,
"only odd-indexed files' segments must survive partial removal"
);
assert_publish_bijection(&finalized);
sqry_core::graph::unified::publish::assert_publish_invariants(&finalized, &expected_dead);
for (i, nodes) in file_nodes.iter().enumerate() {
if i % 2 == 0 {
continue;
}
assert!(nodes.len() >= 2);
let out = finalized.edges().edges_from(nodes[0]);
assert!(
out.iter().any(|e| e.target == nodes[1]),
"intra-file edge nodes[0]->nodes[1] in file {i} must survive"
);
}
}
#[test]
fn remove_file_clears_file_segments_rebuild_path() {
let mut graph = CodeGraph::new();
let sym = graph.strings_mut().intern("sym").expect("intern");
let path = PathBuf::from("/tmp/sqryd_segment_fixture/only_file.rs");
let fid = graph.files_mut().register(&path).expect("register file");
let mut nodes = Vec::new();
let mut first_slot: Option<u32> = None;
let mut last_slot: u32 = 0;
for _ in 0..5 {
let nid = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, sym, fid))
.expect("alloc node");
if first_slot.is_none() {
first_slot = Some(nid.index());
}
last_slot = nid.index();
nodes.push(nid);
graph.files_mut().record_node(fid, nid);
}
let start_slot = first_slot.expect("5 nodes allocated");
graph.test_only_record_file_segment(fid, start_slot, last_slot - start_slot + 1);
assert!(graph.file_segments().get(fid).is_some());
let mut rebuild = graph.clone_for_rebuild();
let removed = rebuild.remove_file(fid);
assert_eq!(removed.len(), 5, "every node must be returned");
assert!(
rebuild.file_segments().get(fid).is_none(),
"FileSegmentTable entry must be cleared by RebuildGraph::remove_file"
);
let finalized = rebuild.finalize().expect("finalize must succeed");
assert!(
finalized.file_segments().get(fid).is_none(),
"finalize must publish a FileSegmentTable with no entry for the removed file"
);
assert_eq!(
finalized.file_segments().segment_count(),
0,
"no other segments should exist"
);
}
#[test]
fn remove_file_clears_file_segments_codegraph_path() {
let mut graph = CodeGraph::new();
let sym = graph.strings_mut().intern("sym").expect("intern");
let path = PathBuf::from("/tmp/sqryd_segment_fixture/codegraph_only_file.rs");
let fid = graph.files_mut().register(&path).expect("register file");
let mut first_slot: Option<u32> = None;
let mut last_slot: u32 = 0;
for _ in 0..5 {
let nid = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, sym, fid))
.expect("alloc node");
if first_slot.is_none() {
first_slot = Some(nid.index());
}
last_slot = nid.index();
graph.files_mut().record_node(fid, nid);
}
let start_slot = first_slot.expect("5 nodes allocated");
graph.test_only_record_file_segment(fid, start_slot, last_slot - start_slot + 1);
assert!(graph.file_segments().get(fid).is_some());
let mut rebuild = graph.clone_for_rebuild();
let _ = rebuild.remove_file(fid);
assert!(rebuild.file_segments().get(fid).is_none());
}
#[test]
fn remove_file_tombstones_only_the_target_range_after_file_id_recycle() {
let mut graph = CodeGraph::new();
let sym = graph.strings_mut().intern("sym").expect("intern");
let path_a = PathBuf::from("/tmp/sqryd_recycle_fixture/file_a.rs");
let fid_a = graph
.files_mut()
.register(&path_a)
.expect("register file A");
let mut a_first: Option<u32> = None;
let mut a_last: u32 = 0;
for _ in 0..4 {
let nid = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, sym, fid_a))
.expect("alloc");
if a_first.is_none() {
a_first = Some(nid.index());
}
a_last = nid.index();
graph.files_mut().record_node(fid_a, nid);
}
let a_start = a_first.expect("4 allocated");
let a_slot_count = a_last - a_start + 1;
graph.test_only_record_file_segment(fid_a, a_start, a_slot_count);
let recorded_a = *graph
.file_segments()
.get(fid_a)
.expect("segment A recorded");
assert_eq!(recorded_a.start_slot, a_start);
assert_eq!(recorded_a.slot_count, a_slot_count);
let mut rebuild = graph.clone_for_rebuild();
let _removed_a = rebuild.remove_file(fid_a);
assert!(
rebuild.file_segments().get(fid_a).is_none(),
"RebuildGraph::remove_file must clear the file's segment entry"
);
let after_a = rebuild.finalize().expect("finalize");
assert!(
after_a.file_segments().get(fid_a).is_none(),
"finalize must not republish file A's stale segment"
);
assert_eq!(
after_a.file_segments().segment_count(),
0,
"no stale segments must survive finalize"
);
let mut graph_after = after_a.clone();
let path_b = PathBuf::from("/tmp/sqryd_recycle_fixture/file_b.rs");
let fid_b = graph_after
.files_mut()
.register(&path_b)
.expect("register file B");
assert_eq!(
fid_b.index(),
fid_a.index(),
"FileRegistry must recycle file A's FileId when registering file B; \
without recycling, the stale-segment attack surface doesn't apply"
);
assert!(
graph_after.file_segments().get(fid_b).is_none(),
"recycled FileId must not inherit file A's stale segment"
);
let mut b_first: Option<u32> = None;
let mut b_last: u32 = 0;
let mut b_indices: Vec<u32> = Vec::new();
for _ in 0..6 {
let nid = graph_after
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, sym, fid_b))
.expect("alloc");
if b_first.is_none() {
b_first = Some(nid.index());
}
b_last = nid.index();
b_indices.push(nid.index());
graph_after.files_mut().record_node(fid_b, nid);
}
let b_start = b_first.expect("6 allocated");
let b_min = *b_indices.iter().min().expect("6 allocated");
let b_max = *b_indices.iter().max().expect("6 allocated");
let b_span_start = b_min;
let b_span_count = b_max - b_min + 1;
graph_after.test_only_record_file_segment(fid_b, b_span_start, b_span_count);
let seg_b = *graph_after
.file_segments()
.get(fid_b)
.expect("segment B recorded");
assert_eq!(
seg_b.start_slot, b_span_start,
"fid_b's segment must map to file B's new start slot, not file A's"
);
assert_eq!(
seg_b.slot_count, b_span_count,
"fid_b's segment must map to file B's new slot count, not file A's"
);
assert!(
seg_b.slot_count != a_slot_count,
"fid_b's segment slot_count ({}) must not equal file A's stale count ({}) — \
this would indicate the iter-1 fix is missing and the stale segment leaked",
seg_b.slot_count,
a_slot_count,
);
let _ = b_start;
let _ = b_last;
}