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