use cch::Cch;
use cch::bundle::MetricBundle;
use cch::graph::Graph;
use routingkit_cch::ffi;
fn csr_from_arcs(node_count: u32, tail: &[u32], head: &[u32]) -> Graph {
assert_eq!(tail.len(), head.len());
let n = node_count as usize;
let mut degree = vec![0u32; n];
for &t in tail {
degree[t as usize] += 1;
}
let mut first_out = vec![0u32; n + 1];
for v in 0..n {
first_out[v + 1] = first_out[v] + degree[v];
}
let mut next = first_out[..n].to_vec();
let mut g_head = vec![0u32; head.len()];
for (&t, &h) in tail.iter().zip(head.iter()) {
let slot = next[t as usize] as usize;
g_head[slot] = h;
next[t as usize] += 1;
}
Graph {
first_out,
head: g_head,
weight: vec![1u32; head.len()],
}
}
fn assert_oracle_cross_compat(
name: &str,
node_count: u32,
tail: &[u32],
head: &[u32],
order: &[u32],
weights: &[u32],
) {
assert_eq!(
tail.len(),
weights.len(),
"[{name}] one weight per input arc"
);
let cch = unsafe { ffi::cch_new(order, tail, head, |_| {}, false) };
let cch_ref = cch.as_ref().expect("cch_new returned null");
let dir = tempfile::tempdir().expect("tempdir");
let struct_path = dir.path().join("oracle.cch-struct");
unsafe {
ffi::cch_save_struct(cch_ref, struct_path.to_str().unwrap()).expect("cch_save_struct");
}
let loaded = Cch::load_struct(&struct_path).expect("Cch::load_struct on oracle file");
let rust_metric = loaded.customize(weights);
let metric_path = dir.path().join("oracle.cch-metric");
let mut metric = unsafe { ffi::cch_metric_new(cch_ref, weights) };
unsafe {
ffi::cch_metric_customize(metric.as_mut().expect("metric pin"));
ffi::cch_save_metric(
metric.as_ref().expect("metric ref"),
metric_path.to_str().unwrap(),
)
.expect("cch_save_metric");
}
let mbundle = MetricBundle::open(&metric_path).expect("MetricBundle::open");
let mv = mbundle.view();
assert_eq!(
rust_metric.forward, mv.forward,
"[{name}] forward metric mismatch (oracle struct → rust load → rust customize)"
);
assert_eq!(
rust_metric.backward, mv.backward,
"[{name}] backward metric mismatch (oracle struct → rust load → rust customize)"
);
let fresh = Cch::build(&csr_from_arcs(node_count, tail, head), order);
assert_eq!(loaded.rank, fresh.rank, "[{name}] rank");
assert_eq!(loaded.up_head, fresh.up_head, "[{name}] up_head");
assert_eq!(loaded.down_to_up, fresh.down_to_up, "[{name}] down_to_up");
assert_eq!(
loaded.input_arc_to_cch_arc, fresh.input_arc_to_cch_arc,
"[{name}] input_arc_to_cch_arc"
);
}
#[test]
fn oracle_cross_compat_path_identity() {
let n = 5u32;
let tail = vec![0u32, 1, 1, 2, 2, 3, 3, 4];
let head = vec![1u32, 0, 2, 1, 3, 2, 4, 3];
let order: Vec<u32> = (0..n).collect();
let weights = vec![10u32, 11, 20, 21, 30, 31, 40, 41];
assert_oracle_cross_compat("path_identity", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_path_reversed() {
let n = 5u32;
let tail = vec![0u32, 1, 1, 2, 2, 3, 3, 4];
let head = vec![1u32, 0, 2, 1, 3, 2, 4, 3];
let order: Vec<u32> = (0..n).rev().collect();
let weights = vec![10u32, 11, 20, 21, 30, 31, 40, 41];
assert_oracle_cross_compat("path_reversed", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_fillin() {
let n = 4u32;
let tail = vec![0u32, 0, 0, 1, 2, 3];
let head = vec![1u32, 2, 3, 0, 0, 0];
let order: Vec<u32> = vec![0, 1, 2, 3];
let weights = vec![5u32, 7, 9, 6, 8, 10];
assert_oracle_cross_compat("fillin", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_parallel_arcs() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let order: Vec<u32> = vec![0, 1, 2, 3];
let weights = vec![50u32, 9, 40, 8, 17, 18, 19, 20];
assert_oracle_cross_compat("parallel_arcs", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_with_inf_weights() {
let n = 4u32;
let tail = vec![0u32, 0, 0, 1, 2, 3];
let head = vec![1u32, 2, 3, 0, 0, 0];
let order: Vec<u32> = vec![0, 1, 2, 3];
let inf = cch::INF_WEIGHT;
let weights = vec![5u32, inf, 9, inf, 8, 10];
assert_oracle_cross_compat("with_inf", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_grid_block_boundary() {
let cols = 24u32;
let rows = 24u32;
let n = cols * rows;
let mut tail = Vec::new();
let mut head = Vec::new();
for r in 0..rows {
for c in 0..cols {
let v = r * cols + c;
if c + 1 < cols {
tail.push(v);
head.push(v + 1);
}
if c > 0 {
tail.push(v);
head.push(v - 1);
}
if r + 1 < rows {
tail.push(v);
head.push(v + cols);
}
if r > 0 {
tail.push(v);
head.push(v - cols);
}
}
}
let graph = csr_from_arcs(n, &tail, &head);
let order = cch::degree_order(&graph);
let c = Cch::build(&graph, &order);
assert!(c.cch_arc_count() > 512, "grid must exceed 512 CCH arcs");
#[allow(clippy::cast_possible_truncation)]
let weights: Vec<u32> = (0..tail.len() as u32)
.map(|i| (i * 7 + 1) % 9973 + 1)
.collect();
assert_oracle_cross_compat("grid_24x24", n, &tail, &head, &order, &weights);
}
#[test]
fn oracle_cross_compat_empty_arcs() {
let n = 4u32;
let tail: Vec<u32> = vec![];
let head: Vec<u32> = vec![];
let order: Vec<u32> = (0..n).collect();
let weights: Vec<u32> = vec![];
assert_oracle_cross_compat("empty_arcs", n, &tail, &head, &order, &weights);
}
#[test]
fn rust_file_round_trip_recustomize() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let order: Vec<u32> = vec![2, 0, 3, 1];
let weights = vec![50u32, 9, 40, 8, 17, 18, 19, 20];
let graph = csr_from_arcs(n, &tail, &head);
let original = Cch::build(&graph, &order);
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("rt.cch-struct");
original.save_struct(&path).expect("save_struct");
let loaded = Cch::load_struct(&path).expect("load_struct");
let want = original.customize(&weights);
let got = loaded.customize(&weights);
assert_eq!(want.forward, got.forward, "forward re-customize");
assert_eq!(want.backward, got.backward, "backward re-customize");
}
#[test]
fn load_struct_missing_file_errors() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("does-not-exist.cch-struct");
let err = Cch::load_struct(&path).map(|_| ()).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
fn valid_struct_bytes() -> Vec<u8> {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr_from_arcs(n, &tail, &head), &[0, 1, 2, 3]);
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("v.cch-struct");
c.save_struct(&path).expect("save_struct");
std::fs::read(&path).expect("read back")
}
fn read_u64_usize(b: &[u8], o: usize) -> usize {
let mut buf = [0u8; 8];
buf.copy_from_slice(&b[o..o + 8]);
usize::try_from(u64::from_le_bytes(buf)).expect("fits")
}
fn section_start_offsets(b: &[u8]) -> Vec<usize> {
let mut starts = Vec::with_capacity(19);
let mut pos = 40usize;
for _ in 0..10 {
starts.push(pos);
pos += 8 + read_u64_usize(b, pos);
}
for _ in 0..3 {
starts.push(pos);
pos += 16 + read_u64_usize(b, pos + 8);
}
for _ in 0..6 {
starts.push(pos);
pos += 8 + read_u64_usize(b, pos);
}
starts
}
fn write_and_load(dir: &std::path::Path, name: &str, bytes: &[u8]) -> std::io::Error {
let path = dir.join(name);
std::fs::write(&path, bytes).expect("write corrupt file");
Cch::load_struct(&path).map(|_| ()).unwrap_err()
}
#[test]
fn load_struct_rejects_bad_magic() {
let dir = tempfile::tempdir().expect("tempdir");
let mut b = valid_struct_bytes();
b[0..8].copy_from_slice(&0xDEAD_BEEF_DEAD_BEEFu64.to_le_bytes());
let err = write_and_load(dir.path(), "bad.cch-struct", &b);
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("bad magic"));
}
#[test]
fn load_struct_rejects_truncated() {
let dir = tempfile::tempdir().expect("tempdir");
let b = valid_struct_bytes();
let err = write_and_load(dir.path(), "trunc.cch-struct", &b[..40]);
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("truncated"));
}
#[test]
fn load_struct_rejects_inflated_count() {
let dir = tempfile::tempdir().expect("tempdir");
let mut b = valid_struct_bytes();
b[16..24].copy_from_slice(&u64::MAX.to_le_bytes());
let err = write_and_load(dir.path(), "ovf.cch-struct", &b);
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("does not match expected"));
}
#[test]
fn load_struct_rejects_non_monotonic_extra_csr() {
let dir = tempfile::tempdir().expect("tempdir");
let mut b = valid_struct_bytes();
let starts = section_start_offsets(&b);
let data = starts[15] + 8;
b[data..data + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let err = write_and_load(dir.path(), "csr.cch-struct", &b);
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("not monotonic"));
}