use super::types::{CsrIndex, Direction};
fn make_csr() -> CsrIndex {
let mut csr = CsrIndex::new();
csr.add_edge("a", "KNOWS", "b").unwrap();
csr.add_edge("b", "KNOWS", "c").unwrap();
csr.add_edge("c", "KNOWS", "d").unwrap();
csr.add_edge("a", "WORKS", "e").unwrap();
csr
}
#[test]
fn neighbors_out() {
let csr = make_csr();
let n = csr.neighbors("a", None, Direction::Out);
assert_eq!(n.len(), 2);
let dsts: Vec<&str> = n.iter().map(|(_, d)| d.as_str()).collect();
assert!(dsts.contains(&"b"));
assert!(dsts.contains(&"e"));
}
#[test]
fn neighbors_filtered() {
let csr = make_csr();
let n = csr.neighbors("a", Some("KNOWS"), Direction::Out);
assert_eq!(n.len(), 1);
assert_eq!(n[0].1, "b");
}
#[test]
fn neighbors_in() {
let csr = make_csr();
let n = csr.neighbors("b", None, Direction::In);
assert_eq!(n.len(), 1);
assert_eq!(n[0].1, "a");
}
#[test]
fn incremental_remove() {
let mut csr = make_csr();
assert_eq!(csr.neighbors("a", Some("KNOWS"), Direction::Out).len(), 1);
csr.remove_edge("a", "KNOWS", "b");
assert_eq!(csr.neighbors("a", Some("KNOWS"), Direction::Out).len(), 0);
}
#[test]
fn duplicate_add_is_idempotent() {
let mut csr = CsrIndex::new();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("a", "L", "b").unwrap();
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 1);
}
#[test]
fn compact_merges_buffer_into_dense() {
let mut csr = CsrIndex::new();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("b", "L", "c").unwrap();
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 1);
csr.compact();
assert!(csr.buffer_out.iter().all(|b| b.is_empty()));
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 1);
assert_eq!(csr.neighbors("b", None, Direction::Out).len(), 1);
}
#[test]
fn compact_handles_deletes() {
let mut csr = CsrIndex::new();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("a", "L", "c").unwrap();
csr.compact();
csr.remove_edge("a", "L", "b");
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 1);
csr.compact();
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 1);
assert_eq!(csr.neighbors("a", None, Direction::Out)[0].1, "c");
}
#[test]
fn label_interning_reduces_memory() {
let mut csr = CsrIndex::new();
for i in 0..100 {
csr.add_edge(&format!("n{i}"), "FOLLOWS", &format!("n{}", i + 1))
.unwrap();
}
assert_eq!(csr.id_to_label.len(), 1);
assert_eq!(csr.id_to_label[0], "FOLLOWS");
}
#[test]
fn edge_count() {
let csr = make_csr();
assert_eq!(csr.edge_count(), 4);
}
#[test]
fn checkpoint_roundtrip() {
let mut csr = make_csr();
csr.compact();
let bytes = csr.checkpoint_to_bytes();
assert!(!bytes.is_empty());
let restored = CsrIndex::from_checkpoint(&bytes).expect("roundtrip");
assert_eq!(restored.node_count(), csr.node_count());
assert_eq!(restored.edge_count(), csr.edge_count());
let n = restored.neighbors("a", Some("KNOWS"), Direction::Out);
assert_eq!(n.len(), 1);
assert_eq!(n[0].1, "b");
}
#[test]
fn memory_estimation() {
let csr = make_csr();
let mem = csr.estimated_memory_bytes();
assert!(mem > 0);
}
#[test]
fn out_degree_and_in_degree() {
let mut csr = CsrIndex::new();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("a", "L", "c").unwrap();
csr.add_edge("d", "L", "b").unwrap();
let a_id = *csr.node_to_id.get("a").unwrap();
let b_id = *csr.node_to_id.get("b").unwrap();
assert_eq!(csr.out_degree(a_id), 2);
assert_eq!(csr.in_degree(b_id), 2);
}
#[test]
fn remove_node_edges_all() {
let mut csr = CsrIndex::new();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("a", "L", "c").unwrap();
csr.add_edge("d", "L", "a").unwrap();
let removed = csr.remove_node_edges("a");
assert_eq!(removed, 3);
assert_eq!(csr.neighbors("a", None, Direction::Out).len(), 0);
assert_eq!(csr.neighbors("a", None, Direction::In).len(), 0);
}
#[test]
fn add_node_idempotent() {
let mut csr = CsrIndex::new();
let id1 = csr.add_node("x");
let id2 = csr.add_node("x");
assert_eq!(id1, id2);
assert_eq!(csr.node_count(), 1);
}
#[test]
fn node_labels_bitset() {
let mut csr = CsrIndex::new();
csr.add_edge("alice", "KNOWS", "bob").unwrap();
csr.add_edge("acme", "EMPLOYS", "alice").unwrap();
assert!(csr.add_node_label("alice", "Person"));
assert!(csr.add_node_label("bob", "Person"));
assert!(csr.add_node_label("acme", "Company"));
let alice_id = csr.node_id("alice").unwrap();
let bob_id = csr.node_id("bob").unwrap();
let acme_id = csr.node_id("acme").unwrap();
assert!(csr.node_has_label(alice_id, "Person"));
assert!(!csr.node_has_label(alice_id, "Company"));
assert!(csr.node_has_label(acme_id, "Company"));
assert!(!csr.node_has_label(acme_id, "Person"));
assert!(csr.add_node_label("alice", "Employee"));
assert!(csr.node_has_label(alice_id, "Person"));
assert!(csr.node_has_label(alice_id, "Employee"));
assert_eq!(csr.node_labels(alice_id), vec!["Person", "Employee"]);
csr.remove_node_label("alice", "Employee");
assert!(!csr.node_has_label(alice_id, "Employee"));
assert!(csr.node_has_label(alice_id, "Person"));
assert!(!csr.node_has_label(bob_id, "NonExistent"));
}
#[test]
fn edge_label_interning_does_not_alias_past_u16_max() {
let mut csr = CsrIndex::new();
const N: usize = 65_537;
let mut ids: Vec<u32> = Vec::with_capacity(N);
for i in 0..N {
let label = format!("l_{i}");
csr.add_edge("src", &label, "dst").unwrap();
let id = csr
.label_id(&label)
.expect("label_id must resolve just-inserted label");
ids.push(id);
}
let unique: std::collections::HashSet<u32> = ids.iter().copied().collect();
assert_eq!(
unique.len(),
N,
"every distinct label must map to a distinct id; got {} unique ids for {} labels",
unique.len(),
N
);
for (i, &id) in ids.iter().enumerate() {
let name = csr.label_name(id);
assert_eq!(
name,
format!("l_{i}"),
"label_name({id}) must round-trip to inserted label l_{i}; got {name:?}"
);
}
}
#[test]
fn edge_label_ids_survive_compaction() {
let mut csr = CsrIndex::new();
const N: usize = 512;
for i in 0..N {
csr.add_edge(&format!("src_{i}"), &format!("L_{i}"), &format!("dst_{i}"))
.unwrap();
}
let before: Vec<u32> = (0..N)
.map(|i| csr.label_id(&format!("L_{i}")).expect("label present"))
.collect();
csr.compact();
for (i, &id) in before.iter().enumerate() {
let after_id = csr
.label_id(&format!("L_{i}"))
.expect("label must remain resolvable after compact");
assert_eq!(
after_id, id,
"label id for L_{i} must be stable across compact(); before={id} after={after_id}"
);
assert_eq!(
csr.label_name(id),
format!("L_{i}"),
"label_name({id}) must still round-trip after compact"
);
}
}