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 handle: None,
140 id: id.to_string(),
141 title: format!("Unit {}", id),
142 status: Status::Open,
143 priority: 2,
144 parent: None,
145 dependencies: vec![],
146 labels: vec![],
147 assignee: None,
148 updated_at: Utc::now(),
149 produces: vec![],
150 requires: vec![],
151 has_verify: true,
152 verify: None,
153 created_at: Utc::now(),
154 claimed_by: None,
155 attempts: 0,
156 paths: vec![],
157 kind: crate::unit::UnitType::Task,
158 feature: false,
159 has_decisions: false,
160 }
161 }
162
163 fn make_index(entries: Vec<IndexEntry>) -> Index {
164 Index { units: entries }
165 }
166
167 #[test]
170 fn blocking_not_blocked_when_deps_closed() {
171 let mut dep = make_entry("1");
172 dep.status = Status::Closed;
173
174 let mut entry = make_entry("2");
175 entry.dependencies = vec!["1".into()];
176 entry.produces = vec!["Foo".into()];
177 entry.paths = vec!["src/foo.rs".into()];
178
179 let index = make_index(vec![dep, entry.clone()]);
180 assert_eq!(check_blocked(&entry, &index), None);
181 }
182
183 #[test]
184 fn blocking_waiting_on_open_dep() {
185 let dep = make_entry("1"); let mut entry = make_entry("2");
188 entry.dependencies = vec!["1".into()];
189 entry.produces = vec!["Foo".into()];
190 entry.paths = vec!["src/foo.rs".into()];
191
192 let index = make_index(vec![dep, entry.clone()]);
193 assert_eq!(
194 check_blocked(&entry, &index),
195 Some(BlockReason::WaitingOn(vec!["1".into()]))
196 );
197 }
198
199 #[test]
200 fn blocking_waiting_on_missing_dep() {
201 let mut entry = make_entry("2");
202 entry.dependencies = vec!["999".into()]; entry.produces = vec!["Foo".into()];
204 entry.paths = vec!["src/foo.rs".into()];
205
206 let index = make_index(vec![entry.clone()]);
207 assert_eq!(
208 check_blocked(&entry, &index),
209 Some(BlockReason::WaitingOn(vec!["999".into()]))
210 );
211 }
212
213 #[test]
214 fn blocking_waiting_on_multiple_deps() {
215 let dep_a = make_entry("1"); let dep_b = make_entry("3"); let mut entry = make_entry("2");
219 entry.dependencies = vec!["1".into(), "3".into()];
220 entry.produces = vec!["Foo".into()];
221 entry.paths = vec!["src/foo.rs".into()];
222
223 let index = make_index(vec![dep_a, entry.clone(), dep_b]);
224 assert_eq!(
225 check_blocked(&entry, &index),
226 Some(BlockReason::WaitingOn(vec!["1".into(), "3".into()]))
227 );
228 }
229
230 #[test]
233 fn blocking_waiting_on_sibling_producer() {
234 let mut producer = make_entry("5.1");
235 producer.parent = Some("5".into());
236 producer.produces = vec!["UserType".into()];
237
238 let mut consumer = make_entry("5.2");
239 consumer.parent = Some("5".into());
240 consumer.requires = vec!["UserType".into()];
241 consumer.produces = vec!["UserAPI".into()];
242 consumer.paths = vec!["src/api.rs".into()];
243
244 let index = make_index(vec![producer, consumer.clone()]);
245 assert_eq!(
246 check_blocked(&consumer, &index),
247 Some(BlockReason::WaitingOn(vec!["5.1".into()]))
248 );
249 }
250
251 #[test]
252 fn blocking_not_blocked_when_producer_closed() {
253 let mut producer = make_entry("5.1");
254 producer.parent = Some("5".into());
255 producer.produces = vec!["UserType".into()];
256 producer.status = Status::Closed;
257
258 let mut consumer = make_entry("5.2");
259 consumer.parent = Some("5".into());
260 consumer.requires = vec!["UserType".into()];
261 consumer.produces = vec!["UserAPI".into()];
262 consumer.paths = vec!["src/api.rs".into()];
263
264 let index = make_index(vec![producer, consumer.clone()]);
265 assert_eq!(check_blocked(&consumer, &index), None);
266 }
267
268 #[test]
269 fn blocking_no_duplicate_when_dep_and_requires_overlap() {
270 let mut producer = make_entry("5.1");
271 producer.parent = Some("5".into());
272 producer.produces = vec!["UserType".into()];
273
274 let mut consumer = make_entry("5.2");
275 consumer.parent = Some("5".into());
276 consumer.dependencies = vec!["5.1".into()]; consumer.requires = vec!["UserType".into()]; consumer.produces = vec!["UserAPI".into()];
279 consumer.paths = vec!["src/api.rs".into()];
280
281 let index = make_index(vec![producer, consumer.clone()]);
282 if let Some(BlockReason::WaitingOn(ids)) = check_blocked(&consumer, &index) {
283 assert_eq!(ids, vec!["5.1".to_string()]);
285 } else {
286 panic!("Expected WaitingOn");
287 }
288 }
289
290 #[test]
293 fn warning_oversized_too_many_produces() {
294 let mut entry = make_entry("1");
295 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["src/a.rs".into()];
297
298 let index = make_index(vec![entry.clone()]);
300 assert_eq!(check_blocked(&entry, &index), None);
301 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
302 }
303
304 #[test]
305 fn warning_oversized_too_many_paths() {
306 let mut entry = make_entry("1");
307 entry.produces = vec!["A".into()];
308 entry.paths = vec![
309 "a.rs".into(),
310 "b.rs".into(),
311 "c.rs".into(),
312 "d.rs".into(),
313 "e.rs".into(),
314 "f.rs".into(),
315 ]; let index = make_index(vec![entry.clone()]);
318 assert_eq!(check_blocked(&entry, &index), None);
319 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
320 }
321
322 #[test]
323 fn warning_not_oversized_at_threshold() {
324 let mut entry = make_entry("1");
325 entry.produces = vec!["A".into(), "B".into(), "C".into()]; entry.paths = vec![
327 "a.rs".into(),
328 "b.rs".into(),
329 "c.rs".into(),
330 "d.rs".into(),
331 "e.rs".into(),
332 ]; assert_eq!(check_scope_warning(&entry), None);
335 }
336
337 #[test]
340 fn unscoped_unit_is_not_blocked() {
341 let entry = make_entry("1"); let index = make_index(vec![entry.clone()]);
344 assert_eq!(check_blocked(&entry, &index), None);
345 }
346
347 #[test]
348 fn not_blocked_with_produces_only() {
349 let mut entry = make_entry("1");
350 entry.produces = vec!["SomeType".into()];
351
352 let index = make_index(vec![entry.clone()]);
353 assert_eq!(check_blocked(&entry, &index), None);
354 }
355
356 #[test]
357 fn not_blocked_with_paths_only() {
358 let mut entry = make_entry("1");
359 entry.paths = vec!["src/main.rs".into()];
360
361 let index = make_index(vec![entry.clone()]);
362 assert_eq!(check_blocked(&entry, &index), None);
363 }
364
365 #[test]
368 fn blocking_display_waiting_on() {
369 let reason = BlockReason::WaitingOn(vec!["3.1".into(), "3.2".into()]);
370 assert_eq!(format!("{}", reason), "waiting on 3.1, 3.2");
371 }
372
373 #[test]
374 fn warning_display_oversized() {
375 assert_eq!(format!("{}", ScopeWarning::Oversized), "oversized");
376 }
377
378 #[test]
381 fn blocking_deps_still_block_oversized_units() {
382 let dep = make_entry("1"); let mut entry = make_entry("2");
385 entry.dependencies = vec!["1".into()];
386 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["a.rs".into()];
388
389 let index = make_index(vec![dep, entry.clone()]);
390 assert!(matches!(
391 check_blocked(&entry, &index),
392 Some(BlockReason::WaitingOn(_))
393 ));
394 }
395
396 #[test]
397 fn blocking_deps_still_block_unscoped_units() {
398 let dep = make_entry("1"); let mut entry = make_entry("2");
401 entry.dependencies = vec!["1".into()];
402 let index = make_index(vec![dep, entry.clone()]);
405 assert!(matches!(
406 check_blocked(&entry, &index),
407 Some(BlockReason::WaitingOn(_))
408 ));
409 }
410}