use std::collections::HashMap;
use issundb::{DegreeDirection, Graph, GraphQueryExt, NodeId};
use serde_json::json;
use tempfile::TempDir;
fn load_modern() -> (TempDir, Graph, HashMap<&'static str, NodeId>) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
let mut ids = HashMap::new();
for (name, age) in [("marko", 29), ("vadas", 27), ("josh", 32), ("peter", 35)] {
let id = g
.add_node("person", &json!({ "name": name, "age": age }))
.unwrap();
ids.insert(name, id);
}
for name in ["lop", "ripple"] {
let id = g
.add_node("software", &json!({ "name": name, "lang": "java" }))
.unwrap();
ids.insert(name, id);
}
for (src, dst, etype, weight) in [
("marko", "vadas", "knows", 0.5),
("marko", "josh", "knows", 1.0),
("marko", "lop", "created", 0.4),
("josh", "ripple", "created", 1.0),
("josh", "lop", "created", 0.4),
("peter", "lop", "created", 0.2),
] {
g.add_edge(ids[src], ids[dst], etype, &json!({ "weight": weight }))
.unwrap();
}
(dir, g, ids)
}
fn node_name(g: &Graph, id: NodeId) -> String {
let record = g.get_node(id).unwrap().unwrap();
let props: serde_json::Value = rmp_serde::from_slice(&record.props).unwrap();
props["name"].as_str().unwrap().to_string()
}
fn sorted_names(g: &Graph, ids: impl IntoIterator<Item = NodeId>) -> Vec<String> {
let mut names: Vec<String> = ids.into_iter().map(|id| node_name(g, id)).collect();
names.sort();
names
}
#[test]
fn modern_node_and_edge_counts() {
let (_dir, g, _ids) = load_modern();
assert_eq!(g.all_nodes().unwrap().len(), 6);
assert_eq!(g.node_count_by_label("person").unwrap(), 4);
assert_eq!(g.node_count_by_label("software").unwrap(), 2);
assert_eq!(g.edge_count_by_type("knows").unwrap(), 2);
assert_eq!(g.edge_count_by_type("created").unwrap(), 4);
}
#[test]
fn marko_knows_vadas_and_josh() {
let (_dir, g, ids) = load_modern();
let known: Vec<NodeId> = g
.out_neighbors(ids["marko"])
.unwrap()
.into_iter()
.filter(|n| g.type_name(n.edge_type).unwrap().as_deref() == Some("knows"))
.map(|n| n.node)
.collect();
assert_eq!(sorted_names(&g, known), ["josh", "vadas"]);
}
#[test]
fn lop_created_by_marko_josh_and_peter() {
let (_dir, g, ids) = load_modern();
let creators: Vec<NodeId> = g
.in_neighbors(ids["lop"])
.unwrap()
.into_iter()
.map(|n| n.node)
.collect();
assert_eq!(sorted_names(&g, creators), ["josh", "marko", "peter"]);
}
#[test]
fn out_degrees_match_documented_topology() {
let (_dir, g, ids) = load_modern();
let out = g.degree_centrality(DegreeDirection::Out).unwrap();
assert_eq!(out[&ids["marko"]], 3);
assert_eq!(out[&ids["josh"]], 2);
assert_eq!(out[&ids["peter"]], 1);
assert_eq!(out[&ids["vadas"]], 0);
assert_eq!(out[&ids["lop"]], 0);
assert_eq!(out[&ids["ripple"]], 0);
}
#[test]
fn modern_is_one_weakly_connected_acyclic_component() {
let (_dir, g, _ids) = load_modern();
let components = g.connected_components().unwrap();
let first = *components.values().next().unwrap();
assert_eq!(components.len(), 6);
assert!(components.values().all(|&c| c == first));
assert!(!g.detect_cycle().unwrap());
}
#[test]
fn shortest_path_marko_to_ripple_goes_through_josh() {
let (_dir, g, ids) = load_modern();
let path = g
.shortest_path(ids["marko"], ids["ripple"])
.unwrap()
.expect("ripple is reachable from marko");
assert_eq!(path, vec![ids["marko"], ids["josh"], ids["ripple"]]);
}
#[test]
fn weighted_shortest_path_marko_to_ripple_costs_two() {
let (_dir, g, ids) = load_modern();
let path = g
.shortest_path_dijkstra(ids["marko"], ids["ripple"])
.unwrap()
.expect("ripple is reachable from marko");
assert_eq!(path.nodes, vec![ids["marko"], ids["josh"], ids["ripple"]]);
assert!((path.total_weight - 2.0).abs() < 1e-9);
}
#[test]
fn two_hop_frontier_from_marko_is_lop_and_ripple() {
let (_dir, g, _ids) = load_modern();
let result = g
.query("MATCH (a)-->(b)-->(c) RETURN c.name AS name ORDER BY name")
.unwrap();
let names: Vec<&str> = result
.records
.iter()
.map(|r| r.values[0].as_str().unwrap())
.collect();
assert_eq!(names, ["lop", "ripple"]);
}
#[test]
fn cypher_marko_knows_over_thirty_is_josh() {
let (_dir, g, _ids) = load_modern();
let result = g
.query(
"MATCH (m:person {name: 'marko'})-[:knows]->(p) \
WHERE p.age > 30 RETURN p.name AS name",
)
.unwrap();
assert_eq!(result.records.len(), 1);
assert_eq!(result.records[0].values[0], json!("josh"));
}
#[test]
fn cypher_co_developers_of_marko_are_josh_and_peter() {
let (_dir, g, _ids) = load_modern();
let result = g
.query(
"MATCH (m:person {name: 'marko'})-[:created]->(s)<-[:created]-(c) \
RETURN c.name AS name ORDER BY name",
)
.unwrap();
let names: Vec<&str> = result
.records
.iter()
.map(|r| r.values[0].as_str().unwrap())
.collect();
assert_eq!(names, ["josh", "peter"]);
}
#[test]
fn cypher_mean_person_age_is_thirty_point_seven_five() {
let (_dir, g, _ids) = load_modern();
let result = g
.query("MATCH (p:person) RETURN avg(p.age) AS mean_age")
.unwrap();
assert_eq!(result.records.len(), 1);
let mean = result.records[0].values[0].as_f64().unwrap();
assert!((mean - 30.75).abs() < 1e-9);
}
#[test]
fn cypher_software_creators_grouped_by_project() {
let (_dir, g, _ids) = load_modern();
let result = g
.query(
"MATCH (p:person)-[:created]->(s:software) \
RETURN s.name AS software, count(p) AS creators ORDER BY software",
)
.unwrap();
let rows: Vec<(String, i64)> = result
.records
.iter()
.map(|r| {
(
r.values[0].as_str().unwrap().to_string(),
r.values[1].as_i64().unwrap(),
)
})
.collect();
assert_eq!(rows, [("lop".into(), 3), ("ripple".into(), 1)]);
}
#[test]
fn cypher_knows_edge_weights_match_fixture() {
let (_dir, g, _ids) = load_modern();
let result = g
.query(
"MATCH (:person {name: 'marko'})-[k:knows]->(p) \
RETURN p.name AS name, k.weight AS weight ORDER BY weight",
)
.unwrap();
let rows: Vec<(String, f64)> = result
.records
.iter()
.map(|r| {
(
r.values[0].as_str().unwrap().to_string(),
r.values[1].as_f64().unwrap(),
)
})
.collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].0, "vadas");
assert!((rows[0].1 - 0.5).abs() < 1e-9);
assert_eq!(rows[1].0, "josh");
assert!((rows[1].1 - 1.0).abs() < 1e-9);
}