#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use fsci_conformance::{ArmCounts, CompareLedger};
use fsci_sparse::{CooMatrix, CsrMatrix, FormatConvertible, Shape2D, shortest_path};
use serde::Serialize;
const PACKET_ID: &str = "FSCI-P2C-007";
const ABS_TOL: f64 = 1.0e-12;
const ARMS: [&str; 10] = [
"g1_0_to_4_distance",
"g1_0_to_4_path_starts_at_source",
"g1_0_to_4_path_ends_at_target",
"g1_path_edges_valid",
"g1_path_sum_equals_distance",
"self_distance_zero",
"disconnected_returns_infinity",
"oob_source_returns_infinity",
"oob_target_returns_infinity",
"indirect_path_wins",
];
#[derive(Debug, Clone, Serialize)]
struct CaseDiff {
case_id: String,
pass: bool,
note: String,
}
#[derive(Debug, Clone, Serialize)]
struct DiffLog {
test_id: String,
category: String,
case_count: usize,
compared: BTreeMap<String, ArmCounts>,
pass: bool,
timestamp_ms: u128,
duration_ns: u128,
cases: Vec<CaseDiff>,
}
fn output_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("fixtures/artifacts/{PACKET_ID}/diff"))
}
fn ensure_output_dir() {
fs::create_dir_all(output_dir()).expect("create shortest_path diff dir");
}
fn timestamp_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis())
}
fn emit_log(log: &DiffLog) {
ensure_output_dir();
let path = output_dir().join(format!("{}.json", log.test_id));
let json = serde_json::to_string_pretty(log).expect("serialize log");
fs::write(path, json).expect("write log");
}
fn build_csr(n: usize, trips: &[(usize, usize, f64)]) -> CsrMatrix {
let data: Vec<f64> = trips.iter().map(|t| t.2).collect();
let rs: Vec<usize> = trips.iter().map(|t| t.0).collect();
let cs: Vec<usize> = trips.iter().map(|t| t.1).collect();
let coo = CooMatrix::from_triplets(Shape2D::new(n, n), data, rs, cs, true).unwrap();
coo.to_csr().unwrap()
}
fn edge_weight(g: &CsrMatrix, u: usize, v: usize) -> Option<f64> {
let indptr = g.indptr();
let indices = g.indices();
let data = g.data();
let start = indptr[u];
let end = indptr[u + 1];
for idx in start..end {
if indices[idx] == v {
return Some(data[idx]);
}
}
None
}
#[test]
fn diff_sparse_shortest_path_properties() {
let start = Instant::now();
let mut diffs: Vec<CaseDiff> = Vec::new();
let mut ledger = CompareLedger::new("diff_sparse_shortest_path_properties", &ARMS);
let mut check = |id: &str, ok: bool, note: String| {
diffs.push(CaseDiff {
case_id: id.into(),
pass: ok,
note,
});
};
let g1 = build_csr(
5,
&[
(0, 1, 1.0),
(0, 2, 4.0),
(1, 2, 2.0),
(1, 3, 5.0),
(2, 3, 1.0),
(3, 4, 3.0),
],
);
{
let (d, p) = shortest_path(&g1, true, 0, 4);
if ledger
.pair(
"g1_0_to_4_distance",
"g1_0_to_4_distance",
Some(7.0),
Some(d),
)
.is_some()
{
ledger.compared(
"g1_0_to_4_distance",
"g1_0_to_4_distance",
(d - 7.0).abs() < ABS_TOL,
);
}
check(
"g1_0_to_4_distance",
(d - 7.0).abs() < ABS_TOL,
format!("d={d} p={p:?}"),
);
ledger.compared(
"g1_0_to_4_path_starts_at_source",
"g1_0_to_4_path_starts_at_source",
p.first() == Some(&0),
);
check(
"g1_0_to_4_path_starts_at_source",
p.first() == Some(&0),
format!("p={p:?}"),
);
ledger.compared(
"g1_0_to_4_path_ends_at_target",
"g1_0_to_4_path_ends_at_target",
p.last() == Some(&4),
);
check(
"g1_0_to_4_path_ends_at_target",
p.last() == Some(&4),
format!("p={p:?}"),
);
let mut edges_valid = true;
let mut path_sum = 0.0;
for w in p.windows(2) {
match edge_weight(&g1, w[0], w[1]) {
Some(weight) => path_sum += weight,
None => {
edges_valid = false;
break;
}
}
}
ledger.compared("g1_path_edges_valid", "g1_path_edges_valid", edges_valid);
check("g1_path_edges_valid", edges_valid, format!("p={p:?}"));
if ledger
.pair(
"g1_path_sum_equals_distance",
"g1_path_sum_equals_distance",
Some(d),
Some(path_sum),
)
.is_some()
{
ledger.compared(
"g1_path_sum_equals_distance",
"g1_path_sum_equals_distance",
(path_sum - d).abs() < ABS_TOL,
);
}
check(
"g1_path_sum_equals_distance",
(path_sum - d).abs() < ABS_TOL,
format!("path_sum={path_sum} d={d}"),
);
}
{
let (d, p) = shortest_path(&g1, true, 2, 2);
if ledger
.pair(
"self_distance_zero",
"self_distance_zero",
Some(0.0),
Some(d),
)
.is_some()
{
ledger.compared(
"self_distance_zero",
"self_distance_zero",
d == 0.0 && p == vec![2],
);
}
check(
"self_distance_zero",
d == 0.0 && p == vec![2],
format!("d={d} p={p:?}"),
);
}
{
let (d, p) = shortest_path(&g1, true, 4, 0);
ledger.compared(
"disconnected_returns_infinity",
"disconnected_returns_infinity",
d == f64::INFINITY && p.is_empty(),
);
check(
"disconnected_returns_infinity",
d == f64::INFINITY && p.is_empty(),
format!("d={d} p={p:?}"),
);
}
{
let (d, p) = shortest_path(&g1, true, 99, 0);
ledger.compared(
"oob_source_returns_infinity",
"oob_source_returns_infinity",
d == f64::INFINITY && p.is_empty(),
);
check(
"oob_source_returns_infinity",
d == f64::INFINITY && p.is_empty(),
format!("d={d} p={p:?}"),
);
}
{
let (d, p) = shortest_path(&g1, true, 0, 99);
ledger.compared(
"oob_target_returns_infinity",
"oob_target_returns_infinity",
d == f64::INFINITY && p.is_empty(),
);
check(
"oob_target_returns_infinity",
d == f64::INFINITY && p.is_empty(),
format!("d={d} p={p:?}"),
);
}
{
let g2 = build_csr(3, &[(0, 1, 5.0), (1, 2, 7.0), (0, 2, 100.0)]);
let (d, p) = shortest_path(&g2, true, 0, 2);
if ledger
.pair(
"indirect_path_wins",
"indirect_path_wins",
Some(12.0),
Some(d),
)
.is_some()
{
ledger.compared(
"indirect_path_wins",
"indirect_path_wins",
(d - 12.0).abs() < ABS_TOL && p == vec![0, 1, 2],
);
}
check(
"indirect_path_wins",
(d - 12.0).abs() < ABS_TOL && p == vec![0, 1, 2],
format!("d={d} p={p:?}"),
);
}
let all_pass = diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_sparse_shortest_path_properties".into(),
category: "fsci_sparse::shortest_path property-based coverage".into(),
case_count: diffs.len(),
compared: ledger.counts().clone(),
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
cases: diffs.clone(),
};
emit_log(&log);
for d in &diffs {
if !d.pass {
eprintln!("shortest_path mismatch: {} — {}", d.case_id, d.note);
}
}
assert!(
all_pass,
"shortest_path coverage failed: {} cases",
diffs.len()
);
ledger.finish(1);
}