use fabula::prelude::*;
use fabula_petgraph::{PetTemporalGraph, PetValue};
type Graph = PetTemporalGraph<String, String, PetValue<String>, i64>;
fn str_val(s: &str) -> PetValue<String> {
PetValue::Str(s.to_string())
}
fn node_val(s: &str) -> PetValue<String> {
PetValue::Node(s.to_string())
}
fn hospitality_graph() -> Graph {
let mut g = Graph::new(0);
g.add_node("ev1".into());
g.add_edge(
"ev1".into(),
"eventType".into(),
str_val("enterTown"),
Interval::open(1),
);
g.add_edge(
"ev1".into(),
"actor".into(),
node_val("alice"),
Interval::open(1),
);
g.add_node("ev2".into());
g.add_edge(
"ev2".into(),
"eventType".into(),
str_val("showHospitality"),
Interval::open(2),
);
g.add_edge(
"ev2".into(),
"actor".into(),
node_val("bob"),
Interval::open(2),
);
g.add_edge(
"ev2".into(),
"target".into(),
node_val("alice"),
Interval::open(2),
);
g.add_node("ev3".into());
g.add_edge(
"ev3".into(),
"eventType".into(),
str_val("harm"),
Interval::open(3),
);
g.add_edge(
"ev3".into(),
"actor".into(),
node_val("bob"),
Interval::open(3),
);
g.add_edge(
"ev3".into(),
"target".into(),
node_val("alice"),
Interval::open(3),
);
g.add_node("alice".into());
g.add_node("bob".into());
g.set_time(10);
g
}
fn violation_of_hospitality() -> Pattern<String, PetValue<String>> {
PatternBuilder::new("violation_of_hospitality")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), str_val("enterTown"))
.edge_bind("e1", "actor".into(), "guest")
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), str_val("showHospitality"))
.edge_bind("e2", "actor".into(), "host")
.edge_bind("e2", "target".into(), "guest")
})
.stage("e3", |s| {
s.edge("e3", "eventType".into(), str_val("harm"))
.edge_bind("e3", "actor".into(), "host")
.edge_bind("e3", "target".into(), "guest")
})
.unless_between("e1", "e3", |neg| {
neg.edge("eMid", "eventType".into(), str_val("leaveTown"))
.edge_bind("eMid", "actor".into(), "guest")
})
.build()
}
#[test]
fn petgraph_batch_hospitality() {
let g = hospitality_graph();
let mut engine: SiftEngineFor<Graph> = SiftEngine::new();
engine.register(violation_of_hospitality());
let matches = engine.evaluate(&g);
assert_eq!(matches.len(), 1, "should find violation of hospitality");
match &matches[0].bindings["guest"] {
BoundValue::Node(n) => assert_eq!(n, "alice"),
other => panic!("expected guest=alice, got {:?}", other),
}
match &matches[0].bindings["host"] {
BoundValue::Node(n) => assert_eq!(n, "bob"),
other => panic!("expected host=bob, got {:?}", other),
}
}
#[test]
fn petgraph_batch_negation_blocks() {
let mut g = hospitality_graph();
g.add_edge(
"ev_leave".into(),
"eventType".into(),
str_val("leaveTown"),
Interval::open(2),
);
g.add_edge(
"ev_leave".into(),
"actor".into(),
node_val("alice"),
Interval::open(2),
);
let mut engine: SiftEngineFor<Graph> = SiftEngine::new();
engine.register(violation_of_hospitality());
assert_eq!(engine.evaluate(&g).len(), 0, "guest left — negation blocks");
}
#[test]
fn petgraph_incremental_three_stages() {
let mut g = Graph::new(0);
let mut engine: SiftEngineFor<Graph> = SiftEngine::new();
engine.register(violation_of_hospitality());
g.add_node("alice".into());
g.add_node("ev1".into());
g.add_edge(
"ev1".into(),
"eventType".into(),
str_val("enterTown"),
Interval::open(1),
);
g.add_edge(
"ev1".into(),
"actor".into(),
node_val("alice"),
Interval::open(1),
);
g.set_time(1);
let ev = engine.on_edge_added(
&g,
&"ev1".into(),
&"eventType".into(),
&str_val("enterTown"),
&Interval::open(1),
);
assert!(ev.iter().any(|e| matches!(e, SiftEvent::Advanced { .. })));
g.add_node("bob".into());
g.add_node("ev2".into());
g.add_edge(
"ev2".into(),
"eventType".into(),
str_val("showHospitality"),
Interval::open(2),
);
g.add_edge(
"ev2".into(),
"actor".into(),
node_val("bob"),
Interval::open(2),
);
g.add_edge(
"ev2".into(),
"target".into(),
node_val("alice"),
Interval::open(2),
);
g.set_time(2);
let ev = engine.on_edge_added(
&g,
&"ev2".into(),
&"eventType".into(),
&str_val("showHospitality"),
&Interval::open(2),
);
assert!(ev.iter().any(|e| matches!(e, SiftEvent::Advanced { .. })));
g.add_node("ev3".into());
g.add_edge(
"ev3".into(),
"eventType".into(),
str_val("harm"),
Interval::open(3),
);
g.add_edge(
"ev3".into(),
"actor".into(),
node_val("bob"),
Interval::open(3),
);
g.add_edge(
"ev3".into(),
"target".into(),
node_val("alice"),
Interval::open(3),
);
g.set_time(3);
let ev = engine.on_edge_added(
&g,
&"ev3".into(),
&"eventType".into(),
&str_val("harm"),
&Interval::open(3),
);
assert!(ev.iter().any(|e| matches!(e, SiftEvent::Completed { .. })));
}
#[test]
fn petgraph_single_stage_immediate() {
let mut g = Graph::new(0);
let mut engine: SiftEngineFor<Graph> = SiftEngine::new();
let pattern = PatternBuilder::new("find_harm")
.stage("e", |s| {
s.edge("e", "eventType".into(), str_val("harm")).edge_bind(
"e",
"actor".into(),
"attacker",
)
})
.build();
engine.register(pattern);
g.add_node("ev1".into());
g.add_node("bob".into());
g.add_edge(
"ev1".into(),
"eventType".into(),
str_val("harm"),
Interval::open(1),
);
g.add_edge(
"ev1".into(),
"actor".into(),
node_val("bob"),
Interval::open(1),
);
g.set_time(1);
let ev = engine.on_edge_added(
&g,
&"ev1".into(),
&"eventType".into(),
&str_val("harm"),
&Interval::open(1),
);
assert!(ev.iter().any(|e| matches!(e, SiftEvent::Completed { .. })));
}
#[test]
fn petgraph_why_not_on_empty() {
let g = Graph::new(0);
let mut engine: SiftEngineFor<Graph> = SiftEngine::new();
engine.register(violation_of_hospitality());
let analysis = engine.why_not(&g, "violation_of_hospitality").unwrap();
assert!(!analysis.stages.is_empty());
match analysis.stages[0].status {
StageStatus::Unmatched => {}
ref other => panic!("expected Unmatched, got {:?}", other),
}
}