1use super::{BacklogItem, ItemId};
4use std::collections::{HashMap, HashSet};
5
6#[must_use]
12pub fn parent_creates_cycle(items: &[BacklogItem], child: &ItemId, parent: &ItemId) -> bool {
13 if child == parent {
14 return true;
15 }
16 let parents: HashMap<&ItemId, &ItemId> = items
17 .iter()
18 .filter_map(|it| it.parent.as_ref().map(|p| (&it.id, p)))
19 .collect();
20 let mut current = Some(parent);
21 let mut seen = HashSet::new();
22 while let Some(id) = current {
23 if id == child {
24 return true;
25 }
26 if !seen.insert(id) {
27 break;
28 }
29 current = parents.get(id).copied();
30 }
31 false
32}
33
34#[must_use]
40pub fn dependency_creates_cycle(items: &[BacklogItem], item: &ItemId, dep: &ItemId) -> bool {
41 if item == dep {
42 return true;
43 }
44 let deps: HashMap<&ItemId, &[ItemId]> = items
45 .iter()
46 .map(|it| (&it.id, it.depends_on.as_slice()))
47 .collect();
48 let mut stack = vec![dep];
49 let mut seen = HashSet::new();
50 while let Some(id) = stack.pop() {
51 if id == item {
52 return true;
53 }
54 if !seen.insert(id) {
55 continue;
56 }
57 if let Some(edges) = deps.get(id) {
58 stack.extend(edges.iter());
59 }
60 }
61 false
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::backlog::Status;
68 use crate::rank::Rank;
69 use chrono::DateTime;
70
71 fn item(n: u32) -> BacklogItem {
73 BacklogItem::new(
74 ItemId::new("T", n),
75 format!("Item {n}"),
76 Status::new("todo"),
77 Rank::between(None, None).expect("open bounds produce a rank"),
78 DateTime::from_timestamp(0, 0).expect("valid epoch"),
79 )
80 .expect("valid item")
81 }
82
83 fn item_with_parent(n: u32, parent: u32) -> BacklogItem {
85 let mut it = item(n);
86 it.parent = Some(ItemId::new("T", parent));
87 it
88 }
89
90 fn item_with_deps(n: u32, deps: &[u32]) -> BacklogItem {
92 let mut it = item(n);
93 it.depends_on = deps.iter().map(|&d| ItemId::new("T", d)).collect();
94 it
95 }
96
97 #[test]
100 fn parent_self_reference_is_a_cycle() {
101 let items = [item(1)];
102 assert!(parent_creates_cycle(
103 &items,
104 &ItemId::new("T", 1),
105 &ItemId::new("T", 1)
106 ));
107 }
108
109 #[test]
110 fn parent_to_unrelated_item_is_acyclic() {
111 let items = [item(1), item(2)];
113 assert!(!parent_creates_cycle(
114 &items,
115 &ItemId::new("T", 1),
116 &ItemId::new("T", 2)
117 ));
118 }
119
120 #[test]
121 fn parent_to_descendant_is_a_cycle() {
122 let items = [item(1), item_with_parent(2, 1)];
124 assert!(parent_creates_cycle(
125 &items,
126 &ItemId::new("T", 1),
127 &ItemId::new("T", 2)
128 ));
129 }
130
131 #[test]
132 fn parent_to_transitive_descendant_is_a_cycle() {
133 let items = [item(1), item_with_parent(2, 1), item_with_parent(3, 2)];
135 assert!(parent_creates_cycle(
136 &items,
137 &ItemId::new("T", 1),
138 &ItemId::new("T", 3)
139 ));
140 }
141
142 #[test]
143 fn parent_to_sibling_subtree_is_acyclic() {
144 let items = [item(1), item_with_parent(2, 1), item_with_parent(3, 1)];
146 assert!(!parent_creates_cycle(
147 &items,
148 &ItemId::new("T", 3),
149 &ItemId::new("T", 2)
150 ));
151 }
152
153 #[test]
156 fn dependency_self_reference_is_a_cycle() {
157 let items = [item(1)];
158 assert!(dependency_creates_cycle(
159 &items,
160 &ItemId::new("T", 1),
161 &ItemId::new("T", 1)
162 ));
163 }
164
165 #[test]
166 fn dependency_on_independent_item_is_acyclic() {
167 let items = [item(1), item(2)];
168 assert!(!dependency_creates_cycle(
169 &items,
170 &ItemId::new("T", 1),
171 &ItemId::new("T", 2)
172 ));
173 }
174
175 #[test]
176 fn dependency_back_edge_is_a_cycle() {
177 let items = [item(1), item_with_deps(2, &[1])];
179 assert!(dependency_creates_cycle(
180 &items,
181 &ItemId::new("T", 1),
182 &ItemId::new("T", 2)
183 ));
184 }
185
186 #[test]
187 fn dependency_transitive_back_edge_is_a_cycle() {
188 let items = [item(1), item_with_deps(2, &[1]), item_with_deps(3, &[2])];
190 assert!(dependency_creates_cycle(
191 &items,
192 &ItemId::new("T", 1),
193 &ItemId::new("T", 3)
194 ));
195 }
196
197 #[test]
198 fn dependency_shared_diamond_is_acyclic() {
199 let items = [
201 item(1),
202 item_with_deps(2, &[1]),
203 item_with_deps(3, &[1]),
204 item_with_deps(4, &[2, 3]),
205 ];
206 assert!(!dependency_creates_cycle(
207 &items,
208 &ItemId::new("T", 4),
209 &ItemId::new("T", 1)
210 ));
211 }
212}