use std::collections::{HashMap, HashSet};
use pattern_core::{
canonical_classifier, filter_graph, fold_graph, from_pattern_graph, from_patterns,
map_all_graph, map_graph, map_with_context, materialize, para_graph, para_graph_fixed, unfold,
unfold_graph, CategoryMappers, GraphClass, GraphClassifier, Pattern, ReconciliationPolicy,
Subject, SubjectMergeStrategy, Substitution, Symbol, Value,
};
fn classifier() -> GraphClassifier<(), Subject> {
canonical_classifier::<Subject>()
}
fn lww() -> ReconciliationPolicy<SubjectMergeStrategy> {
ReconciliationPolicy::LastWriteWins
}
fn subj(id: &str) -> Subject {
Subject {
identity: Symbol(id.to_string()),
labels: HashSet::new(),
properties: HashMap::new(),
}
}
fn subj_with_prop(id: &str, key: &str, val: &str) -> Subject {
let mut props = HashMap::new();
props.insert(key.to_string(), Value::VString(val.to_string()));
Subject {
identity: Symbol(id.to_string()),
labels: HashSet::new(),
properties: props,
}
}
fn node(id: &str) -> Pattern<Subject> {
Pattern::point(subj(id))
}
fn rel(id: &str, src: &str, tgt: &str) -> Pattern<Subject> {
Pattern {
value: subj(id),
elements: vec![node(src), node(tgt)],
}
}
fn annot(id: &str, target_id: &str) -> Pattern<Subject> {
Pattern {
value: subj(id),
elements: vec![node(target_id)],
}
}
#[test]
fn unfold_leaf_seed() {
let tree = unfold(|n: u32| (n, vec![]), 42u32);
assert_eq!(tree.value, 42);
assert!(tree.elements.is_empty());
}
#[test]
fn unfold_depth_2_binary_tree() {
let tree = unfold(
|depth: u32| {
if depth == 0 {
(depth, vec![])
} else {
(depth, vec![depth - 1, depth - 1])
}
},
2u32,
);
assert_eq!(tree.value, 2);
assert_eq!(tree.elements.len(), 2);
assert_eq!(tree.elements[0].value, 1);
assert_eq!(tree.elements[0].elements.len(), 2);
assert_eq!(tree.elements[0].elements[0].value, 0);
}
#[test]
fn unfold_linear_chain() {
let tree = unfold(
|n: u32| {
if n == 0 {
(n, vec![])
} else {
(n, vec![n - 1])
}
},
3u32,
);
assert_eq!(tree.value, 3);
assert_eq!(tree.elements.len(), 1);
assert_eq!(tree.elements[0].value, 2);
assert_eq!(tree.elements[0].elements[0].value, 1);
}
#[test]
fn unfold_graph_from_seeds() {
let classifier = classifier();
let policy = lww();
let seeds = vec!["a", "b", "c"];
let graph = unfold_graph(&classifier, &policy, |id: &str| vec![node(id)], seeds);
assert_eq!(graph.pg_nodes.len(), 3);
}
#[test]
fn unfold_graph_empty_seeds() {
let classifier = classifier();
let policy = lww();
let graph = unfold_graph(&classifier, &policy, |_: &str| vec![], vec!["x"]);
assert_eq!(graph.pg_nodes.len(), 0);
}
#[test]
fn map_graph_transforms_only_nodes() {
let classifier = classifier();
let policy = lww();
let graph = from_patterns(&classifier, vec![rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let mappers = CategoryMappers {
nodes: Box::new(|mut p: Pattern<Subject>| {
p.value
.properties
.insert("transformed".to_string(), Value::VString("yes".to_string()));
p
}),
..CategoryMappers::identity()
};
let view = map_graph(&classifier, mappers, view);
for (cls, pat) in &view.view_elements {
match cls {
GraphClass::GNode => {
assert!(
pat.value.properties.contains_key("transformed"),
"node should have 'transformed' property"
);
}
GraphClass::GRelationship => {
assert!(
!pat.value.properties.contains_key("transformed"),
"relationship should not have 'transformed' property"
);
}
_ => {}
}
}
}
#[test]
fn map_all_graph_transforms_all_elements() {
let classifier = classifier();
let policy = lww();
let graph = from_patterns(&classifier, vec![node("a"), node("b")]);
let view = from_pattern_graph(&classifier, &graph);
let view = map_all_graph(
|mut p: Pattern<Subject>| {
p.value
.properties
.insert("touched".to_string(), Value::VString("1".to_string()));
p
},
view,
);
let back = materialize(&classifier, &policy, view);
for n in back.pg_nodes.values() {
assert!(n.value.properties.contains_key("touched"));
}
}
#[test]
fn filter_graph_removes_non_matching() {
let classifier = classifier();
let policy = lww();
let graph = from_patterns(
&classifier,
vec![node("keep1"), node("keep2"), node("drop1")],
);
let view = from_pattern_graph(&classifier, &graph);
let view = filter_graph(
&classifier,
|_cls, p| p.value.identity.0.starts_with("keep"),
Substitution::NoSubstitution,
view,
);
let back = materialize(&classifier, &policy, view);
assert_eq!(back.pg_nodes.len(), 2);
assert!(back.pg_nodes.contains_key(&Symbol("keep1".to_string())));
assert!(back.pg_nodes.contains_key(&Symbol("keep2".to_string())));
assert!(!back.pg_nodes.contains_key(&Symbol("drop1".to_string())));
}
#[test]
fn fold_graph_count_by_class() {
let classifier = classifier();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let (node_count, rel_count) = fold_graph(
|(nc, rc), cls, _p| match cls {
GraphClass::GNode => (nc + 1, rc),
GraphClass::GRelationship => (nc, rc + 1),
_ => (nc, rc),
},
(0usize, 0usize),
&view,
);
assert_eq!(node_count, 2);
assert_eq!(rel_count, 1);
}
#[test]
fn map_with_context_uses_snapshot() {
let classifier = classifier();
let policy = lww();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let view = map_with_context(
&classifier,
|query, mut p| {
let degree = (query.query_degree)(&p);
p.value
.properties
.insert("degree".to_string(), Value::VString(degree.to_string()));
p
},
view,
);
for (cls, pat) in &view.view_elements {
if matches!(cls, GraphClass::GNode) {
assert!(
pat.value.properties.contains_key("degree"),
"node should have degree property"
);
}
}
}
#[test]
fn para_graph_nodes_have_no_sub_elements() {
let classifier = classifier();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let sub_counts = para_graph(
|_query, _element, sub_results: &[usize]| sub_results.len(),
&view,
);
assert_eq!(sub_counts[&Symbol("a".to_string())], 0);
assert_eq!(sub_counts[&Symbol("b".to_string())], 0);
assert_eq!(sub_counts[&Symbol("r1".to_string())], 2);
}
#[test]
fn para_graph_structural_depth() {
let classifier = classifier();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let depths = para_graph(
|_query, _element, sub_results: &[usize]| {
sub_results
.iter()
.cloned()
.max()
.map(|d| d + 1)
.unwrap_or(0)
},
&view,
);
assert_eq!(depths[&Symbol("a".to_string())], 0);
assert_eq!(depths[&Symbol("b".to_string())], 0);
assert_eq!(depths[&Symbol("r1".to_string())], 1);
}
#[test]
fn para_graph_processes_all_element_types() {
let classifier = classifier();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let results = para_graph(|_query, _element, _sub_results: &[usize]| 1usize, &view);
assert_eq!(results.len(), 3);
assert!(results.contains_key(&Symbol("a".to_string())));
assert!(results.contains_key(&Symbol("b".to_string())));
assert!(results.contains_key(&Symbol("r1".to_string())));
}
#[test]
fn para_graph_annotation_of_annotation_ordering() {
let classifier = classifier();
let graph = from_patterns(
&classifier,
vec![
node("n"),
annot("b", "n"), annot("a", "b"), ],
);
let view = from_pattern_graph(&classifier, &graph);
let sub_counts = para_graph(
|_query, _element, sub_results: &[usize]| sub_results.len(),
&view,
);
assert_eq!(sub_counts[&Symbol("n".to_string())], 0);
assert_eq!(sub_counts[&Symbol("b".to_string())], 1);
assert_eq!(sub_counts[&Symbol("a".to_string())], 1);
}
#[test]
fn para_graph_cycle_soft_miss() {
let other_classifier: GraphClassifier<(), Subject> =
GraphClassifier::new(|p: &Pattern<Subject>| {
if p.elements.is_empty() {
GraphClass::GNode
} else {
GraphClass::GOther(())
}
});
let pattern_a = Pattern {
value: subj("a"),
elements: vec![node("b")],
};
let pattern_b = Pattern {
value: subj("b"),
elements: vec![node("a")],
};
let graph = from_patterns(&other_classifier, vec![pattern_a, pattern_b]);
let view = from_pattern_graph(&other_classifier, &graph);
let sub_counts = para_graph(
|_query, _element, sub_results: &[usize]| sub_results.len(),
&view,
);
assert_eq!(sub_counts.len(), 2);
let a = sub_counts[&Symbol("a".to_string())];
let b = sub_counts[&Symbol("b".to_string())];
assert!(
a == 0 || b == 0,
"first cycle member must get subResults=[]"
);
assert_eq!(
a + b,
1,
"exactly one intra-cycle result visible in a 2-element cycle"
);
}
#[test]
fn para_graph_fixed_converges_on_simple_graph() {
let classifier = classifier();
let graph = from_patterns(&classifier, vec![node("a"), node("b"), rel("r1", "a", "b")]);
let view = from_pattern_graph(&classifier, &graph);
let result = para_graph_fixed(
|old: &usize, new: &usize| old == new,
|_query, _element, sub_results: &[usize]| {
sub_results
.iter()
.cloned()
.max()
.map(|d| d + 1)
.unwrap_or(0)
},
0usize,
&view,
);
assert!(result.contains_key(&Symbol("a".to_string())));
assert!(result.contains_key(&Symbol("b".to_string())));
assert!(result.contains_key(&Symbol("r1".to_string())));
assert_eq!(result[&Symbol("a".to_string())], 0);
assert_eq!(result[&Symbol("b".to_string())], 0);
assert_eq!(result[&Symbol("r1".to_string())], 1);
}
#[test]
fn para_graph_fixed_annotation_of_annotation_converges() {
let classifier = classifier();
let graph = from_patterns(
&classifier,
vec![node("n"), annot("b", "n"), annot("a", "b")],
);
let view = from_pattern_graph(&classifier, &graph);
let result = para_graph_fixed(
|old: &usize, new: &usize| old == new,
|_query, _element, sub_results: &[usize]| {
sub_results
.iter()
.cloned()
.max()
.map(|d| d + 1)
.unwrap_or(0)
},
0usize,
&view,
);
assert_eq!(result[&Symbol("n".to_string())], 0); assert_eq!(result[&Symbol("b".to_string())], 1); assert_eq!(result[&Symbol("a".to_string())], 2); }