1use std::fmt;
2
3use crate::index::{ArchiveIndex, Index, IndexEntry};
4use crate::unit::Status;
5
6pub const MAX_PRODUCES: usize = 3;
12
13pub const MAX_PATHS: usize = 5;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum BlockReason {
23 WaitingOn(Vec<String>),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ScopeWarning {
30 Oversized,
32}
33
34impl fmt::Display for BlockReason {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 BlockReason::WaitingOn(ids) => {
38 write!(f, "waiting on {}", ids.join(", "))
39 }
40 }
41 }
42}
43
44impl fmt::Display for ScopeWarning {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 ScopeWarning::Oversized => write!(f, "oversized"),
48 }
49 }
50}
51
52pub fn check_blocked(entry: &IndexEntry, index: &Index) -> Option<BlockReason> {
67 check_blocked_with_archive(entry, index, None)
68}
69
70pub fn check_blocked_with_archive(
73 entry: &IndexEntry,
74 index: &Index,
75 archive: Option<&ArchiveIndex>,
76) -> Option<BlockReason> {
77 let mut waiting_on = Vec::new();
78
79 for dep_id in &entry.dependencies {
81 match index.units.iter().find(|e| e.id == *dep_id) {
82 Some(dep) if dep.status == Status::Closed => {}
83 Some(_) => waiting_on.push(dep_id.clone()), None => {
85 let in_archive = archive
87 .map(|a| a.units.iter().any(|e| e.id == *dep_id))
88 .unwrap_or(false);
89 if !in_archive {
90 waiting_on.push(dep_id.clone());
91 }
92 }
93 }
94 }
95
96 for required in &entry.requires {
98 if let Some(producer) = index
99 .units
100 .iter()
101 .find(|e| e.id != entry.id && e.parent == entry.parent && e.produces.contains(required))
102 {
103 if producer.status != Status::Closed && !waiting_on.contains(&producer.id) {
104 waiting_on.push(producer.id.clone());
105 }
106 }
107 }
109
110 if !waiting_on.is_empty() {
111 return Some(BlockReason::WaitingOn(waiting_on));
112 }
113
114 None
115}
116
117pub fn check_scope_warning(entry: &IndexEntry) -> Option<ScopeWarning> {
122 if entry.produces.len() > MAX_PRODUCES || entry.paths.len() > MAX_PATHS {
123 return Some(ScopeWarning::Oversized);
124 }
125 None
126}
127
128#[cfg(test)]
133mod tests {
134 use super::*;
135 use chrono::Utc;
136
137 fn make_entry(id: &str) -> IndexEntry {
138 IndexEntry {
139 id: id.to_string(),
140 title: format!("Unit {}", id),
141 status: Status::Open,
142 priority: 2,
143 parent: None,
144 dependencies: vec![],
145 labels: vec![],
146 assignee: None,
147 updated_at: Utc::now(),
148 produces: vec![],
149 requires: vec![],
150 has_verify: true,
151 verify: None,
152 created_at: Utc::now(),
153 claimed_by: None,
154 attempts: 0,
155 paths: vec![],
156 feature: false,
157 has_decisions: false,
158 }
159 }
160
161 fn make_index(entries: Vec<IndexEntry>) -> Index {
162 Index { units: entries }
163 }
164
165 #[test]
168 fn blocking_not_blocked_when_deps_closed() {
169 let mut dep = make_entry("1");
170 dep.status = Status::Closed;
171
172 let mut entry = make_entry("2");
173 entry.dependencies = vec!["1".into()];
174 entry.produces = vec!["Foo".into()];
175 entry.paths = vec!["src/foo.rs".into()];
176
177 let index = make_index(vec![dep, entry.clone()]);
178 assert_eq!(check_blocked(&entry, &index), None);
179 }
180
181 #[test]
182 fn blocking_waiting_on_open_dep() {
183 let dep = make_entry("1"); let mut entry = make_entry("2");
186 entry.dependencies = vec!["1".into()];
187 entry.produces = vec!["Foo".into()];
188 entry.paths = vec!["src/foo.rs".into()];
189
190 let index = make_index(vec![dep, entry.clone()]);
191 assert_eq!(
192 check_blocked(&entry, &index),
193 Some(BlockReason::WaitingOn(vec!["1".into()]))
194 );
195 }
196
197 #[test]
198 fn blocking_waiting_on_missing_dep() {
199 let mut entry = make_entry("2");
200 entry.dependencies = vec!["999".into()]; entry.produces = vec!["Foo".into()];
202 entry.paths = vec!["src/foo.rs".into()];
203
204 let index = make_index(vec![entry.clone()]);
205 assert_eq!(
206 check_blocked(&entry, &index),
207 Some(BlockReason::WaitingOn(vec!["999".into()]))
208 );
209 }
210
211 #[test]
212 fn blocking_waiting_on_multiple_deps() {
213 let dep_a = make_entry("1"); let dep_b = make_entry("3"); let mut entry = make_entry("2");
217 entry.dependencies = vec!["1".into(), "3".into()];
218 entry.produces = vec!["Foo".into()];
219 entry.paths = vec!["src/foo.rs".into()];
220
221 let index = make_index(vec![dep_a, entry.clone(), dep_b]);
222 assert_eq!(
223 check_blocked(&entry, &index),
224 Some(BlockReason::WaitingOn(vec!["1".into(), "3".into()]))
225 );
226 }
227
228 #[test]
231 fn blocking_waiting_on_sibling_producer() {
232 let mut producer = make_entry("5.1");
233 producer.parent = Some("5".into());
234 producer.produces = vec!["UserType".into()];
235
236 let mut consumer = make_entry("5.2");
237 consumer.parent = Some("5".into());
238 consumer.requires = vec!["UserType".into()];
239 consumer.produces = vec!["UserAPI".into()];
240 consumer.paths = vec!["src/api.rs".into()];
241
242 let index = make_index(vec![producer, consumer.clone()]);
243 assert_eq!(
244 check_blocked(&consumer, &index),
245 Some(BlockReason::WaitingOn(vec!["5.1".into()]))
246 );
247 }
248
249 #[test]
250 fn blocking_not_blocked_when_producer_closed() {
251 let mut producer = make_entry("5.1");
252 producer.parent = Some("5".into());
253 producer.produces = vec!["UserType".into()];
254 producer.status = Status::Closed;
255
256 let mut consumer = make_entry("5.2");
257 consumer.parent = Some("5".into());
258 consumer.requires = vec!["UserType".into()];
259 consumer.produces = vec!["UserAPI".into()];
260 consumer.paths = vec!["src/api.rs".into()];
261
262 let index = make_index(vec![producer, consumer.clone()]);
263 assert_eq!(check_blocked(&consumer, &index), None);
264 }
265
266 #[test]
267 fn blocking_no_duplicate_when_dep_and_requires_overlap() {
268 let mut producer = make_entry("5.1");
269 producer.parent = Some("5".into());
270 producer.produces = vec!["UserType".into()];
271
272 let mut consumer = make_entry("5.2");
273 consumer.parent = Some("5".into());
274 consumer.dependencies = vec!["5.1".into()]; consumer.requires = vec!["UserType".into()]; consumer.produces = vec!["UserAPI".into()];
277 consumer.paths = vec!["src/api.rs".into()];
278
279 let index = make_index(vec![producer, consumer.clone()]);
280 if let Some(BlockReason::WaitingOn(ids)) = check_blocked(&consumer, &index) {
281 assert_eq!(ids, vec!["5.1".to_string()]);
283 } else {
284 panic!("Expected WaitingOn");
285 }
286 }
287
288 #[test]
291 fn warning_oversized_too_many_produces() {
292 let mut entry = make_entry("1");
293 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["src/a.rs".into()];
295
296 let index = make_index(vec![entry.clone()]);
298 assert_eq!(check_blocked(&entry, &index), None);
299 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
300 }
301
302 #[test]
303 fn warning_oversized_too_many_paths() {
304 let mut entry = make_entry("1");
305 entry.produces = vec!["A".into()];
306 entry.paths = vec![
307 "a.rs".into(),
308 "b.rs".into(),
309 "c.rs".into(),
310 "d.rs".into(),
311 "e.rs".into(),
312 "f.rs".into(),
313 ]; let index = make_index(vec![entry.clone()]);
316 assert_eq!(check_blocked(&entry, &index), None);
317 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
318 }
319
320 #[test]
321 fn warning_not_oversized_at_threshold() {
322 let mut entry = make_entry("1");
323 entry.produces = vec!["A".into(), "B".into(), "C".into()]; entry.paths = vec![
325 "a.rs".into(),
326 "b.rs".into(),
327 "c.rs".into(),
328 "d.rs".into(),
329 "e.rs".into(),
330 ]; assert_eq!(check_scope_warning(&entry), None);
333 }
334
335 #[test]
338 fn unscoped_unit_is_not_blocked() {
339 let entry = make_entry("1"); let index = make_index(vec![entry.clone()]);
342 assert_eq!(check_blocked(&entry, &index), None);
343 }
344
345 #[test]
346 fn not_blocked_with_produces_only() {
347 let mut entry = make_entry("1");
348 entry.produces = vec!["SomeType".into()];
349
350 let index = make_index(vec![entry.clone()]);
351 assert_eq!(check_blocked(&entry, &index), None);
352 }
353
354 #[test]
355 fn not_blocked_with_paths_only() {
356 let mut entry = make_entry("1");
357 entry.paths = vec!["src/main.rs".into()];
358
359 let index = make_index(vec![entry.clone()]);
360 assert_eq!(check_blocked(&entry, &index), None);
361 }
362
363 #[test]
366 fn blocking_display_waiting_on() {
367 let reason = BlockReason::WaitingOn(vec!["3.1".into(), "3.2".into()]);
368 assert_eq!(format!("{}", reason), "waiting on 3.1, 3.2");
369 }
370
371 #[test]
372 fn warning_display_oversized() {
373 assert_eq!(format!("{}", ScopeWarning::Oversized), "oversized");
374 }
375
376 #[test]
379 fn blocking_deps_still_block_oversized_units() {
380 let dep = make_entry("1"); let mut entry = make_entry("2");
383 entry.dependencies = vec!["1".into()];
384 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["a.rs".into()];
386
387 let index = make_index(vec![dep, entry.clone()]);
388 assert!(matches!(
389 check_blocked(&entry, &index),
390 Some(BlockReason::WaitingOn(_))
391 ));
392 }
393
394 #[test]
395 fn blocking_deps_still_block_unscoped_units() {
396 let dep = make_entry("1"); let mut entry = make_entry("2");
399 entry.dependencies = vec!["1".into()];
400 let index = make_index(vec![dep, entry.clone()]);
403 assert!(matches!(
404 check_blocked(&entry, &index),
405 Some(BlockReason::WaitingOn(_))
406 ));
407 }
408}