chase-rs/tests/integration_tests.rs
2026-03-09 11:12:18 +01:00

185 lines
5.6 KiB
Rust

//! Integration tests for the chase algorithm.
use chase_rs::chase::rule::RuleBuilder;
use chase_rs::{chase, Atom, Instance, Term};
#[test]
fn test_transitive_closure() {
// Build a chain: a -> b -> c -> d
let instance: Instance = vec![
Atom::new("Edge", vec![Term::constant("a"), Term::constant("b")]),
Atom::new("Edge", vec![Term::constant("b"), Term::constant("c")]),
Atom::new("Edge", vec![Term::constant("c"), Term::constant("d")]),
]
.into_iter()
.collect();
// Edge(X, Y) -> Path(X, Y)
let rule1 = RuleBuilder::new()
.when("Edge", vec![Term::var("X"), Term::var("Y")])
.then("Path", vec![Term::var("X"), Term::var("Y")])
.build();
// Path(X, Y), Edge(Y, Z) -> Path(X, Z)
let rule2 = RuleBuilder::new()
.when("Path", vec![Term::var("X"), Term::var("Y")])
.when("Edge", vec![Term::var("Y"), Term::var("Z")])
.then("Path", vec![Term::var("X"), Term::var("Z")])
.build();
let result = chase(instance, &[rule1, rule2]);
assert!(result.terminated);
// Should have 6 paths: a->b, b->c, c->d, a->c, b->d, a->d
let paths = result.instance.facts_for_predicate("Path");
assert_eq!(paths.len(), 6);
}
#[test]
fn test_existential_rule_generates_nulls() {
// Every employee must have a department
let instance: Instance = vec![
Atom::new("Employee", vec![Term::constant("alice")]),
Atom::new("Employee", vec![Term::constant("bob")]),
Atom::new("Employee", vec![Term::constant("carol")]),
]
.into_iter()
.collect();
// Employee(X) -> WorksIn(X, Y) where Y is existential
let rule = RuleBuilder::new()
.when("Employee", vec![Term::var("X")])
.then("WorksIn", vec![Term::var("X"), Term::var("Dept")])
.build();
let result = chase(instance, &[rule]);
assert!(result.terminated);
let works_in = result.instance.facts_for_predicate("WorksIn");
assert_eq!(works_in.len(), 3);
// Each should have a unique null
let nulls: Vec<_> = works_in
.iter()
.filter_map(|f| match &f.terms[1] {
Term::Null(id) => Some(*id),
_ => None,
})
.collect();
assert_eq!(nulls.len(), 3);
// All nulls should be unique
let mut unique_nulls = nulls.clone();
unique_nulls.sort();
unique_nulls.dedup();
assert_eq!(unique_nulls.len(), 3);
}
#[test]
fn test_multiple_head_atoms() {
let instance: Instance = vec![Atom::new("Person", vec![Term::constant("alice")])]
.into_iter()
.collect();
// Person(X) -> HasName(X, N), HasAge(X, A)
let rule = RuleBuilder::new()
.when("Person", vec![Term::var("X")])
.then("HasName", vec![Term::var("X"), Term::var("N")])
.then("HasAge", vec![Term::var("X"), Term::var("A")])
.build();
let result = chase(instance, &[rule]);
assert!(result.terminated);
assert_eq!(result.instance.facts_for_predicate("HasName").len(), 1);
assert_eq!(result.instance.facts_for_predicate("HasAge").len(), 1);
}
#[test]
fn test_chase_with_constants_in_rules() {
let instance: Instance = vec![
Atom::new("Status", vec![Term::constant("alice"), Term::constant("active")]),
Atom::new("Status", vec![Term::constant("bob"), Term::constant("inactive")]),
]
.into_iter()
.collect();
// Only active users get access: Status(X, "active") -> HasAccess(X)
let rule = RuleBuilder::new()
.when(
"Status",
vec![Term::var("X"), Term::constant("active")],
)
.then("HasAccess", vec![Term::var("X")])
.build();
let result = chase(instance, &[rule]);
assert!(result.terminated);
let access = result.instance.facts_for_predicate("HasAccess");
assert_eq!(access.len(), 1);
// Only alice should have access
let fact = access[0];
assert_eq!(fact.terms[0], Term::constant("alice"));
}
#[test]
fn test_chase_reaches_fixpoint() {
// Test that applying the same rule multiple times doesn't create duplicates
let instance: Instance = vec![Atom::new("Fact", vec![Term::constant("x")])]
.into_iter()
.collect();
// Fact(X) -> Derived(X)
let rule = RuleBuilder::new()
.when("Fact", vec![Term::var("X")])
.then("Derived", vec![Term::var("X")])
.build();
let result = chase(instance, &[rule]);
assert!(result.terminated);
assert_eq!(result.instance.facts_for_predicate("Derived").len(), 1);
assert_eq!(result.steps, 1); // Should complete in one step
}
#[test]
fn test_self_join_rule() {
// Find pairs of people with the same manager
let instance: Instance = vec![
Atom::new(
"ManagedBy",
vec![Term::constant("alice"), Term::constant("eve")],
),
Atom::new(
"ManagedBy",
vec![Term::constant("bob"), Term::constant("eve")],
),
Atom::new(
"ManagedBy",
vec![Term::constant("carol"), Term::constant("frank")],
),
]
.into_iter()
.collect();
// ManagedBy(X, M), ManagedBy(Y, M) -> SameTeam(X, Y)
let rule = RuleBuilder::new()
.when("ManagedBy", vec![Term::var("X"), Term::var("M")])
.when("ManagedBy", vec![Term::var("Y"), Term::var("M")])
.then("SameTeam", vec![Term::var("X"), Term::var("Y")])
.build();
let result = chase(instance, &[rule]);
assert!(result.terminated);
// Should have: (alice, alice), (alice, bob), (bob, alice), (bob, bob), (carol, carol)
let same_team = result.instance.facts_for_predicate("SameTeam");
assert_eq!(same_team.len(), 5);
}