Skip to main content

cedar_policy_core/
transitive_closure.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Module containing code to compute the transitive closure of a graph.
18//! This is a generic utility, and not specific to Cedar.
19
20use std::collections::{HashMap, HashSet};
21use std::fmt::{Debug, Display};
22use std::hash::Hash;
23
24mod err;
25pub use err::*;
26use itertools::Itertools;
27
28/// Trait used to generalize transitive closure computation. This trait should
29/// be implemented for types representing a node in the hierarchy (e.g., the
30/// entity hierarchy) where we need to compute the transitive closure of the
31/// hierarchy starting from only direct adjacencies. This trait is parametrized
32/// by a type `K` which represents a unique identifier for graph nodes.
33pub trait TCNode<K> {
34    /// Extract a unique identifier for the node.
35    fn get_key(&self) -> K;
36
37    /// Add an edge out off this node to the node with key `k`.
38    fn add_edge_to(&mut self, k: K);
39
40    /// Retrieve an iterator for the edges out of this node.
41    fn out_edges(&self) -> Box<dyn Iterator<Item = &K> + '_>;
42
43    /// Return true when their is an edge between this node and the node with
44    /// key `k`.
45    fn has_edge_to(&self, k: &K) -> bool;
46
47    /// Resets edges to base
48    fn reset_edges(&mut self);
49
50    /// Retrieves an iterator for direct edges out of this node.
51    fn direct_edges(&self) -> Box<dyn Iterator<Item = &K> + '_> {
52        self.out_edges()
53    }
54}
55
56/// Given Graph as a map from keys with type `K` to implementations of `TCNode`
57/// with type `V`, compute the transitive closure of the hierarchy. In case of
58/// error, the result contains an error structure `Err<K>` which contains the
59/// keys (with type `K`) for the nodes in the graph which caused the error.
60/// If `enforce_dag` then also check that the hierarchy is a DAG
61pub fn compute_tc<K, V>(nodes: &mut HashMap<K, V>, enforce_dag: bool) -> Result<(), K>
62where
63    K: Clone + Eq + Hash + Debug + Display,
64    V: TCNode<K>,
65{
66    compute_tc_internal::<K, V>(nodes);
67    if enforce_dag {
68        return enforce_dag_from_tc(nodes);
69    }
70    Ok(())
71}
72
73/// Given graph as a map from keys with type `K` to implementations of `TCNode`
74/// with type `V`, compute the transitive closure of the hierarchy. In case of
75/// error, the result contains an error structure `Err<K>` which contains the
76/// keys (with type `K`) for the nodes in the graph which caused the error.
77fn compute_tc_internal<K, V>(nodes: &mut HashMap<K, V>)
78where
79    K: Clone + Eq + Hash,
80    V: TCNode<K>,
81{
82    // To avoid needing both immutable and mutable borrows of `nodes`,
83    // we collect all the needed updates in this structure
84    // (maps keys to ancestor UIDs to add to it)
85    // and then do all the updates at once in a second loop
86    let mut ancestors: HashMap<K, HashSet<K>> = HashMap::new();
87    for node in nodes.values() {
88        let this_node_ancestors: &mut HashSet<K> = ancestors.entry(node.get_key()).or_default();
89        add_ancestors_to_set(node, nodes, this_node_ancestors);
90    }
91    for node in nodes.values_mut() {
92        node.reset_edges();
93        #[expect(
94            clippy::expect_used,
95            reason = "All nodes in `ancestors` came from `nodes`"
96        )]
97        for ancestor_uid in ancestors
98            .get(&node.get_key())
99            .expect("shouldn't have added any new values to the `nodes` map")
100        {
101            node.add_edge_to(ancestor_uid.clone());
102        }
103    }
104}
105
106/// Given a graph (as a map from keys to `TCNode`), enforce that
107/// all transitive edges are included, ie, the transitive closure has already
108/// been computed and that it is a DAG. If this is not the case, return an appropriate
109/// `TCEnforcementError`.
110pub fn enforce_tc_and_dag<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
111where
112    K: Clone + Eq + Hash + Debug + Display,
113    V: TCNode<K>,
114{
115    let res = enforce_tc(entities);
116    if res.is_ok() {
117        return enforce_dag_from_tc(entities);
118    }
119    res
120}
121
122/// Given a DAG (as a map from keys to `TCNode`), enforce that
123/// all transitive edges are included, i.e., the transitive closure has already
124/// been computed. If this is not the case, return an appropriate
125/// `MissingTcEdge` error.
126fn enforce_tc<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
127where
128    K: Clone + Eq + Hash + Debug + Display,
129    V: TCNode<K>,
130{
131    for entity in entities.values() {
132        for parent_uid in entity.out_edges() {
133            // check that `entity` is also a child of all of this parent's parents
134            if let Some(parent) = entities.get(parent_uid) {
135                for grandparent in parent.out_edges() {
136                    if !entity.has_edge_to(grandparent) {
137                        return Err(TcError::missing_tc_edge(
138                            entity.get_key(),
139                            parent_uid.clone(),
140                            grandparent.clone(),
141                        ));
142                    }
143                }
144            }
145        }
146    }
147    Ok(())
148}
149
150/// For the given `node` in the given `hierarchy`, add all of the `node`'s
151/// transitive ancestors to the given set. Assume that any nodes already in
152/// `ancestors` don't need to be searched -- they have been already handled.
153fn add_ancestors_to_set<K, V>(node: &V, hierarchy: &HashMap<K, V>, ancestors: &mut HashSet<K>)
154where
155    K: Clone + Eq + Hash,
156    V: TCNode<K>,
157{
158    for parent_uid in node.direct_edges() {
159        if ancestors.insert(parent_uid.clone()) {
160            // discovered a new ancestor, so add the ancestors of `ancestor` as
161            // well
162            if let Some(ancestor) = hierarchy.get(parent_uid) {
163                add_ancestors_to_set(ancestor, hierarchy, ancestors);
164            }
165        }
166    }
167}
168
169/// Once the transitive closure (as defined above) is computed/enforced for the graph, we have:
170/// \forall u,v,w \in Vertices . (u,v) \in Edges /\ (v,w) \in Edges -> (u,w) \in Edges
171///
172/// Then the graph has a cycle if
173/// \exists v \in Vertices. (v,v) \in Edges
174fn enforce_dag_from_tc<K, V>(entities: &HashMap<K, V>) -> Result<(), K>
175where
176    K: Clone + Eq + Hash + Debug + Display,
177    V: TCNode<K>,
178{
179    for entity in entities.values() {
180        let key = entity.get_key();
181        if entity.out_edges().contains(&key) {
182            return Err(TcError::has_cycle(key));
183        }
184    }
185    Ok(())
186}
187
188#[cfg(test)]
189#[expect(clippy::panic, clippy::indexing_slicing, reason = "test code")]
190mod tests {
191    use crate::ast::{Entity, EntityUID};
192
193    use super::*;
194
195    #[test]
196    fn basic() {
197        // start with A -> B -> C
198        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
199        a.add_parent(EntityUID::with_eid("B"));
200        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
201        b.add_parent(EntityUID::with_eid("C"));
202        let c = Entity::with_uid(EntityUID::with_eid("C"));
203        let mut entities = HashMap::from([
204            (a.uid().clone(), a),
205            (b.uid().clone(), b),
206            (c.uid().clone(), c),
207        ]);
208        // currently doesn't pass TC enforcement
209        assert!(enforce_tc(&entities).is_err());
210        // compute TC
211        compute_tc_internal(&mut entities);
212        let a = &entities[&EntityUID::with_eid("A")];
213        let b = &entities[&EntityUID::with_eid("B")];
214        let c = &entities[&EntityUID::with_eid("C")];
215        // should have added the A -> C edge
216        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
217        // but we shouldn't have added other edges, like B -> A or C -> A
218        assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
219        assert!(!c.is_descendant_of(&EntityUID::with_eid("A")));
220        // now it should pass TC enforcement
221        assert!(enforce_tc(&entities).is_ok());
222        // passes cycle check after TC enforcement
223        assert!(enforce_dag_from_tc(&entities).is_ok());
224    }
225
226    #[test]
227    fn reversed() {
228        // same as basic(), but we put the entities in the map in the reverse
229        // order, which shouldn't make a difference
230        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
231        a.add_parent(EntityUID::with_eid("B"));
232        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
233        b.add_parent(EntityUID::with_eid("C"));
234        let c = Entity::with_uid(EntityUID::with_eid("C"));
235        let mut entities = HashMap::from([
236            (c.uid().clone(), c),
237            (b.uid().clone(), b),
238            (a.uid().clone(), a),
239        ]);
240        // currently doesn't pass TC enforcement
241        assert!(enforce_tc(&entities).is_err());
242        // compute TC
243        compute_tc_internal(&mut entities);
244        let a = &entities[&EntityUID::with_eid("A")];
245        let b = &entities[&EntityUID::with_eid("B")];
246        let c = &entities[&EntityUID::with_eid("C")];
247        // should have added the A -> C edge
248        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
249        // but we shouldn't have added other edges, like B -> A or C -> A
250        assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
251        assert!(!c.is_descendant_of(&EntityUID::with_eid("A")));
252        // now it should pass TC enforcement
253        assert!(enforce_tc(&entities).is_ok());
254        // passes cycle check after TC enforcement
255        assert!(enforce_dag_from_tc(&entities).is_ok());
256    }
257
258    #[test]
259    fn deeper() {
260        // start with A -> B -> C -> D -> E
261        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
262        a.add_parent(EntityUID::with_eid("B"));
263        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
264        b.add_parent(EntityUID::with_eid("C"));
265        let mut c = Entity::with_uid(EntityUID::with_eid("C"));
266        c.add_parent(EntityUID::with_eid("D"));
267        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
268        d.add_parent(EntityUID::with_eid("E"));
269        let e = Entity::with_uid(EntityUID::with_eid("E"));
270        let mut entities = HashMap::from([
271            (a.uid().clone(), a),
272            (b.uid().clone(), b),
273            (c.uid().clone(), c),
274            (d.uid().clone(), d),
275            (e.uid().clone(), e),
276        ]);
277        // currently doesn't pass TC enforcement
278        assert!(enforce_tc(&entities).is_err());
279        // compute TC
280        compute_tc_internal(&mut entities);
281        let a = &entities[&EntityUID::with_eid("A")];
282        let b = &entities[&EntityUID::with_eid("B")];
283        let c = &entities[&EntityUID::with_eid("C")];
284        // should have added many edges which we check for
285        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
286        assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
287        assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
288        assert!(b.is_descendant_of(&EntityUID::with_eid("D")));
289        assert!(b.is_descendant_of(&EntityUID::with_eid("E")));
290        assert!(c.is_descendant_of(&EntityUID::with_eid("E")));
291        // now it should pass TC enforcement
292        assert!(enforce_tc(&entities).is_ok());
293        // passes cycle check after TC enforcement
294        assert!(enforce_dag_from_tc(&entities).is_ok());
295    }
296
297    #[test]
298    fn not_alphabetized() {
299        // same as deeper(), but the entities' parent relations don't follow
300        // alphabetical order. (In case we end up iterating through the map
301        // in alphabetical order, this test will ensure that everything works
302        // even when we aren't processing the entities in hierarchy order.)
303        // start with foo -> bar -> baz -> ham -> eggs
304        let mut foo = Entity::with_uid(EntityUID::with_eid("foo"));
305        foo.add_parent(EntityUID::with_eid("bar"));
306        let mut bar = Entity::with_uid(EntityUID::with_eid("bar"));
307        bar.add_parent(EntityUID::with_eid("baz"));
308        let mut baz = Entity::with_uid(EntityUID::with_eid("baz"));
309        baz.add_parent(EntityUID::with_eid("ham"));
310        let mut ham = Entity::with_uid(EntityUID::with_eid("ham"));
311        ham.add_parent(EntityUID::with_eid("eggs"));
312        let eggs = Entity::with_uid(EntityUID::with_eid("eggs"));
313        let mut entities = HashMap::from([
314            (ham.uid().clone(), ham),
315            (bar.uid().clone(), bar),
316            (foo.uid().clone(), foo),
317            (eggs.uid().clone(), eggs),
318            (baz.uid().clone(), baz),
319        ]);
320        // currently doesn't pass TC enforcement
321        assert!(enforce_tc(&entities).is_err());
322        // compute TC
323        compute_tc_internal(&mut entities);
324        let foo = &entities[&EntityUID::with_eid("foo")];
325        let bar = &entities[&EntityUID::with_eid("bar")];
326        let baz = &entities[&EntityUID::with_eid("baz")];
327        // should have added many edges which we check for
328        assert!(foo.is_descendant_of(&EntityUID::with_eid("baz")));
329        assert!(foo.is_descendant_of(&EntityUID::with_eid("ham")));
330        assert!(foo.is_descendant_of(&EntityUID::with_eid("eggs")));
331        assert!(bar.is_descendant_of(&EntityUID::with_eid("ham")));
332        assert!(bar.is_descendant_of(&EntityUID::with_eid("eggs")));
333        assert!(baz.is_descendant_of(&EntityUID::with_eid("eggs")));
334        // now it should pass TC enforcement
335        assert!(enforce_tc(&entities).is_ok());
336        // passes cycle check after TC enforcement
337        assert!(enforce_dag_from_tc(&entities).is_ok());
338    }
339
340    #[test]
341    fn multi_parents() {
342        // start with this:
343        //     B -> C
344        //   /
345        // A
346        //   \
347        //     D -> E
348        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
349        a.add_parent(EntityUID::with_eid("B"));
350        a.add_parent(EntityUID::with_eid("D"));
351        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
352        b.add_parent(EntityUID::with_eid("C"));
353        let c = Entity::with_uid(EntityUID::with_eid("C"));
354        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
355        d.add_parent(EntityUID::with_eid("E"));
356        let e = Entity::with_uid(EntityUID::with_eid("E"));
357        let mut entities = HashMap::from([
358            (a.uid().clone(), a),
359            (b.uid().clone(), b),
360            (c.uid().clone(), c),
361            (d.uid().clone(), d),
362            (e.uid().clone(), e),
363        ]);
364        // currently doesn't pass TC enforcement
365        assert!(enforce_tc(&entities).is_err());
366        // compute TC
367        compute_tc_internal(&mut entities);
368        let a = &entities[&EntityUID::with_eid("A")];
369        let b = &entities[&EntityUID::with_eid("B")];
370        let d = &entities[&EntityUID::with_eid("D")];
371        // should have added the A -> C edge and the A -> E edge
372        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
373        assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
374        // but it shouldn't have added these other edges
375        assert!(!b.is_descendant_of(&EntityUID::with_eid("D")));
376        assert!(!b.is_descendant_of(&EntityUID::with_eid("E")));
377        assert!(!d.is_descendant_of(&EntityUID::with_eid("B")));
378        assert!(!d.is_descendant_of(&EntityUID::with_eid("C")));
379        // now it should pass TC enforcement
380        assert!(enforce_tc(&entities).is_ok());
381        // passes cycle check after TC enforcement
382        assert!(enforce_dag_from_tc(&entities).is_ok());
383    }
384
385    #[test]
386    fn dag() {
387        // start with this:
388        //     B -> C
389        //   /  \
390        // A      D -> E -> H
391        //   \        /
392        //     F -> G
393        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
394        a.add_parent(EntityUID::with_eid("B"));
395        a.add_parent(EntityUID::with_eid("F"));
396        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
397        b.add_parent(EntityUID::with_eid("C"));
398        b.add_parent(EntityUID::with_eid("D"));
399        let c = Entity::with_uid(EntityUID::with_eid("C"));
400        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
401        d.add_parent(EntityUID::with_eid("E"));
402        let mut e = Entity::with_uid(EntityUID::with_eid("E"));
403        e.add_parent(EntityUID::with_eid("H"));
404        let mut f = Entity::with_uid(EntityUID::with_eid("F"));
405        f.add_parent(EntityUID::with_eid("G"));
406        let mut g = Entity::with_uid(EntityUID::with_eid("G"));
407        g.add_parent(EntityUID::with_eid("E"));
408        let h = Entity::with_uid(EntityUID::with_eid("H"));
409        let mut entities = HashMap::from([
410            (a.uid().clone(), a),
411            (b.uid().clone(), b),
412            (c.uid().clone(), c),
413            (d.uid().clone(), d),
414            (e.uid().clone(), e),
415            (f.uid().clone(), f),
416            (g.uid().clone(), g),
417            (h.uid().clone(), h),
418        ]);
419        // currently doesn't pass TC enforcement
420        assert!(enforce_tc(&entities).is_err());
421        // compute TC
422        compute_tc_internal(&mut entities);
423        let a = &entities[&EntityUID::with_eid("A")];
424        let b = &entities[&EntityUID::with_eid("B")];
425        let f = &entities[&EntityUID::with_eid("F")];
426        // should have added many edges which we check for
427        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
428        assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
429        assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
430        assert!(a.is_descendant_of(&EntityUID::with_eid("F")));
431        assert!(a.is_descendant_of(&EntityUID::with_eid("G")));
432        assert!(a.is_descendant_of(&EntityUID::with_eid("H")));
433        assert!(b.is_descendant_of(&EntityUID::with_eid("E")));
434        assert!(b.is_descendant_of(&EntityUID::with_eid("H")));
435        assert!(f.is_descendant_of(&EntityUID::with_eid("E")));
436        assert!(f.is_descendant_of(&EntityUID::with_eid("H")));
437        // but it shouldn't have added these other edges
438        assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
439        assert!(!b.is_descendant_of(&EntityUID::with_eid("G")));
440        assert!(!f.is_descendant_of(&EntityUID::with_eid("C")));
441        assert!(!f.is_descendant_of(&EntityUID::with_eid("D")));
442        // now it should pass TC enforcement
443        assert!(enforce_tc(&entities).is_ok());
444        // passes cycle check after TC enforcement
445        assert!(enforce_dag_from_tc(&entities).is_ok());
446    }
447
448    #[test]
449    fn already_edges() {
450        // start with this, which already includes some (but not all) transitive
451        // edges
452        //     B --> E
453        //   /  \   /
454        // A ---> C
455        //   \   /
456        //     D --> F
457        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
458        a.add_parent(EntityUID::with_eid("B"));
459        a.add_parent(EntityUID::with_eid("C"));
460        a.add_parent(EntityUID::with_eid("D"));
461        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
462        b.add_parent(EntityUID::with_eid("C"));
463        b.add_parent(EntityUID::with_eid("E"));
464        let mut c = Entity::with_uid(EntityUID::with_eid("C"));
465        c.add_parent(EntityUID::with_eid("E"));
466        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
467        d.add_parent(EntityUID::with_eid("C"));
468        d.add_parent(EntityUID::with_eid("F"));
469        let e = Entity::with_uid(EntityUID::with_eid("E"));
470        let f = Entity::with_uid(EntityUID::with_eid("F"));
471        let mut entities = HashMap::from([
472            (a.uid().clone(), a),
473            (b.uid().clone(), b),
474            (c.uid().clone(), c),
475            (d.uid().clone(), d),
476            (e.uid().clone(), e),
477            (f.uid().clone(), f),
478        ]);
479        // currently doesn't pass TC enforcement
480        assert!(enforce_tc(&entities).is_err());
481        // compute TC
482        compute_tc_internal(&mut entities);
483        let a = &entities[&EntityUID::with_eid("A")];
484        let b = &entities[&EntityUID::with_eid("B")];
485        let c = &entities[&EntityUID::with_eid("C")];
486        let d = &entities[&EntityUID::with_eid("D")];
487        // should have added many edges which we check for
488        assert!(a.is_descendant_of(&EntityUID::with_eid("E")));
489        assert!(a.is_descendant_of(&EntityUID::with_eid("F")));
490        assert!(d.is_descendant_of(&EntityUID::with_eid("E")));
491        // but it shouldn't have added these other edges
492        assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
493        assert!(!c.is_descendant_of(&EntityUID::with_eid("F")));
494        // now it should pass TC enforcement
495        assert!(enforce_tc(&entities).is_ok());
496        // passes cycle check after TC enforcement
497        assert!(enforce_dag_from_tc(&entities).is_ok());
498    }
499
500    #[test]
501    fn disjoint_dag() {
502        // graph with disconnected components:
503        //     B -> C
504        //
505        // A      D -> E -> H
506        //   \
507        //     F -> G
508        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
509        a.add_parent(EntityUID::with_eid("F"));
510        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
511        b.add_parent(EntityUID::with_eid("C"));
512        let c = Entity::with_uid(EntityUID::with_eid("C"));
513        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
514        d.add_parent(EntityUID::with_eid("E"));
515        let mut e = Entity::with_uid(EntityUID::with_eid("E"));
516        e.add_parent(EntityUID::with_eid("H"));
517        let mut f = Entity::with_uid(EntityUID::with_eid("F"));
518        f.add_parent(EntityUID::with_eid("G"));
519        let g = Entity::with_uid(EntityUID::with_eid("G"));
520        let h = Entity::with_uid(EntityUID::with_eid("H"));
521        let mut entities = HashMap::from([
522            (a.uid().clone(), a),
523            (b.uid().clone(), b),
524            (c.uid().clone(), c),
525            (d.uid().clone(), d),
526            (e.uid().clone(), e),
527            (f.uid().clone(), f),
528            (g.uid().clone(), g),
529            (h.uid().clone(), h),
530        ]);
531        // currently doesn't pass TC enforcement
532        assert!(enforce_tc(&entities).is_err());
533        // compute TC
534        compute_tc_internal(&mut entities);
535        let a = &entities[&EntityUID::with_eid("A")];
536        let b = &entities[&EntityUID::with_eid("B")];
537        let d = &entities[&EntityUID::with_eid("D")];
538        let f = &entities[&EntityUID::with_eid("F")];
539        // should have added two edges
540        assert!(a.is_descendant_of(&EntityUID::with_eid("G")));
541        assert!(d.is_descendant_of(&EntityUID::with_eid("H")));
542        // but it shouldn't have added these other edges
543        assert!(!a.is_descendant_of(&EntityUID::with_eid("C")));
544        assert!(!a.is_descendant_of(&EntityUID::with_eid("D")));
545        assert!(!a.is_descendant_of(&EntityUID::with_eid("E")));
546        assert!(!a.is_descendant_of(&EntityUID::with_eid("H")));
547        assert!(!b.is_descendant_of(&EntityUID::with_eid("E")));
548        assert!(!b.is_descendant_of(&EntityUID::with_eid("H")));
549        assert!(!f.is_descendant_of(&EntityUID::with_eid("E")));
550        assert!(!f.is_descendant_of(&EntityUID::with_eid("H")));
551        assert!(!b.is_descendant_of(&EntityUID::with_eid("F")));
552        assert!(!b.is_descendant_of(&EntityUID::with_eid("G")));
553        assert!(!f.is_descendant_of(&EntityUID::with_eid("C")));
554        assert!(!f.is_descendant_of(&EntityUID::with_eid("D")));
555        // now it should pass TC enforcement
556        assert!(enforce_tc(&entities).is_ok());
557        // passes cycle check after TC enforcement
558        assert!(enforce_dag_from_tc(&entities).is_ok());
559    }
560
561    #[test]
562    fn trivial_cycle() {
563        // this graph is invalid, but we want to still have some reasonable behavior
564        // if we encounter it (and definitely not panic, infinitely recurse, etc)
565        //
566        // A -> B -> B
567        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
568        a.add_parent(EntityUID::with_eid("B"));
569        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
570        b.add_parent(EntityUID::with_eid("B"));
571        let mut entities = HashMap::from([(a.uid().clone(), a), (b.uid().clone(), b)]);
572        // computing TC should succeed without panicking, infinitely recursing, etc
573        compute_tc_internal(&mut entities);
574        // fails cycle check
575        match enforce_dag_from_tc(&entities) {
576            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
577            Err(TcError::HasCycle(err)) => {
578                assert!(err.vertex_with_loop() == &EntityUID::with_eid("B"));
579            }
580            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
581        }
582        let a = &entities[&EntityUID::with_eid("A")];
583        let b = &entities[&EntityUID::with_eid("B")];
584        // we check that the A -> B edge still exists
585        assert!(a.is_descendant_of(&EntityUID::with_eid("B")));
586        // but it shouldn't have added a B -> A edge
587        assert!(!b.is_descendant_of(&EntityUID::with_eid("A")));
588        // we also check that, whatever compute_tc_internal did with this invalid input, the
589        // final result still passes enforce_tc
590        assert!(enforce_tc(&entities).is_ok());
591        // still fails cycle check
592        match enforce_dag_from_tc(&entities) {
593            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
594            Err(TcError::HasCycle(err)) => {
595                assert!(err.vertex_with_loop() == &EntityUID::with_eid("B"));
596            }
597            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
598        }
599    }
600
601    #[test]
602    fn nontrivial_cycle() {
603        // this graph is invalid, but we want to still have some reasonable behavior
604        // if we encounter it (and definitely not panic, infinitely recurse, etc)
605        //
606        //          D
607        //        /
608        // A -> B -> C -> A
609        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
610        a.add_parent(EntityUID::with_eid("B"));
611        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
612        b.add_parent(EntityUID::with_eid("C"));
613        b.add_parent(EntityUID::with_eid("D"));
614        let mut c = Entity::with_uid(EntityUID::with_eid("C"));
615        c.add_parent(EntityUID::with_eid("A"));
616        let d = Entity::with_uid(EntityUID::with_eid("D"));
617        let mut entities = HashMap::from([
618            (a.uid().clone(), a),
619            (b.uid().clone(), b),
620            (c.uid().clone(), c),
621            (d.uid().clone(), d),
622        ]);
623        // computing TC should succeed without panicking, infinitely recursing, etc
624        compute_tc_internal(&mut entities);
625        // fails cycle check
626        match enforce_dag_from_tc(&entities) {
627            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
628            Err(TcError::HasCycle(err)) => {
629                assert!(
630                    err.vertex_with_loop() == &EntityUID::with_eid("A")
631                        || err.vertex_with_loop() == &EntityUID::with_eid("B")
632                        || err.vertex_with_loop() == &EntityUID::with_eid("C")
633                );
634            }
635            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
636        }
637        //TC tests
638        let a = &entities[&EntityUID::with_eid("A")];
639        let b = &entities[&EntityUID::with_eid("B")];
640        // we should have added A -> C and A -> D edges, at least
641        assert!(a.is_descendant_of(&EntityUID::with_eid("C")));
642        assert!(a.is_descendant_of(&EntityUID::with_eid("D")));
643        // and we should also have added a B -> A edge
644        assert!(b.is_descendant_of(&EntityUID::with_eid("A")));
645        // we also check that, whatever compute_tc_internal did with this invalid input, the
646        // final result still passes enforce_tc
647        assert!(enforce_tc(&entities).is_ok());
648        // still fails cycle check
649        match enforce_dag_from_tc(&entities) {
650            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
651            Err(TcError::HasCycle(err)) => {
652                assert!(
653                    err.vertex_with_loop() == &EntityUID::with_eid("A")
654                        || err.vertex_with_loop() == &EntityUID::with_eid("B")
655                        || err.vertex_with_loop() == &EntityUID::with_eid("C")
656                );
657            }
658            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
659        }
660    }
661
662    #[test]
663    fn disjoint_cycles() {
664        // graph with disconnected components including cycles:
665        //     B -> C -> B
666        //
667        // A      D -> E -> H -> D
668        //   \
669        //     F -> G
670        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
671        a.add_parent(EntityUID::with_eid("F"));
672        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
673        b.add_parent(EntityUID::with_eid("C"));
674        let mut c: Entity = Entity::with_uid(EntityUID::with_eid("C"));
675        c.add_parent(EntityUID::with_eid("B"));
676        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
677        d.add_parent(EntityUID::with_eid("E"));
678        let mut e = Entity::with_uid(EntityUID::with_eid("E"));
679        e.add_parent(EntityUID::with_eid("H"));
680        let mut f = Entity::with_uid(EntityUID::with_eid("F"));
681        f.add_parent(EntityUID::with_eid("G"));
682        let g = Entity::with_uid(EntityUID::with_eid("G"));
683        let mut h = Entity::with_uid(EntityUID::with_eid("H"));
684        h.add_parent(EntityUID::with_eid("D"));
685        let mut entities = HashMap::from([
686            (a.uid().clone(), a),
687            (b.uid().clone(), b),
688            (c.uid().clone(), c),
689            (d.uid().clone(), d),
690            (e.uid().clone(), e),
691            (f.uid().clone(), f),
692            (g.uid().clone(), g),
693            (h.uid().clone(), h),
694        ]);
695        // currently doesn't pass TC enforcement
696        assert!(enforce_tc(&entities).is_err());
697        // compute TC
698        compute_tc_internal(&mut entities);
699        // now it should pass TC enforcement
700        assert!(enforce_tc(&entities).is_ok());
701        // still fails cycle check
702        match enforce_dag_from_tc(&entities) {
703            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
704            Err(TcError::HasCycle(err)) => {
705                // two possible cycles
706                assert!(
707                    err.vertex_with_loop() == &EntityUID::with_eid("B")
708                        || err.vertex_with_loop() == &EntityUID::with_eid("C")
709                        || err.vertex_with_loop() == &EntityUID::with_eid("D")
710                        || err.vertex_with_loop() == &EntityUID::with_eid("E")
711                        || err.vertex_with_loop() == &EntityUID::with_eid("H")
712                );
713            }
714            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
715        }
716    }
717
718    #[test]
719    fn intersecting_cycles() {
720        // graph with two intersecting cycles:
721        //  A -> B -> C -> E -
722        //  ^    ^         ^  |
723        //  |    |         |  |
724        //  |    |        /   |
725        //   \  /        /    |
726        //    D ------>F      |
727        //    ^               |
728        //    |___------------
729        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
730        a.add_parent(EntityUID::with_eid("B"));
731        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
732        b.add_parent(EntityUID::with_eid("C"));
733        let mut c = Entity::with_uid(EntityUID::with_eid("C"));
734        c.add_parent(EntityUID::with_eid("E"));
735        let mut d = Entity::with_uid(EntityUID::with_eid("D"));
736        d.add_parent(EntityUID::with_eid("A"));
737        d.add_parent(EntityUID::with_eid("B"));
738        d.add_parent(EntityUID::with_eid("F"));
739        let mut e = Entity::with_uid(EntityUID::with_eid("E"));
740        e.add_parent(EntityUID::with_eid("D"));
741        let mut f = Entity::with_uid(EntityUID::with_eid("F"));
742        f.add_parent(EntityUID::with_eid("E"));
743        let mut entities = HashMap::from([
744            (a.uid().clone(), a),
745            (b.uid().clone(), b),
746            (c.uid().clone(), c),
747            (d.uid().clone(), d),
748            (e.uid().clone(), e),
749            (f.uid().clone(), f),
750        ]);
751        // fails TC enforcement
752        assert!(enforce_tc(&entities).is_err());
753        // compute TC
754        compute_tc_internal(&mut entities);
755        // now it should pass TC enforcement
756        assert!(enforce_tc(&entities).is_ok());
757        // but still fail cycle check
758        match enforce_dag_from_tc(&entities) {
759            Ok(_) => panic!("enforce_dag_from_tc should have returned an error"),
760            Err(TcError::HasCycle(_)) => (), // Every vertex is in a cycle
761            Err(_) => panic!("Unexpected error in enforce_dag_from_tc"),
762        }
763    }
764
765    #[test]
766    fn updated() {
767        // start with A -> B -> C
768        let mut a = Entity::with_uid(EntityUID::with_eid("A"));
769        a.add_parent(EntityUID::with_eid("B"));
770        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
771        b.add_parent(EntityUID::with_eid("C"));
772        let c = Entity::with_uid(EntityUID::with_eid("C"));
773        let mut entities = HashMap::from([
774            (a.uid().clone(), a),
775            (b.uid().clone(), b),
776            (c.uid().clone(), c),
777        ]);
778        // currently doesn't pass TC enforcement
779        assert!(enforce_tc(&entities).is_err());
780        // compute TC
781        compute_tc_internal(&mut entities);
782        let a = &entities[&EntityUID::with_eid("A")];
783        let b = &entities[&EntityUID::with_eid("B")];
784        let c = &entities[&EntityUID::with_eid("C")];
785        // should have added the A -> C edge
786        assert!(a.has_edge_to(&EntityUID::with_eid("C")));
787        // but we shouldn't have added other edges, like B -> A or C -> A
788        assert!(!b.has_edge_to(&EntityUID::with_eid("A")));
789        assert!(!c.has_edge_to(&EntityUID::with_eid("A")));
790        // now it should pass TC enforcement
791        assert!(enforce_tc(&entities).is_ok());
792        // passes cycle check after TC enforcement
793        assert!(enforce_dag_from_tc(&entities).is_ok());
794        // D doesn't exist yet
795        assert!(!a.has_edge_to(&EntityUID::with_eid("D")));
796
797        // change from B -> C to B -> D
798        let mut b = Entity::with_uid(EntityUID::with_eid("B"));
799        b.add_parent(EntityUID::with_eid("D"));
800        let d = Entity::with_uid(EntityUID::with_eid("D"));
801        entities.insert(b.uid().clone(), b);
802        entities.insert(d.uid().clone(), d);
803
804        // currently doesn't pass TC enforcement
805        assert!(enforce_tc(&entities).is_err());
806        // compute TC
807        compute_tc_internal(&mut entities);
808        let a = &entities[&EntityUID::with_eid("A")];
809        let b = &entities[&EntityUID::with_eid("B")];
810        let c = &entities[&EntityUID::with_eid("C")];
811        let d = &entities[&EntityUID::with_eid("D")];
812        // should have added the A -> D edge
813        assert!(a.has_edge_to(&EntityUID::with_eid("D")));
814        // should not have the A -> C edge
815        assert!(!a.has_edge_to(&EntityUID::with_eid("C")));
816        assert!(!b.has_edge_to(&EntityUID::with_eid("C")));
817        // but we shouldn't have added other edges, like B -> A or C -> A
818        assert!(!b.has_edge_to(&EntityUID::with_eid("A")));
819        assert!(!c.has_edge_to(&EntityUID::with_eid("A")));
820        assert!(!d.has_edge_to(&EntityUID::with_eid("A")));
821        // now it should pass TC enforcement
822        assert!(enforce_tc(&entities).is_ok());
823        // passes cycle check after TC enforcement
824        assert!(enforce_dag_from_tc(&entities).is_ok());
825    }
826}