use std::collections::HashMap;
use issundb::{DegreeDirection, EdgeId, Graph, NodeId};
use serde::Deserialize;
use serde_json::json;
use tempfile::TempDir;
#[derive(Deserialize)]
struct Corpus {
cases: Vec<Case>,
}
#[derive(Deserialize)]
struct Case {
id: String,
n: usize,
edges: Vec<[usize; 2]>,
capacities: Vec<u32>,
wcc: Vec<Vec<usize>>,
scc: Vec<Vec<usize>>,
sp_len: Vec<[i64; 3]>,
maxflow: Vec<[i64; 3]>,
is_dag: bool,
degree: Degrees,
dijkstra: Vec<(usize, usize, f64)>,
all_sp: Vec<(usize, usize, Vec<Vec<usize>>)>,
bfs: Vec<(usize, u8, Vec<usize>)>,
top_k: Vec<(usize, usize, Vec<f64>)>,
spanning: Spanning,
}
#[derive(Deserialize)]
struct Spanning {
min: SpanningForest,
max: SpanningForest,
}
#[derive(Deserialize)]
struct SpanningForest {
weight: f64,
edges: usize,
}
#[derive(Deserialize)]
struct Degrees {
#[serde(rename = "in")]
incoming: Vec<u64>,
out: Vec<u64>,
both: Vec<u64>,
}
fn load_corpus() -> Corpus {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/networkx_oracle.json"
);
let text = std::fs::read_to_string(path).expect("read oracle fixture corpus");
serde_json::from_str(&text).expect("parse oracle fixture corpus")
}
fn build(case: &Case) -> (TempDir, Graph, Vec<NodeId>) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let ids: Vec<NodeId> = (0..case.n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
for (edge, &cap) in case.edges.iter().zip(&case.capacities) {
g.add_edge(ids[edge[0]], ids[edge[1]], "E", &json!({ "capacity": cap }))
.unwrap();
}
g.rebuild_csr().unwrap();
(dir, g, ids)
}
fn canonical_partition(labels: &HashMap<NodeId, u64>, ids: &[NodeId]) -> Vec<Vec<usize>> {
let index_of: HashMap<NodeId, usize> = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut groups: HashMap<u64, Vec<usize>> = HashMap::new();
for (&id, &label) in labels {
groups.entry(label).or_default().push(index_of[&id]);
}
let mut parts: Vec<Vec<usize>> = groups
.into_values()
.map(|mut g| {
g.sort_unstable();
g
})
.collect();
parts.sort();
parts
}
#[test]
fn connected_components_match_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
let got = canonical_partition(&g.connected_components().unwrap(), &ids);
assert_eq!(
got, case.wcc,
"case {}: weakly connected components mismatch",
case.id
);
}
}
#[test]
fn strongly_connected_components_match_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
let got = canonical_partition(&g.strongly_connected_components().unwrap(), &ids);
assert_eq!(
got, case.scc,
"case {}: strongly connected components mismatch",
case.id
);
}
}
#[test]
fn shortest_path_lengths_match_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for &[s, t, expected] in &case.sp_len {
let path = g.shortest_path(ids[s as usize], ids[t as usize]).unwrap();
let got: i64 = match path {
Some(p) => (p.len() as i64) - 1,
None => -1,
};
assert_eq!(
got, expected,
"case {}: shortest_path({s}, {t}) length mismatch",
case.id
);
}
}
}
#[test]
fn maximum_flow_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for &[s, t, expected] in &case.maxflow {
let got = g
.maximum_flow(ids[s as usize], ids[t as usize], "capacity")
.unwrap();
assert!(
(got - expected as f64).abs() < 1e-6,
"case {}: maximum_flow({s}, {t}) = {got}, expected {expected}",
case.id
);
}
}
}
#[test]
fn detect_cycle_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, _ids) = build(case);
let has_cycle = g.detect_cycle().unwrap();
assert_eq!(
has_cycle, !case.is_dag,
"case {}: detect_cycle = {has_cycle}, is_dag = {}",
case.id, case.is_dag
);
}
}
#[test]
fn degree_centrality_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
let cases = [
(DegreeDirection::In, &case.degree.incoming, "in"),
(DegreeDirection::Out, &case.degree.out, "out"),
(DegreeDirection::Both, &case.degree.both, "both"),
];
for (dir, expected, name) in cases {
let got = g.degree_centrality(dir).unwrap();
for (i, &id) in ids.iter().enumerate() {
assert_eq!(
got[&id], expected[i],
"case {}: {name}-degree[{i}] = {}, expected {}",
case.id, got[&id], expected[i]
);
}
}
}
}
#[test]
fn shortest_path_dijkstra_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for &(s, t, expected) in &case.dijkstra {
let path = g.shortest_path_dijkstra(ids[s], ids[t]).unwrap();
match path {
Some(p) => assert!(
expected >= 0.0 && (p.total_weight - expected).abs() < 1e-6,
"case {}: dijkstra({s}, {t}) = {}, expected {expected}",
case.id,
p.total_weight
),
None => assert!(
expected < 0.0,
"case {}: dijkstra({s}, {t}) = unreachable, expected {expected}",
case.id
),
}
}
}
}
#[test]
fn all_shortest_paths_match_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
let index_of: HashMap<NodeId, usize> =
ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
for (s, t, expected) in &case.all_sp {
let got = g.all_shortest_paths(ids[*s], ids[*t]).unwrap();
let mut got_idx: Vec<Vec<usize>> = got
.into_iter()
.map(|p| p.into_iter().map(|id| index_of[&id]).collect())
.collect();
got_idx.sort();
assert_eq!(
&got_idx, expected,
"case {}: all_shortest_paths({s}, {t}) mismatch",
case.id
);
}
}
}
fn sorted_indices(nodes: Vec<NodeId>, ids: &[NodeId]) -> Vec<usize> {
let index_of: HashMap<NodeId, usize> = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut v: Vec<usize> = nodes.into_iter().map(|id| index_of[&id]).collect();
v.sort_unstable();
v
}
#[test]
fn bfs_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for (start, hops, expected) in &case.bfs {
let got = sorted_indices(g.bfs(ids[*start], *hops).unwrap(), &ids);
assert_eq!(
&got, expected,
"case {}: bfs({start}, {hops}) reachable-set mismatch",
case.id
);
}
}
}
#[test]
fn dfs_matches_networkx() {
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for (start, hops, expected) in &case.bfs {
let got = sorted_indices(g.dfs(ids[*start], *hops).unwrap(), &ids);
assert_eq!(
&got, expected,
"case {}: dfs({start}, {hops}) reachable-set mismatch",
case.id
);
}
}
}
#[test]
fn shortest_path_top_k_matches_networkx() {
const K: usize = 3;
const TOL: f64 = 1e-6;
let corpus = load_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build(case);
for (s, t, expected) in &case.top_k {
let paths = g
.shortest_path_top_k(ids[*s], ids[*t], K, "capacity")
.unwrap();
let mut got: Vec<f64> = paths.iter().map(|p| p.total_weight).collect();
got.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(
got.len(),
expected.len(),
"case {}: top_k({s}, {t}) returned {} paths, expected {}",
case.id,
got.len(),
expected.len()
);
for (g_w, e_w) in got.iter().zip(expected) {
assert!(
(g_w - e_w).abs() < TOL,
"case {}: top_k({s}, {t}) weight {g_w}, expected {e_w}",
case.id
);
}
}
}
}
#[derive(Deserialize)]
struct PageRankCorpus {
meta: PageRankMeta,
cases: Vec<PageRankCase>,
}
#[derive(Deserialize)]
struct PageRankMeta {
alpha: f32,
}
#[derive(Deserialize)]
struct PageRankCase {
id: String,
n: usize,
edges: Vec<[usize; 2]>,
pagerank: Vec<f64>,
}
#[test]
fn pagerank_matches_networkx() {
const ITERATIONS: u32 = 200;
const TOL: f64 = 1e-4;
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/networkx_pagerank.json"
);
let text = std::fs::read_to_string(path).expect("read pagerank fixture corpus");
let corpus: PageRankCorpus =
serde_json::from_str(&text).expect("parse pagerank fixture corpus");
for case in &corpus.cases {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let ids: Vec<NodeId> = (0..case.n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
for edge in &case.edges {
g.add_edge(ids[edge[0]], ids[edge[1]], "E", &json!({}))
.unwrap();
}
g.rebuild_csr().unwrap();
let ranks = g.page_rank(ITERATIONS, corpus.meta.alpha).unwrap();
for (i, &id) in ids.iter().enumerate() {
let got = ranks[&id] as f64;
let expected = case.pagerank[i];
assert!(
(got - expected).abs() < TOL,
"case {}: pagerank[{i}] = {got}, expected {expected}",
case.id
);
}
}
}
#[derive(Deserialize)]
struct CentralityCorpus {
cases: Vec<CentralityCase>,
}
#[derive(Deserialize)]
struct CentralityCase {
id: String,
n: usize,
edges: Vec<[usize; 2]>,
betweenness: Vec<f64>,
harmonic: Vec<f64>,
}
fn load_centrality_corpus() -> CentralityCorpus {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/networkx_centrality.json"
);
let text = std::fs::read_to_string(path).expect("read centrality fixture corpus");
serde_json::from_str(&text).expect("parse centrality fixture corpus")
}
fn build_unweighted(case: &CentralityCase) -> (TempDir, Graph, Vec<NodeId>) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let ids: Vec<NodeId> = (0..case.n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
for edge in &case.edges {
g.add_edge(ids[edge[0]], ids[edge[1]], "E", &json!({}))
.unwrap();
}
g.rebuild_csr().unwrap();
(dir, g, ids)
}
#[test]
fn betweenness_centrality_matches_networkx() {
const TOL: f64 = 1e-6;
let corpus = load_centrality_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build_unweighted(case);
let got = g.betweenness_centrality().unwrap();
for (i, &id) in ids.iter().enumerate() {
let expected = case.betweenness[i];
assert!(
(got[&id] - expected).abs() < TOL,
"case {}: betweenness[{i}] = {}, expected {expected}",
case.id,
got[&id]
);
}
}
}
#[test]
fn harmonic_centrality_matches_networkx() {
const TOL: f64 = 1e-6;
let corpus = load_centrality_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build_unweighted(case);
let got = g.harmonic_centrality().unwrap();
for (i, &id) in ids.iter().enumerate() {
let expected = case.harmonic[i];
assert!(
(got[&id] - expected).abs() < TOL,
"case {}: harmonic[{i}] = {}, expected {expected}",
case.id,
got[&id]
);
}
}
}
#[derive(Deserialize)]
struct PathsCorpus {
cases: Vec<PathsCase>,
}
#[derive(Deserialize)]
struct PathsCase {
id: String,
n: usize,
edges: Vec<[usize; 2]>,
all_paths: Vec<(usize, usize, Vec<Vec<usize>>)>,
longest: Vec<(usize, usize, usize)>,
}
fn load_paths_corpus() -> PathsCorpus {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/networkx_paths.json"
);
let text = std::fs::read_to_string(path).expect("read paths fixture corpus");
serde_json::from_str(&text).expect("parse paths fixture corpus")
}
fn build_paths(case: &PathsCase) -> (TempDir, Graph, Vec<NodeId>) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let ids: Vec<NodeId> = (0..case.n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
for edge in &case.edges {
g.add_edge(ids[edge[0]], ids[edge[1]], "E", &json!({}))
.unwrap();
}
g.rebuild_csr().unwrap();
(dir, g, ids)
}
#[test]
fn all_paths_match_networkx() {
let corpus = load_paths_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build_paths(case);
let index_of: HashMap<NodeId, usize> =
ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
for (s, t, expected) in &case.all_paths {
let got = g.all_paths(ids[*s], ids[*t]).unwrap();
let mut got_idx: Vec<Vec<usize>> = got
.into_iter()
.map(|p| p.into_iter().map(|id| index_of[&id]).collect())
.collect();
got_idx.sort();
assert_eq!(
&got_idx, expected,
"case {}: all_paths({s}, {t}) mismatch",
case.id
);
}
}
}
#[test]
fn spanning_forest_matches_networkx() {
const TOL: f64 = 1e-9;
let corpus = load_corpus();
for case in &corpus.cases {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let ids: Vec<NodeId> = (0..case.n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
let mut weight_of: HashMap<EdgeId, f64> = HashMap::new();
for (edge, &cap) in case.edges.iter().zip(&case.capacities) {
let eid = g
.add_edge(ids[edge[0]], ids[edge[1]], "E", &json!({ "capacity": cap }))
.unwrap();
weight_of.insert(eid, cap as f64);
}
g.rebuild_csr().unwrap();
for (maximum, expected) in [(false, &case.spanning.min), (true, &case.spanning.max)] {
let kind = if maximum { "max" } else { "min" };
let forest = g.spanning_forest("capacity", maximum).unwrap();
assert_eq!(
forest.len(),
expected.edges,
"case {}: {kind} spanning forest edge count = {}, expected {}",
case.id,
forest.len(),
expected.edges
);
let total: f64 = forest.iter().map(|eid| weight_of[eid]).sum();
assert!(
(total - expected.weight).abs() < TOL,
"case {}: {kind} spanning forest weight = {total}, expected {}",
case.id,
expected.weight
);
}
}
}
#[test]
fn longest_path_matches_networkx() {
let corpus = load_paths_corpus();
for case in &corpus.cases {
let (_dir, g, ids) = build_paths(case);
for (s, t, expected_len) in &case.longest {
let got = g.longest_path(ids[*s], ids[*t]).unwrap();
let got_len = got.map(|p| p.len());
assert_eq!(
got_len,
Some(*expected_len),
"case {}: longest_path({s}, {t}) node count mismatch",
case.id
);
}
}
}