1use std::fmt;
2
3use crate::bean::Status;
4use crate::index::{Index, IndexEntry};
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> {
63 let mut waiting_on = Vec::new();
64
65 for dep_id in &entry.dependencies {
67 match index.beans.iter().find(|e| e.id == *dep_id) {
68 Some(dep) if dep.status == Status::Closed => {}
69 _ => waiting_on.push(dep_id.clone()),
70 }
71 }
72
73 for required in &entry.requires {
75 if let Some(producer) = index
76 .beans
77 .iter()
78 .find(|e| e.id != entry.id && e.parent == entry.parent && e.produces.contains(required))
79 {
80 if producer.status != Status::Closed && !waiting_on.contains(&producer.id) {
81 waiting_on.push(producer.id.clone());
82 }
83 }
84 }
85
86 if !waiting_on.is_empty() {
87 return Some(BlockReason::WaitingOn(waiting_on));
88 }
89
90 None
91}
92
93pub fn check_scope_warning(entry: &IndexEntry) -> Option<ScopeWarning> {
98 if entry.produces.len() > MAX_PRODUCES || entry.paths.len() > MAX_PATHS {
99 return Some(ScopeWarning::Oversized);
100 }
101 None
102}
103
104#[cfg(test)]
109mod tests {
110 use super::*;
111 use chrono::Utc;
112
113 fn make_entry(id: &str) -> IndexEntry {
114 IndexEntry {
115 id: id.to_string(),
116 title: format!("Bean {}", id),
117 status: Status::Open,
118 priority: 2,
119 parent: None,
120 dependencies: vec![],
121 labels: vec![],
122 assignee: None,
123 updated_at: Utc::now(),
124 produces: vec![],
125 requires: vec![],
126 has_verify: true,
127 claimed_by: None,
128 attempts: 0,
129 paths: vec![],
130 }
131 }
132
133 fn make_index(entries: Vec<IndexEntry>) -> Index {
134 Index { beans: entries }
135 }
136
137 #[test]
140 fn blocking_not_blocked_when_deps_closed() {
141 let mut dep = make_entry("1");
142 dep.status = Status::Closed;
143
144 let mut entry = make_entry("2");
145 entry.dependencies = vec!["1".into()];
146 entry.produces = vec!["Foo".into()];
147 entry.paths = vec!["src/foo.rs".into()];
148
149 let index = make_index(vec![dep, entry.clone()]);
150 assert_eq!(check_blocked(&entry, &index), None);
151 }
152
153 #[test]
154 fn blocking_waiting_on_open_dep() {
155 let dep = make_entry("1"); let mut entry = make_entry("2");
158 entry.dependencies = vec!["1".into()];
159 entry.produces = vec!["Foo".into()];
160 entry.paths = vec!["src/foo.rs".into()];
161
162 let index = make_index(vec![dep, entry.clone()]);
163 assert_eq!(
164 check_blocked(&entry, &index),
165 Some(BlockReason::WaitingOn(vec!["1".into()]))
166 );
167 }
168
169 #[test]
170 fn blocking_waiting_on_missing_dep() {
171 let mut entry = make_entry("2");
172 entry.dependencies = vec!["999".into()]; entry.produces = vec!["Foo".into()];
174 entry.paths = vec!["src/foo.rs".into()];
175
176 let index = make_index(vec![entry.clone()]);
177 assert_eq!(
178 check_blocked(&entry, &index),
179 Some(BlockReason::WaitingOn(vec!["999".into()]))
180 );
181 }
182
183 #[test]
184 fn blocking_waiting_on_multiple_deps() {
185 let dep_a = make_entry("1"); let dep_b = make_entry("3"); let mut entry = make_entry("2");
189 entry.dependencies = vec!["1".into(), "3".into()];
190 entry.produces = vec!["Foo".into()];
191 entry.paths = vec!["src/foo.rs".into()];
192
193 let index = make_index(vec![dep_a, entry.clone(), dep_b]);
194 assert_eq!(
195 check_blocked(&entry, &index),
196 Some(BlockReason::WaitingOn(vec!["1".into(), "3".into()]))
197 );
198 }
199
200 #[test]
203 fn blocking_waiting_on_sibling_producer() {
204 let mut producer = make_entry("5.1");
205 producer.parent = Some("5".into());
206 producer.produces = vec!["UserType".into()];
207
208 let mut consumer = make_entry("5.2");
209 consumer.parent = Some("5".into());
210 consumer.requires = vec!["UserType".into()];
211 consumer.produces = vec!["UserAPI".into()];
212 consumer.paths = vec!["src/api.rs".into()];
213
214 let index = make_index(vec![producer, consumer.clone()]);
215 assert_eq!(
216 check_blocked(&consumer, &index),
217 Some(BlockReason::WaitingOn(vec!["5.1".into()]))
218 );
219 }
220
221 #[test]
222 fn blocking_not_blocked_when_producer_closed() {
223 let mut producer = make_entry("5.1");
224 producer.parent = Some("5".into());
225 producer.produces = vec!["UserType".into()];
226 producer.status = Status::Closed;
227
228 let mut consumer = make_entry("5.2");
229 consumer.parent = Some("5".into());
230 consumer.requires = vec!["UserType".into()];
231 consumer.produces = vec!["UserAPI".into()];
232 consumer.paths = vec!["src/api.rs".into()];
233
234 let index = make_index(vec![producer, consumer.clone()]);
235 assert_eq!(check_blocked(&consumer, &index), None);
236 }
237
238 #[test]
239 fn blocking_no_duplicate_when_dep_and_requires_overlap() {
240 let mut producer = make_entry("5.1");
241 producer.parent = Some("5".into());
242 producer.produces = vec!["UserType".into()];
243
244 let mut consumer = make_entry("5.2");
245 consumer.parent = Some("5".into());
246 consumer.dependencies = vec!["5.1".into()]; consumer.requires = vec!["UserType".into()]; consumer.produces = vec!["UserAPI".into()];
249 consumer.paths = vec!["src/api.rs".into()];
250
251 let index = make_index(vec![producer, consumer.clone()]);
252 if let Some(BlockReason::WaitingOn(ids)) = check_blocked(&consumer, &index) {
253 assert_eq!(ids, vec!["5.1".to_string()]);
255 } else {
256 panic!("Expected WaitingOn");
257 }
258 }
259
260 #[test]
263 fn warning_oversized_too_many_produces() {
264 let mut entry = make_entry("1");
265 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["src/a.rs".into()];
267
268 let index = make_index(vec![entry.clone()]);
270 assert_eq!(check_blocked(&entry, &index), None);
271 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
272 }
273
274 #[test]
275 fn warning_oversized_too_many_paths() {
276 let mut entry = make_entry("1");
277 entry.produces = vec!["A".into()];
278 entry.paths = vec![
279 "a.rs".into(),
280 "b.rs".into(),
281 "c.rs".into(),
282 "d.rs".into(),
283 "e.rs".into(),
284 "f.rs".into(),
285 ]; let index = make_index(vec![entry.clone()]);
288 assert_eq!(check_blocked(&entry, &index), None);
289 assert_eq!(check_scope_warning(&entry), Some(ScopeWarning::Oversized));
290 }
291
292 #[test]
293 fn warning_not_oversized_at_threshold() {
294 let mut entry = make_entry("1");
295 entry.produces = vec!["A".into(), "B".into(), "C".into()]; entry.paths = vec![
297 "a.rs".into(),
298 "b.rs".into(),
299 "c.rs".into(),
300 "d.rs".into(),
301 "e.rs".into(),
302 ]; assert_eq!(check_scope_warning(&entry), None);
305 }
306
307 #[test]
310 fn unscoped_bean_is_not_blocked() {
311 let entry = make_entry("1"); let index = make_index(vec![entry.clone()]);
314 assert_eq!(check_blocked(&entry, &index), None);
315 }
316
317 #[test]
318 fn not_blocked_with_produces_only() {
319 let mut entry = make_entry("1");
320 entry.produces = vec!["SomeType".into()];
321
322 let index = make_index(vec![entry.clone()]);
323 assert_eq!(check_blocked(&entry, &index), None);
324 }
325
326 #[test]
327 fn not_blocked_with_paths_only() {
328 let mut entry = make_entry("1");
329 entry.paths = vec!["src/main.rs".into()];
330
331 let index = make_index(vec![entry.clone()]);
332 assert_eq!(check_blocked(&entry, &index), None);
333 }
334
335 #[test]
338 fn blocking_display_waiting_on() {
339 let reason = BlockReason::WaitingOn(vec!["3.1".into(), "3.2".into()]);
340 assert_eq!(format!("{}", reason), "waiting on 3.1, 3.2");
341 }
342
343 #[test]
344 fn warning_display_oversized() {
345 assert_eq!(format!("{}", ScopeWarning::Oversized), "oversized");
346 }
347
348 #[test]
351 fn blocking_deps_still_block_oversized_beans() {
352 let dep = make_entry("1"); let mut entry = make_entry("2");
355 entry.dependencies = vec!["1".into()];
356 entry.produces = vec!["A".into(), "B".into(), "C".into(), "D".into()]; entry.paths = vec!["a.rs".into()];
358
359 let index = make_index(vec![dep, entry.clone()]);
360 assert!(matches!(
361 check_blocked(&entry, &index),
362 Some(BlockReason::WaitingOn(_))
363 ));
364 }
365
366 #[test]
367 fn blocking_deps_still_block_unscoped_beans() {
368 let dep = make_entry("1"); let mut entry = make_entry("2");
371 entry.dependencies = vec!["1".into()];
372 let index = make_index(vec![dep, entry.clone()]);
375 assert!(matches!(
376 check_blocked(&entry, &index),
377 Some(BlockReason::WaitingOn(_))
378 ));
379 }
380}