use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display};
use std::hash::Hash;
mod err;
pub use err::*;
use itertools::Itertools;
pub trait TCNode<K> {
fn get_key(&self) -> K;
fn add_edge_to(&mut self, k: K);
fn out_edges(&self) -> Box<dyn Iterator<Item = &K> + '_>;
fn has_edge_to(&self, k: &K) -> bool;
fn reset_edges(&mut self);
fn direct_edges(&self) -> Box<dyn Iterator<Item = &K> + '_> {
self.out_edges()
}
}
pub fn compute_tc<K, V>(nodes: &mut HashMap<K, V>, enforce_dag: bool) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
if enforce_dag {
cyclic_tc(nodes);
return enforce_dag_from_tc(nodes);
}
let all_node_ids = nodes.keys().map(K::clone).collect::<Vec<K>>();
compute_tc_internal(all_node_ids.into_iter(), nodes, HashSet::new(), false);
Ok(())
}
pub fn repair_tc<K, V>(
nodes_to_fix: HashSet<K>,
nodes: &mut HashMap<K, V>,
enforce_dag: bool,
) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
let seen: HashSet<K> = nodes
.keys()
.filter(|x| !nodes_to_fix.contains(x))
.cloned()
.collect();
compute_tc_internal::<K, V>(nodes_to_fix.iter().cloned(), nodes, seen, enforce_dag);
if enforce_dag {
return enforce_dag_from_tc_for(&nodes_to_fix, nodes);
}
Ok(())
}
fn compute_tc_internal<K, V>(
node_ids: impl Iterator<Item = K>,
nodes: &mut HashMap<K, V>,
mut seen: HashSet<K>,
detect_cyles: bool,
) where
K: Clone + Eq + Hash,
V: TCNode<K>,
{
for node_id in node_ids {
if detect_cyles || seen.insert(node_id.clone()) {
add_ancestors(&node_id, nodes, &mut seen);
}
}
}
fn cyclic_tc<K, V>(nodes: &mut HashMap<K, V>)
where
K: Clone + Eq + Hash + Debug,
V: TCNode<K>,
{
let node_ids = nodes.keys().map(K::clone).collect::<Vec<K>>();
let mut order_visited = HashMap::new();
let mut root = HashMap::new();
let mut vstack = Vec::new();
let mut cstack = Vec::new();
let mut component = HashMap::new();
let mut comp_succ = Vec::new();
let mut comp_elts = Vec::new();
for node_id in node_ids {
if !order_visited.contains_key(&node_id) {
cyclic_tc_internal(
&node_id,
nodes,
&mut order_visited,
&mut root,
&mut vstack,
&mut cstack,
&mut component,
&mut comp_succ,
&mut comp_elts,
);
}
}
for comp_id in 0..comp_elts.len() {
let mut elt_succ = HashSet::new();
#[expect(
clippy::indexing_slicing,
reason = "`comp_succ` and `comp_elts` must have the same length, thus `comp_id` is a valid index to `comp_succ`."
)]
for comp_parent_id in comp_succ[comp_id].iter() {
#[expect(
clippy::indexing_slicing,
reason = "`comp_parent_id` must be a valid component id to be inserted into `comp_succ` therefore must exist within `comp_elts`."
)]
for node_id in comp_elts[*comp_parent_id].iter() {
elt_succ.insert(node_id.clone());
}
}
#[expect(
clippy::indexing_slicing,
reason = "`comp_id` in [0, |`comp_elts`|) is a valid index into `comp_elts`."
)]
for node_id in comp_elts[comp_id].iter() {
let node = match nodes.get_mut(node_id) {
Some(node) => node,
None => continue,
};
for parent_id in elt_succ.iter() {
node.add_edge_to(parent_id.clone());
}
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "internal function in complex algorithm"
)]
fn cyclic_tc_internal<K, V>(
node_id: &K,
nodes: &HashMap<K, V>,
order_visited: &mut HashMap<K, usize>,
root: &mut HashMap<K, K>,
vstack: &mut Vec<K>,
cstack: &mut Vec<usize>,
component: &mut HashMap<K, usize>,
comp_succ: &mut Vec<HashSet<usize>>,
comp_elts: &mut Vec<HashSet<K>>,
) where
K: Clone + Eq + Hash + Debug,
V: TCNode<K>,
{
let node_order = order_visited.len();
let mut root_order = node_order;
order_visited.insert(node_id.clone(), node_order);
root.insert(node_id.clone(), node_id.clone());
vstack.push(node_id.clone());
let height = cstack.len();
let mut self_loop = false;
let out_edges = match nodes.get(node_id) {
Some(node) => node.out_edges().collect(),
None => Vec::new(),
};
for parent_id in out_edges {
if node_id == parent_id {
self_loop = true;
} else {
let mut maybe_forward_edge = true;
if !order_visited.contains_key(parent_id) {
maybe_forward_edge = false;
cyclic_tc_internal(
parent_id,
nodes,
order_visited,
root,
vstack,
cstack,
component,
comp_succ,
comp_elts,
);
}
match component.get(parent_id) {
None => {
#[expect(
clippy::expect_used,
reason = "`parent_id` must have been visited either by a previous call or just above"
)]
let parent_root = root
.get(parent_id)
.expect("Parent has been visited so it must have a root.");
#[expect(
clippy::expect_used,
reason = "in order for `parent_root` to be the parent of `parent_id` it must have been visited."
)]
let parent_root_order = order_visited
.get(parent_root)
.expect("The parent's root must have been visited.");
if *parent_root_order < root_order {
root_order = *parent_root_order;
root.insert(node_id.clone(), parent_root.clone());
}
}
Some(parent_component) => {
#[expect(
clippy::expect_used,
reason = "`parent_id` must have been visited either by a previous call or just above"
)]
let parent_order = order_visited
.get(parent_id)
.expect("The parent must have been traversed by this point.");
if !(maybe_forward_edge && &node_order < parent_order) {
cstack.push(*parent_component);
}
}
}
}
} #[expect(
clippy::expect_used,
reason = "`node_id` must have a root. It was inserted at the begining of this function"
)]
let node_root = root
.get(node_id)
.expect("Node must have a root by this point.");
if node_id == node_root {
let component_id = comp_elts.len();
let mut succ = HashSet::new();
let mut elmts = HashSet::new();
#[expect(
clippy::expect_used,
reason = " The vertex stack must not be empty because at least node_id must be on the stack!"
)]
if self_loop || vstack.last().expect("vertex stack must be non-empty") != node_id {
succ.insert(component_id);
}
let mut cstack_tail = cstack.split_off(height);
cstack_tail.sort_by(|a, b| b.cmp(a));
for i in 0..cstack_tail.len() {
#[expect(
clippy::indexing_slicing,
reason = "both `i` and `i - 1` are guaranteed to be valid indices into `cstack_tail`."
)]
if i == 0 || cstack_tail[i - 1] != cstack_tail[i] {
#[expect(
clippy::indexing_slicing,
reason = "`i` is a valid index into `cstack_tail`."
)]
let tail_elt = cstack_tail[i];
if succ.insert(tail_elt) {
#[expect(
clippy::indexing_slicing,
reason = "`tail_elt` is a component id created by a previous call to `cyclic_tc_internal` and thus must be a valid index to `comp_succ`."
)]
succ.extend(comp_succ[tail_elt].clone());
}
}
}
loop {
#[expect(
clippy::expect_used,
reason = "The vertex stack `vstack` must contain at least `node_id`"
)]
let ancestor_id = vstack.pop().expect("Vetex stack must be non-empty");
component.insert(ancestor_id.clone(), component_id);
elmts.insert(ancestor_id.clone());
if *node_id == ancestor_id {
break;
}
}
comp_succ.push(succ);
comp_elts.push(elmts);
}
}
fn add_ancestors<K, V>(node_id: &K, nodes: &mut HashMap<K, V>, seen: &mut HashSet<K>)
where
K: Clone + Eq + Hash,
V: TCNode<K>,
{
let mut ancestors: HashSet<K> = HashSet::new();
let mut explored: HashSet<K> = HashSet::new();
let out_edges: Vec<K> = match nodes.get(node_id) {
Some(node) => node.out_edges().map(K::clone).collect(),
None => return,
};
for ancestor_id in out_edges {
if seen.insert(ancestor_id.clone()) {
add_ancestors(&ancestor_id, nodes, seen);
}
if explored.insert(ancestor_id.clone()) {
let ancestor = match nodes.get(&ancestor_id) {
Some(ancestor) => ancestor,
None => continue,
};
for grand_ancestor_id in ancestor.out_edges() {
ancestors.insert(grand_ancestor_id.clone());
}
}
}
#[expect(
clippy::expect_used,
reason = "this node should always exist because of the check to get `out_edges`"
)]
let node = nodes
.get_mut(node_id)
.expect("This node should always exist.");
for ancestor_id in ancestors {
node.add_edge_to(ancestor_id);
}
}
pub fn enforce_tc_and_dag<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
let res = enforce_tc(entities);
if res.is_ok() {
return enforce_dag_from_tc(entities);
}
res
}
fn enforce_tc<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
for entity in entities.values() {
for parent_uid in entity.out_edges() {
if let Some(parent) = entities.get(parent_uid) {
for grandparent in parent.out_edges() {
if !entity.has_edge_to(grandparent) {
return Err(TcError::missing_tc_edge(
entity.get_key(),
parent_uid.clone(),
grandparent.clone(),
));
}
}
}
}
}
Ok(())
}
fn enforce_dag_from_tc<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
for entity in entities.values() {
let key = entity.get_key();
if entity.out_edges().contains(&key) {
return Err(TcError::has_cycle(key));
}
}
Ok(())
}
fn enforce_dag_from_tc_for<K, V>(
nodes_to_check: &HashSet<K>,
entities: &HashMap<K, V>,
) -> Result<(), K>
where
K: Clone + Eq + Hash + Debug + Display,
V: TCNode<K>,
{
for key in nodes_to_check {
if let Some(entity) = entities.get(key) {
if entity.has_edge_to(key) {
return Err(TcError::has_cycle(key.clone()));
}
}
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::panic, clippy::indexing_slicing, reason = "test code")]
mod tests {
use crate::ast::{Entity, EntityUID};
use super::*;
#[test]
fn basic() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
assert!(!c.is_descendant_of(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn reversed() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(c.uid().clone(), c),
(b.uid().clone(), b),
(a.uid().clone(), a),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
assert!(!c.is_descendant_of(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn deeper() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("D"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
assert!(b.is_descendant_of(&EntityUID::with_eid("D")));
assert!(b.is_descendant_of(&EntityUID::with_eid("E")));
assert!(c.is_descendant_of(&EntityUID::with_eid("E")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn not_alphabetized() {
let mut foo = Entity::with_uid(EntityUID::with_eid("foo"));
foo.add_parent(EntityUID::with_eid("bar"));
let mut bar = Entity::with_uid(EntityUID::with_eid("bar"));
bar.add_parent(EntityUID::with_eid("baz"));
let mut baz = Entity::with_uid(EntityUID::with_eid("baz"));
baz.add_parent(EntityUID::with_eid("ham"));
let mut ham = Entity::with_uid(EntityUID::with_eid("ham"));
ham.add_parent(EntityUID::with_eid("eggs"));
let eggs = Entity::with_uid(EntityUID::with_eid("eggs"));
let mut entities = HashMap::from([
(ham.uid().clone(), ham),
(bar.uid().clone(), bar),
(foo.uid().clone(), foo),
(eggs.uid().clone(), eggs),
(baz.uid().clone(), baz),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let foo = &entities[&EntityUID::with_eid("foo")];
let bar = &entities[&EntityUID::with_eid("bar")];
let baz = &entities[&EntityUID::with_eid("baz")];
assert!(foo.is_descendant_of(&EntityUID::with_eid("baz")));
assert!(foo.is_descendant_of(&EntityUID::with_eid("ham")));
assert!(foo.is_descendant_of(&EntityUID::with_eid("eggs")));
assert!(bar.is_descendant_of(&EntityUID::with_eid("ham")));
assert!(bar.is_descendant_of(&EntityUID::with_eid("eggs")));
assert!(baz.is_descendant_of(&EntityUID::with_eid("eggs")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn multi_parents() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("D"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let d = &entities[&EntityUID::with_eid("D")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("D")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!d.is_descendant_of(&EntityUID::with_eid("B")));
assert!(!d.is_descendant_of(&EntityUID::with_eid("C")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn dag() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("D"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let mut g = Entity::with_uid(EntityUID::with_eid("G"));
g.add_parent(EntityUID::with_eid("E"));
let h = Entity::with_uid(EntityUID::with_eid("H"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let f = &entities[&EntityUID::with_eid("F")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
assert!(a.is_descendant_of(&EntityUID::with_eid("F")));
assert!(a.is_descendant_of(&EntityUID::with_eid("G")));
assert!(a.is_descendant_of(&EntityUID::with_eid("H")));
assert!(b.is_descendant_of(&EntityUID::with_eid("E")));
assert!(b.is_descendant_of(&EntityUID::with_eid("H")));
assert!(f.is_descendant_of(&EntityUID::with_eid("E")));
assert!(f.is_descendant_of(&EntityUID::with_eid("H")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("G")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("C")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("D")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn already_edges() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("C"));
a.add_parent(EntityUID::with_eid("D"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("E"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("E"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("C"));
d.add_parent(EntityUID::with_eid("F"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
let f = Entity::with_uid(EntityUID::with_eid("F"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
let d = &entities[&EntityUID::with_eid("D")];
assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
assert!(a.is_descendant_of(&EntityUID::with_eid("F")));
assert!(d.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
assert!(!c.is_descendant_of(&EntityUID::with_eid("F")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn disjoint_dag() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let g = Entity::with_uid(EntityUID::with_eid("G"));
let h = Entity::with_uid(EntityUID::with_eid("H"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let d = &entities[&EntityUID::with_eid("D")];
let f = &entities[&EntityUID::with_eid("F")];
assert!(a.is_descendant_of(&EntityUID::with_eid("G")));
assert!(d.is_descendant_of(&EntityUID::with_eid("H")));
assert!(!a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(!a.is_descendant_of(&EntityUID::with_eid("D")));
assert!(!a.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!a.is_descendant_of(&EntityUID::with_eid("H")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("H")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("E")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("H")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("G")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("C")));
assert!(!f.is_descendant_of(&EntityUID::with_eid("D")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
#[test]
fn trivial_cycle() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("B"));
let mut entities = HashMap::from([(a.uid().clone(), a), (b.uid().clone(), b)]);
compute_tc(&mut entities, false).expect("Failed to compute transitive closure");
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(err)) => {
assert!(err.vertex_with_loop() == &EntityUID::with_eid("B"));
}
Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
assert!(a.is_descendant_of(&EntityUID::with_eid("B")));
assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(err)) => {
assert!(err.vertex_with_loop() == &EntityUID::with_eid("B"));
}
Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
}
#[test]
fn nontrivial_cycle() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("D"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("A"));
let d = Entity::with_uid(EntityUID::with_eid("D"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
]);
compute_tc_internal(
entities
.keys()
.map(EntityUID::clone)
.collect::<Vec<EntityUID>>()
.into_iter(),
&mut entities,
HashSet::new(),
true,
);
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(err)) => {
assert!(
err.vertex_with_loop() == &EntityUID::with_eid("A")
|| err.vertex_with_loop() == &EntityUID::with_eid("B")
|| err.vertex_with_loop() == &EntityUID::with_eid("C")
);
}
Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
assert!(b.is_descendant_of(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(err)) => {
assert!(
err.vertex_with_loop() == &EntityUID::with_eid("A")
|| err.vertex_with_loop() == &EntityUID::with_eid("B")
|| err.vertex_with_loop() == &EntityUID::with_eid("C")
);
}
Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
}
#[test]
fn disjoint_cycles() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c: Entity = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("B"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let g = Entity::with_uid(EntityUID::with_eid("G"));
let mut h = Entity::with_uid(EntityUID::with_eid("H"));
h.add_parent(EntityUID::with_eid("D"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
]);
assert!(enforce_tc(&entities).is_err());
compute_tc_internal(
entities
.keys()
.map(EntityUID::clone)
.collect::<Vec<EntityUID>>()
.into_iter(),
&mut entities,
HashSet::new(),
true,
);
assert!(enforce_tc(&entities).is_ok());
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(err)) => {
assert!(
err.vertex_with_loop() == &EntityUID::with_eid("B")
|| err.vertex_with_loop() == &EntityUID::with_eid("C")
|| err.vertex_with_loop() == &EntityUID::with_eid("D")
|| err.vertex_with_loop() == &EntityUID::with_eid("E")
|| err.vertex_with_loop() == &EntityUID::with_eid("H")
);
}
Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
}
#[test]
fn intersecting_cycles() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("E"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("A"));
d.add_parent(EntityUID::with_eid("B"));
d.add_parent(EntityUID::with_eid("F"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("D"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("E"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
]);
assert!(enforce_tc(&entities).is_err());
cyclic_tc(&mut entities);
assert!(enforce_tc(&entities).is_ok());
match enforce_dag_from_tc(&entities) {
Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
Err(TcError::HasCycle(_)) => (), Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
}
}
#[test]
fn updated() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
]);
assert!(enforce_tc(&entities).is_err());
let all_ids: Vec<_> = entities.keys().cloned().collect();
compute_tc_internal(all_ids.into_iter(), &mut entities, HashSet::new(), false);
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
assert!(a.has_edge_to(&EntityUID::with_eid("C")));
assert!(!b.has_edge_to(&EntityUID::with_eid("A")));
assert!(!c.has_edge_to(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
assert!(!a.has_edge_to(&EntityUID::with_eid("D")));
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("D"));
let d = Entity::with_uid(EntityUID::with_eid("D"));
entities.insert(a.uid().clone(), a);
entities.insert(b.uid().clone(), b);
entities.insert(d.uid().clone(), d);
assert!(enforce_tc(&entities).is_err());
let all_ids: Vec<_> = entities.keys().cloned().collect();
compute_tc_internal(all_ids.into_iter(), &mut entities, HashSet::new(), false);
let a = &entities[&EntityUID::with_eid("A")];
let b = &entities[&EntityUID::with_eid("B")];
let c = &entities[&EntityUID::with_eid("C")];
let d = &entities[&EntityUID::with_eid("D")];
assert!(a.has_edge_to(&EntityUID::with_eid("D")));
assert!(!a.has_edge_to(&EntityUID::with_eid("C")));
assert!(!b.has_edge_to(&EntityUID::with_eid("C")));
assert!(!b.has_edge_to(&EntityUID::with_eid("A")));
assert!(!c.has_edge_to(&EntityUID::with_eid("A")));
assert!(!d.has_edge_to(&EntityUID::with_eid("A")));
assert!(enforce_tc(&entities).is_ok());
assert!(enforce_dag_from_tc(&entities).is_ok());
}
fn snapshot(entities: &HashMap<EntityUID, Entity>) -> HashMap<EntityUID, HashSet<EntityUID>> {
entities
.iter()
.map(|(k, v)| (k.clone(), v.out_edges().cloned().collect()))
.collect()
}
fn fresh_copy(entities: &HashMap<EntityUID, Entity>) -> HashMap<EntityUID, Entity> {
entities
.values()
.map(|e| {
let mut fresh = Entity::with_uid(e.uid().clone());
for p in e.parents() {
fresh.add_parent(p.clone());
}
(fresh.uid().clone(), fresh)
})
.collect()
}
#[test]
fn cyclic_tc_oracle_on_dags() {
for (name, base) in &dag_test_graphs() {
let mut via_compute = fresh_copy(base);
compute_tc(&mut via_compute, false).expect("compute_tc failed");
let mut via_cyclic = fresh_copy(base);
cyclic_tc(&mut via_cyclic);
assert_eq!(
snapshot(&via_compute),
snapshot(&via_cyclic),
"TC mismatch on DAG '{name}'"
);
}
}
#[test]
fn cyclic_tc_oracle_on_cycles() {
for (name, base) in &cyclic_test_graphs() {
let mut entities = fresh_copy(base);
cyclic_tc(&mut entities);
assert!(
enforce_tc(&entities).is_ok(),
"enforce_tc failed after cyclic_tc on '{name}'"
);
assert!(
enforce_dag_from_tc(&entities).is_err(),
"expected cycle detection on '{name}'"
);
}
}
#[test]
fn self_loop_with_grandchild() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
]);
compute_tc(&mut entities, false).expect("compute_tc failed");
assert!(entities[&EntityUID::with_eid("A")].has_edge_to(&EntityUID::with_eid("C")));
assert!(enforce_tc(&entities).is_ok());
}
fn dag_test_graphs() -> Vec<(&'static str, HashMap<EntityUID, Entity>)> {
vec![
("A->B->C", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
])
}),
("A->B->C->D->E", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("D"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
])
}),
("multi_parents", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("D"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
])
}),
("diamond", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("C"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("D"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("D"));
let d = Entity::with_uid(EntityUID::with_eid("D"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
])
}),
("dag_with_join", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("D"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let mut g = Entity::with_uid(EntityUID::with_eid("G"));
g.add_parent(EntityUID::with_eid("E"));
let h = Entity::with_uid(EntityUID::with_eid("H"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
])
}),
("already_edges", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("C"));
a.add_parent(EntityUID::with_eid("D"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("E"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("E"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("C"));
d.add_parent(EntityUID::with_eid("F"));
let e = Entity::with_uid(EntityUID::with_eid("E"));
let f = Entity::with_uid(EntityUID::with_eid("F"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
])
}),
("disjoint", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let g = Entity::with_uid(EntityUID::with_eid("G"));
let h = Entity::with_uid(EntityUID::with_eid("H"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
])
}),
]
}
fn cyclic_test_graphs() -> Vec<(&'static str, HashMap<EntityUID, Entity>)> {
vec![
("trivial_cycle", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("B"));
HashMap::from([(a.uid().clone(), a), (b.uid().clone(), b)])
}),
("simple_cycle", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
b.add_parent(EntityUID::with_eid("D"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("A"));
let d = Entity::with_uid(EntityUID::with_eid("D"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
])
}),
("disjoint_cycles", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("F"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("B"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("E"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("H"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("G"));
let g = Entity::with_uid(EntityUID::with_eid("G"));
let mut h = Entity::with_uid(EntityUID::with_eid("H"));
h.add_parent(EntityUID::with_eid("D"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
(g.uid().clone(), g),
(h.uid().clone(), h),
])
}),
("intersecting_cycles", {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let mut c = Entity::with_uid(EntityUID::with_eid("C"));
c.add_parent(EntityUID::with_eid("E"));
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("A"));
d.add_parent(EntityUID::with_eid("B"));
d.add_parent(EntityUID::with_eid("F"));
let mut e = Entity::with_uid(EntityUID::with_eid("E"));
e.add_parent(EntityUID::with_eid("D"));
let mut f = Entity::with_uid(EntityUID::with_eid("F"));
f.add_parent(EntityUID::with_eid("E"));
HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
(d.uid().clone(), d),
(e.uid().clone(), e),
(f.uid().clone(), f),
])
}),
]
}
#[test]
fn add_ancestors_dangling_parent_adds_transitive() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
a.add_parent(EntityUID::with_eid("X"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
]);
compute_tc(&mut entities, false).expect("compute_tc failed");
let a = entities.get(&EntityUID::with_eid("A")).unwrap();
assert!(a.is_descendant_of(&EntityUID::with_eid("C")),);
}
#[test]
fn repair_tc_no_dag_computes_tc() {
let mut a = Entity::with_uid(EntityUID::with_eid("A"));
a.add_parent(EntityUID::with_eid("B"));
let mut b = Entity::with_uid(EntityUID::with_eid("B"));
b.add_parent(EntityUID::with_eid("C"));
let c = Entity::with_uid(EntityUID::with_eid("C"));
let mut entities = HashMap::from([
(a.uid().clone(), a),
(b.uid().clone(), b),
(c.uid().clone(), c),
]);
compute_tc(&mut entities, false).expect("initial compute_tc failed");
let mut d = Entity::with_uid(EntityUID::with_eid("D"));
d.add_parent(EntityUID::with_eid("B"));
entities.insert(d.uid().clone(), d);
let nodes_to_fix = HashSet::from([EntityUID::with_eid("D")]);
repair_tc(nodes_to_fix, &mut entities, false).expect("repair_tc failed");
let d = entities.get(&EntityUID::with_eid("D")).unwrap();
assert!(d.is_descendant_of(&EntityUID::with_eid("C")));
}
fn repair_tc_test_cases() -> Vec<(
&'static str,
Vec<(&'static str, &'static str)>,
Vec<(&'static str, &'static str)>,
Vec<&'static str>,
)> {
vec![
(
"add_leaf_node",
vec![("A", "B"), ("B", "C")],
vec![("D", "C")],
vec!["D"],
),
(
"add_intermediate_edge",
vec![("A", "B"), ("B", "C"), ("C", "D")],
vec![("A", "C")],
vec!["A"],
),
(
"diamond_new_entry",
vec![("A", "B"), ("A", "C"), ("B", "D"), ("C", "D")],
vec![("E", "B")],
vec!["E"],
),
(
"multiple_new_nodes",
vec![("A", "B"), ("B", "C")],
vec![("D", "A"), ("E", "A")],
vec!["D", "E"],
),
(
"deep_chain_extension",
vec![("A", "B"), ("B", "C"), ("C", "D"), ("D", "E")],
vec![("F", "A")],
vec!["F"],
),
(
"wide_fan_in",
vec![("B", "D"), ("C", "D")],
vec![("A", "B"), ("A", "C")],
vec!["A"],
),
(
"bridge_disjoint_components",
vec![("A", "B"), ("C", "D")],
vec![("E", "B"), ("E", "D")],
vec!["E"],
),
(
"node_gains_second_parent",
vec![("A", "B"), ("B", "C"), ("C", "D")],
vec![("B", "E"), ("E", "D")],
vec!["A", "B", "E"],
),
]
}
#[test]
fn repair_tc_cases() {
for (name, edges, new_edges, nodes_to_fix) in &repair_tc_test_cases() {
let mut ids: HashSet<&str> = HashSet::new();
for (s, d) in edges.iter().chain(new_edges.iter()) {
ids.insert(s);
ids.insert(d);
}
let mut entities: HashMap<EntityUID, Entity> = ids
.iter()
.map(|id| {
let e = Entity::with_uid(EntityUID::with_eid(id));
(e.uid().clone(), e)
})
.collect();
for (src, dst) in edges {
entities
.get_mut(&EntityUID::with_eid(src))
.unwrap()
.add_parent(EntityUID::with_eid(dst));
}
compute_tc(&mut entities, false).expect("initial compute_tc failed");
let fix_ids: HashSet<&str> = nodes_to_fix.iter().copied().collect();
for id in &fix_ids {
entities
.get_mut(&EntityUID::with_eid(id))
.unwrap()
.reset_edges();
}
for (src, dst) in edges.iter().chain(new_edges.iter()) {
if fix_ids.contains(src) {
entities
.get_mut(&EntityUID::with_eid(src))
.unwrap()
.add_parent(EntityUID::with_eid(dst));
}
}
for id in &fix_ids {
entities
.entry(EntityUID::with_eid(id))
.or_insert_with(|| Entity::with_uid(EntityUID::with_eid(id)));
}
let fix_set: HashSet<EntityUID> = nodes_to_fix
.iter()
.map(|id| EntityUID::with_eid(id))
.collect();
repair_tc(fix_set, &mut entities, false)
.unwrap_or_else(|_| panic!("repair_tc failed on '{name}'"));
let repaired = snapshot(&entities);
let mut expected_entities: HashMap<EntityUID, Entity> = ids
.iter()
.map(|id| {
let e = Entity::with_uid(EntityUID::with_eid(id));
(e.uid().clone(), e)
})
.collect();
for (src, dst) in edges.iter().chain(new_edges.iter()) {
expected_entities
.get_mut(&EntityUID::with_eid(src))
.unwrap()
.add_parent(EntityUID::with_eid(dst));
}
compute_tc(&mut expected_entities, false).expect("expected compute_tc failed");
let expected = snapshot(&expected_entities);
assert_eq!(
repaired, expected,
"repair_tc result differs from full compute_tc on '{name}'"
);
}
}
}