Skip to main content

shipper_core/plan/chunking/
mod.rs

1//! Chunking helpers for bounded-size parallel work batches.
2//!
3//! This crate isolates the "split work list by max concurrency" concern from the
4//! parallel publish engine so it can be validated and fuzzed independently.
5
6/// Split a list of items into contiguous chunks bounded by `max_concurrent`.
7///
8/// - `max_concurrent <= 0` is treated as `1`.
9/// - Empty input returns an empty list of chunks.
10/// - Item order is preserved across chunks.
11///
12/// # Examples
13///
14/// ```ignore
15/// use shipper::plan::chunking::chunk_by_max_concurrent;
16///
17/// let items = vec!["a", "b", "c", "d", "e"];
18/// let chunks = chunk_by_max_concurrent(&items, 2);
19/// assert_eq!(chunks, vec![vec!["a", "b"], vec!["c", "d"], vec!["e"]]);
20///
21/// // Empty input returns no chunks
22/// let empty: Vec<i32> = vec![];
23/// assert!(chunk_by_max_concurrent(&empty, 3).is_empty());
24/// ```
25pub fn chunk_by_max_concurrent<T: Clone>(items: &[T], max_concurrent: usize) -> Vec<Vec<T>> {
26    let batch_size = max_concurrent.max(1);
27    if items.is_empty() {
28        return Vec::new();
29    }
30
31    let mut chunks = Vec::new();
32    let mut index = 0usize;
33
34    while index < items.len() {
35        let next = (index + batch_size).min(items.len());
36        chunks.push(items[index..next].to_vec());
37        index = next;
38    }
39
40    chunks
41}
42
43#[cfg(test)]
44mod tests {
45    use super::chunk_by_max_concurrent;
46
47    #[test]
48    fn chunking_empty_input_returns_no_batches() {
49        let items: Vec<String> = vec![];
50        let chunks = chunk_by_max_concurrent(&items, 4);
51        assert!(chunks.is_empty());
52    }
53
54    #[test]
55    fn chunking_respects_max_concurrent() {
56        let items = vec!["a", "b", "c", "d", "e"];
57        let chunks = chunk_by_max_concurrent(&items, 2);
58
59        assert_eq!(chunks.len(), 3);
60        assert_eq!(chunks[0], vec!["a", "b"]);
61        assert_eq!(chunks[1], vec!["c", "d"]);
62        assert_eq!(chunks[2], vec!["e"]);
63    }
64
65    #[test]
66    fn chunking_preserves_order_and_total_items() {
67        let items = vec![1, 2, 3, 4, 5];
68        let chunks = chunk_by_max_concurrent(&items, 10);
69        let flattened: Vec<i32> = chunks
70            .iter()
71            .flat_map(|chunk| chunk.iter().cloned())
72            .collect();
73
74        assert_eq!(flattened, items);
75        assert_eq!(chunks.len(), 1);
76    }
77
78    #[test]
79    fn chunking_max_concurrent_zero_treated_as_one() {
80        let items = vec!["a", "b", "c"];
81        let chunks = chunk_by_max_concurrent(&items, 0);
82        assert_eq!(chunks.len(), 3);
83        assert_eq!(chunks[0], vec!["a"]);
84        assert_eq!(chunks[1], vec!["b"]);
85        assert_eq!(chunks[2], vec!["c"]);
86    }
87
88    #[test]
89    fn chunking_max_concurrent_usize_max() {
90        let items = vec!["x", "y", "z"];
91        let chunks = chunk_by_max_concurrent(&items, usize::MAX);
92        assert_eq!(chunks.len(), 1);
93        assert_eq!(chunks[0], vec!["x", "y", "z"]);
94    }
95
96    #[test]
97    fn chunking_single_item_with_max_concurrent_one() {
98        let items = vec!["only"];
99        let chunks = chunk_by_max_concurrent(&items, 1);
100        assert_eq!(chunks.len(), 1);
101        assert_eq!(chunks[0], vec!["only"]);
102    }
103
104    #[test]
105    fn chunking_large_list_100_items_by_7() {
106        let items: Vec<i32> = (0..100).collect();
107        let chunks = chunk_by_max_concurrent(&items, 7);
108
109        // 100 / 7 = 14 full chunks of 7 + 1 chunk of 2 = 15 chunks
110        assert_eq!(chunks.len(), 15);
111        for chunk in &chunks[..14] {
112            assert_eq!(chunk.len(), 7);
113        }
114        assert_eq!(chunks[14].len(), 2);
115
116        let flattened: Vec<i32> = chunks.into_iter().flatten().collect();
117        assert_eq!(flattened, items);
118    }
119
120    #[test]
121    fn chunking_with_integer_types() {
122        let items: Vec<u64> = vec![10, 20, 30, 40, 50, 60];
123        let chunks = chunk_by_max_concurrent(&items, 4);
124        assert_eq!(chunks.len(), 2);
125        assert_eq!(chunks[0], vec![10, 20, 30, 40]);
126        assert_eq!(chunks[1], vec![50, 60]);
127    }
128
129    // --- Edge-case: empty input variants ---
130
131    #[test]
132    fn empty_i32_slice_returns_empty_chunks() {
133        let items: &[i32] = &[];
134        let chunks = chunk_by_max_concurrent(items, 1);
135        assert!(chunks.is_empty());
136    }
137
138    #[test]
139    fn empty_input_with_large_max_concurrent() {
140        let items: Vec<&str> = vec![];
141        let chunks = chunk_by_max_concurrent(&items, 1000);
142        assert!(chunks.is_empty());
143    }
144
145    // --- Edge-case: single item ---
146
147    #[test]
148    fn single_item_with_large_max_concurrent() {
149        let items = vec![42];
150        let chunks = chunk_by_max_concurrent(&items, 100);
151        assert_eq!(chunks.len(), 1);
152        assert_eq!(chunks[0], vec![42]);
153    }
154
155    #[test]
156    fn single_item_with_max_concurrent_zero() {
157        let items = vec!["alone"];
158        let chunks = chunk_by_max_concurrent(&items, 0);
159        assert_eq!(chunks.len(), 1);
160        assert_eq!(chunks[0], vec!["alone"]);
161    }
162
163    // --- Edge-case: chunk size larger than input ---
164
165    #[test]
166    fn chunk_size_much_larger_than_input() {
167        let items = vec![1, 2, 3];
168        let chunks = chunk_by_max_concurrent(&items, 999);
169        assert_eq!(chunks.len(), 1);
170        assert_eq!(chunks[0], vec![1, 2, 3]);
171    }
172
173    #[test]
174    fn chunk_size_exactly_input_length() {
175        let items = vec!["a", "b", "c", "d"];
176        let chunks = chunk_by_max_concurrent(&items, 4);
177        assert_eq!(chunks.len(), 1);
178        assert_eq!(chunks[0], vec!["a", "b", "c", "d"]);
179    }
180
181    // --- Edge-case: chunk size of 1 ---
182
183    #[test]
184    fn chunk_size_one_produces_individual_chunks() {
185        let items = vec!["x", "y", "z", "w"];
186        let chunks = chunk_by_max_concurrent(&items, 1);
187        assert_eq!(chunks.len(), 4);
188        for (i, chunk) in chunks.iter().enumerate() {
189            assert_eq!(chunk.len(), 1);
190            assert_eq!(chunk[0], items[i]);
191        }
192    }
193
194    // --- Edge-case: large input (1000 items) ---
195
196    #[test]
197    fn large_input_1000_items_by_3() {
198        let items: Vec<i32> = (0..1000).collect();
199        let chunks = chunk_by_max_concurrent(&items, 3);
200        // ceil(1000 / 3) = 334 chunks
201        assert_eq!(chunks.len(), 334);
202        for chunk in &chunks[..333] {
203            assert_eq!(chunk.len(), 3);
204        }
205        assert_eq!(chunks[333].len(), 1);
206        let flattened: Vec<i32> = chunks.into_iter().flatten().collect();
207        assert_eq!(flattened, items);
208    }
209
210    #[test]
211    fn large_input_1000_items_by_1() {
212        let items: Vec<i32> = (0..1000).collect();
213        let chunks = chunk_by_max_concurrent(&items, 1);
214        assert_eq!(chunks.len(), 1000);
215        for (i, chunk) in chunks.iter().enumerate() {
216            assert_eq!(chunk, &[i as i32]);
217        }
218    }
219
220    #[test]
221    fn large_input_1000_items_by_1000() {
222        let items: Vec<i32> = (0..1000).collect();
223        let chunks = chunk_by_max_concurrent(&items, 1000);
224        assert_eq!(chunks.len(), 1);
225        assert_eq!(chunks[0].len(), 1000);
226    }
227
228    #[test]
229    fn large_input_1000_items_by_17() {
230        let items: Vec<i32> = (0..1000).collect();
231        let chunks = chunk_by_max_concurrent(&items, 17);
232        // ceil(1000 / 17) = 59 chunks
233        assert_eq!(chunks.len(), 59);
234        let total: usize = chunks.iter().map(|c| c.len()).sum();
235        assert_eq!(total, 1000);
236        let flattened: Vec<i32> = chunks.into_iter().flatten().collect();
237        assert_eq!(flattened, items);
238    }
239
240    // --- Edge-case: items with dependency-like structure spanning chunks ---
241
242    #[test]
243    fn dependency_items_spanning_multiple_chunks() {
244        // Simulate crate items where some depend on earlier ones
245        #[derive(Debug, Clone, PartialEq)]
246        struct CrateItem {
247            name: &'static str,
248            depends_on: Vec<&'static str>,
249        }
250
251        let items = vec![
252            CrateItem {
253                name: "core",
254                depends_on: vec![],
255            },
256            CrateItem {
257                name: "utils",
258                depends_on: vec!["core"],
259            },
260            CrateItem {
261                name: "api",
262                depends_on: vec!["core", "utils"],
263            },
264            CrateItem {
265                name: "cli",
266                depends_on: vec!["api"],
267            },
268            CrateItem {
269                name: "web",
270                depends_on: vec!["api", "utils"],
271            },
272        ];
273
274        let chunks = chunk_by_max_concurrent(&items, 2);
275        assert_eq!(chunks.len(), 3);
276
277        // "core" and "utils" in first chunk — "utils" depends on "core" from same chunk
278        assert_eq!(chunks[0][0].name, "core");
279        assert_eq!(chunks[0][1].name, "utils");
280
281        // "api" depends on items in prior chunk, "cli" depends on "api" in same chunk
282        assert_eq!(chunks[1][0].name, "api");
283        assert_eq!(chunks[1][1].name, "cli");
284
285        // "web" depends on items from both prior chunks
286        assert_eq!(chunks[2][0].name, "web");
287    }
288
289    #[test]
290    fn dependency_items_all_in_one_chunk() {
291        #[derive(Debug, Clone, PartialEq)]
292        struct CrateItem {
293            name: &'static str,
294            depends_on: Vec<&'static str>,
295        }
296
297        let items = vec![
298            CrateItem {
299                name: "core",
300                depends_on: vec![],
301            },
302            CrateItem {
303                name: "utils",
304                depends_on: vec!["core"],
305            },
306            CrateItem {
307                name: "api",
308                depends_on: vec!["core", "utils"],
309            },
310        ];
311
312        let chunks = chunk_by_max_concurrent(&items, 10);
313        assert_eq!(chunks.len(), 1);
314        assert_eq!(chunks[0].len(), 3);
315        // All dependencies satisfied within the single chunk
316        assert_eq!(chunks[0][0].name, "core");
317        assert_eq!(chunks[0][2].name, "api");
318    }
319
320    // --- Ordering within chunks is preserved ---
321
322    #[test]
323    fn ordering_within_each_chunk_matches_input() {
324        let items: Vec<i32> = (0..20).collect();
325        let chunks = chunk_by_max_concurrent(&items, 4);
326
327        let mut offset = 0;
328        for chunk in &chunks {
329            for (j, &item) in chunk.iter().enumerate() {
330                assert_eq!(item, items[offset + j], "mismatch at offset {offset}+{j}");
331            }
332            offset += chunk.len();
333        }
334        assert_eq!(offset, items.len());
335    }
336
337    #[test]
338    fn ordering_preserved_with_string_items() {
339        let items: Vec<String> = (0..15).map(|i| format!("crate-{i}")).collect();
340        let chunks = chunk_by_max_concurrent(&items, 4);
341
342        let flattened: Vec<String> = chunks.into_iter().flatten().collect();
343        assert_eq!(flattened, items);
344    }
345
346    // --- Chunk sizing: correct boundaries ---
347
348    #[test]
349    fn chunk_boundaries_are_contiguous_and_non_overlapping() {
350        let items: Vec<i32> = (0..13).collect();
351        let chunks = chunk_by_max_concurrent(&items, 4);
352        // chunks: [0..4], [4..8], [8..12], [12..13]
353        assert_eq!(chunks.len(), 4);
354        assert_eq!(chunks[0], vec![0, 1, 2, 3]);
355        assert_eq!(chunks[1], vec![4, 5, 6, 7]);
356        assert_eq!(chunks[2], vec![8, 9, 10, 11]);
357        assert_eq!(chunks[3], vec![12]);
358    }
359
360    #[test]
361    fn all_full_chunks_have_exact_batch_size() {
362        let items: Vec<i32> = (0..30).collect();
363        let chunks = chunk_by_max_concurrent(&items, 7);
364        // 30 / 7 = 4 full chunks of 7 + 1 remainder of 2
365        for chunk in &chunks[..4] {
366            assert_eq!(
367                chunk.len(),
368                7,
369                "full chunks must have exactly batch_size items"
370            );
371        }
372        assert_eq!(chunks[4].len(), 2, "remainder chunk has leftover items");
373    }
374
375    // --- Remainder handling ---
376
377    #[test]
378    fn remainder_chunk_contains_correct_trailing_items() {
379        let items = vec!["a", "b", "c", "d", "e", "f", "g"];
380        let chunks = chunk_by_max_concurrent(&items, 3);
381        // 7 / 3 = 2 full + remainder of 1
382        assert_eq!(chunks.last().unwrap(), &vec!["g"]);
383    }
384
385    #[test]
386    fn no_remainder_when_evenly_divisible() {
387        let items: Vec<i32> = (0..12).collect();
388        let chunks = chunk_by_max_concurrent(&items, 4);
389        assert_eq!(chunks.len(), 3);
390        for chunk in &chunks {
391            assert_eq!(
392                chunk.len(),
393                4,
394                "all chunks should be full when evenly divisible"
395            );
396        }
397    }
398
399    #[test]
400    fn remainder_of_one_less_than_batch() {
401        let items: Vec<i32> = (0..11).collect();
402        let chunks = chunk_by_max_concurrent(&items, 4);
403        // 11 / 4 = 2 full (8 items) + 1 remainder of 3
404        assert_eq!(chunks.len(), 3);
405        assert_eq!(chunks[2].len(), 3);
406        assert_eq!(chunks[2], vec![8, 9, 10]);
407    }
408
409    // --- Edge cases: two items ---
410
411    #[test]
412    fn two_items_chunk_size_one() {
413        let items = vec!["first", "second"];
414        let chunks = chunk_by_max_concurrent(&items, 1);
415        assert_eq!(chunks, vec![vec!["first"], vec!["second"]]);
416    }
417
418    #[test]
419    fn two_items_chunk_size_two() {
420        let items = vec!["first", "second"];
421        let chunks = chunk_by_max_concurrent(&items, 2);
422        assert_eq!(chunks, vec![vec!["first", "second"]]);
423    }
424
425    #[test]
426    fn two_items_chunk_size_three() {
427        let items = vec!["first", "second"];
428        let chunks = chunk_by_max_concurrent(&items, 3);
429        assert_eq!(chunks.len(), 1);
430        assert_eq!(chunks[0], vec!["first", "second"]);
431    }
432
433    // --- Determinism ---
434
435    #[test]
436    fn determinism_same_input_always_produces_same_chunks() {
437        let items: Vec<i32> = (0..23).collect();
438        let first = chunk_by_max_concurrent(&items, 5);
439        for _ in 0..50 {
440            let again = chunk_by_max_concurrent(&items, 5);
441            assert_eq!(first, again, "chunking must be deterministic");
442        }
443    }
444
445    #[test]
446    fn determinism_with_string_items() {
447        let items: Vec<String> = (0..17).map(|i| format!("pkg-{i}")).collect();
448        let first = chunk_by_max_concurrent(&items, 3);
449        for _ in 0..20 {
450            assert_eq!(
451                chunk_by_max_concurrent(&items, 3),
452                first,
453                "string chunking must be deterministic"
454            );
455        }
456    }
457
458    // --- Edge case: chunk_size equals n-1 ---
459
460    #[test]
461    fn chunk_size_one_less_than_total() {
462        let items: Vec<i32> = (0..5).collect();
463        let chunks = chunk_by_max_concurrent(&items, 4);
464        assert_eq!(chunks.len(), 2);
465        assert_eq!(chunks[0], vec![0, 1, 2, 3]);
466        assert_eq!(chunks[1], vec![4]);
467    }
468
469    // --- Edge case: chunk_size equals n+1 ---
470
471    #[test]
472    fn chunk_size_one_more_than_total() {
473        let items: Vec<i32> = (0..5).collect();
474        let chunks = chunk_by_max_concurrent(&items, 6);
475        assert_eq!(chunks.len(), 1);
476        assert_eq!(chunks[0], vec![0, 1, 2, 3, 4]);
477    }
478
479    // --- No empty chunks emitted ---
480
481    #[test]
482    fn no_empty_chunks_emitted() {
483        for n in 0..=20 {
484            for chunk_size in 0..=20 {
485                let items: Vec<i32> = (0..n).collect();
486                let chunks = chunk_by_max_concurrent(&items, chunk_size as usize);
487                for (ci, chunk) in chunks.iter().enumerate() {
488                    assert!(
489                        !chunk.is_empty(),
490                        "chunk {ci} was empty for n={n}, chunk_size={chunk_size}"
491                    );
492                }
493            }
494        }
495    }
496
497    // --- Prime-sized inputs ---
498
499    #[test]
500    fn prime_item_count_with_non_divisor_chunk_size() {
501        // 37 items chunked by 6: 6 full chunks + 1 remainder of 1
502        let items: Vec<i32> = (0..37).collect();
503        let chunks = chunk_by_max_concurrent(&items, 6);
504        assert_eq!(chunks.len(), 7);
505        for chunk in &chunks[..6] {
506            assert_eq!(chunk.len(), 6);
507        }
508        assert_eq!(chunks[6].len(), 1);
509        assert_eq!(chunks[6][0], 36);
510    }
511}
512
513#[cfg(test)]
514mod property_tests {
515    use super::chunk_by_max_concurrent;
516    use proptest::prelude::*;
517
518    proptest! {
519        #[test]
520        fn chunking_preserves_items_and_order(
521            items in prop::collection::vec("[a-z]{0,6}", 0..128),
522            max_concurrent in 1usize..16,
523        ) {
524            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
525            let flattened: Vec<String> = chunks
526                .iter()
527                .flat_map(|chunk| chunk.iter().cloned())
528                .collect();
529
530            prop_assert_eq!(flattened.len(), items.len());
531            prop_assert_eq!(flattened, items.clone());
532
533            let sum_len: usize = chunks.iter().map(|chunk| chunk.len()).sum();
534            prop_assert_eq!(sum_len, items.len());
535            for chunk in chunks {
536                prop_assert!(chunk.len() <= max_concurrent.max(1));
537            }
538        }
539
540        #[test]
541        fn chunk_sizes_never_exceed_max_and_total_preserved(
542            items in prop::collection::vec(0i32..1000, 0..200),
543            max_concurrent in 0usize..32,
544        ) {
545            let effective = max_concurrent.max(1);
546            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
547
548            // Every chunk respects the bound
549            for chunk in &chunks {
550                prop_assert!(chunk.len() <= effective);
551                prop_assert!(!chunk.is_empty());
552            }
553
554            // Total items preserved
555            let total: usize = chunks.iter().map(|c| c.len()).sum();
556            prop_assert_eq!(total, items.len());
557
558            // Order preserved
559            let flattened: Vec<i32> = chunks.into_iter().flatten().collect();
560            prop_assert_eq!(flattened, items);
561        }
562
563        #[test]
564        fn total_items_across_chunks_equals_input_length(
565            items in prop::collection::vec(0u32..500, 0..300),
566            max_concurrent in 1usize..64,
567        ) {
568            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
569            let total: usize = chunks.iter().map(|c| c.len()).sum();
570            prop_assert_eq!(total, items.len());
571        }
572
573        #[test]
574        fn chunk_count_is_ceil_of_n_over_chunk_size(
575            n in 0usize..500,
576            max_concurrent in 1usize..64,
577        ) {
578            let items: Vec<usize> = (0..n).collect();
579            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
580
581            let expected_count = if n == 0 {
582                0
583            } else {
584                n.div_ceil(max_concurrent)
585            };
586            prop_assert_eq!(chunks.len(), expected_count);
587        }
588
589        #[test]
590        fn ordering_within_chunks_preserved_property(
591            items in prop::collection::vec(any::<i64>(), 0..150),
592            max_concurrent in 1usize..20,
593        ) {
594            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
595            let mut offset = 0usize;
596            for chunk in &chunks {
597                for (j, item) in chunk.iter().enumerate() {
598                    prop_assert_eq!(item, &items[offset + j]);
599                }
600                offset += chunk.len();
601            }
602            prop_assert_eq!(offset, items.len());
603        }
604
605        #[test]
606        fn no_empty_chunks_emitted_property(
607            items in prop::collection::vec(any::<u8>(), 0..200),
608            max_concurrent in 0usize..64,
609        ) {
610            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
611            for chunk in &chunks {
612                prop_assert!(!chunk.is_empty(), "chunks must never be empty");
613            }
614        }
615
616        #[test]
617        fn determinism_property(
618            items in prop::collection::vec(any::<i32>(), 0..100),
619            max_concurrent in 1usize..32,
620        ) {
621            let first = chunk_by_max_concurrent(&items, max_concurrent);
622            let second = chunk_by_max_concurrent(&items, max_concurrent);
623            prop_assert_eq!(first, second, "chunking must be deterministic");
624        }
625
626        #[test]
627        fn all_full_chunks_have_batch_size_property(
628            n in 1usize..300,
629            max_concurrent in 1usize..64,
630        ) {
631            let items: Vec<usize> = (0..n).collect();
632            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
633            let effective = max_concurrent.max(1);
634            // All chunks except possibly the last must be exactly batch_size
635            if chunks.len() > 1 {
636                for chunk in &chunks[..chunks.len() - 1] {
637                    prop_assert_eq!(chunk.len(), effective,
638                        "non-last chunks must have exactly batch_size items");
639                }
640            }
641            // Last chunk must have 1..=batch_size items
642            if let Some(last) = chunks.last() {
643                prop_assert!(!last.is_empty() && last.len() <= effective);
644            }
645        }
646
647        /// Chunk indices cover 0..n exactly once: no gaps, no overlaps.
648        #[test]
649        fn chunks_cover_indices_exactly_once(
650            n in 0usize..200,
651            max_concurrent in 1usize..32,
652        ) {
653            let items: Vec<usize> = (0..n).collect();
654            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
655            let mut seen = std::collections::HashSet::new();
656            for chunk in &chunks {
657                for &item in chunk {
658                    prop_assert!(seen.insert(item), "duplicate index {item}");
659                }
660            }
661            prop_assert_eq!(seen.len(), n, "not all indices covered");
662        }
663
664        /// If input elements are unique, output elements are unique.
665        #[test]
666        fn unique_input_produces_unique_output(
667            items in prop::collection::vec(0u32..10_000, 0..100),
668            max_concurrent in 1usize..16,
669        ) {
670            let unique_input: std::collections::HashSet<u32> = items.iter().copied().collect();
671            let chunks = chunk_by_max_concurrent(&items, max_concurrent);
672            let flattened: Vec<u32> = chunks.into_iter().flatten().collect();
673            let unique_output: std::collections::HashSet<u32> = flattened.iter().copied().collect();
674            // If input had duplicates, output should have the same duplicates
675            prop_assert_eq!(flattened.len(), items.len());
676            prop_assert_eq!(unique_output.len(), unique_input.len());
677        }
678
679        /// Idempotency: chunking then flattening then re-chunking produces same chunks.
680        #[test]
681        fn chunking_is_idempotent_after_flatten(
682            items in prop::collection::vec(any::<i32>(), 0..100),
683            max_concurrent in 1usize..16,
684        ) {
685            let chunks1 = chunk_by_max_concurrent(&items, max_concurrent);
686            let flattened: Vec<i32> = chunks1.iter().flat_map(|c| c.iter().cloned()).collect();
687            let chunks2 = chunk_by_max_concurrent(&flattened, max_concurrent);
688            prop_assert_eq!(chunks1, chunks2, "chunking not idempotent");
689        }
690    }
691}
692#[cfg(test)]
693mod snapshot_tests {
694    use super::chunk_by_max_concurrent;
695    use insta::assert_debug_snapshot;
696    use insta::assert_yaml_snapshot;
697
698    #[test]
699    fn snapshot_chunk_5_by_2() {
700        let items = vec!["a", "b", "c", "d", "e"];
701        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 2));
702    }
703
704    #[test]
705    fn snapshot_chunk_6_by_3() {
706        let items = vec!["core", "utils", "api", "cli", "web", "docs"];
707        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 3));
708    }
709
710    #[test]
711    fn snapshot_chunk_single_item() {
712        let items = vec!["only"];
713        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 5));
714    }
715
716    #[test]
717    fn snapshot_chunk_max_concurrent_1() {
718        let items = vec!["a", "b", "c"];
719        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 1));
720    }
721
722    #[test]
723    fn snapshot_chunk_exact_fit() {
724        let items = vec!["x", "y", "z"];
725        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 3));
726    }
727
728    #[test]
729    fn snapshot_chunk_empty() {
730        let items: Vec<&str> = vec![];
731        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 4));
732    }
733
734    #[test]
735    fn snapshot_chunk_max_concurrent_zero() {
736        let items = vec!["a", "b", "c"];
737        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 0));
738    }
739
740    #[test]
741    fn snapshot_chunk_max_concurrent_usize_max() {
742        let items = vec!["x", "y", "z"];
743        assert_debug_snapshot!(chunk_by_max_concurrent(&items, usize::MAX));
744    }
745
746    #[test]
747    fn snapshot_chunk_single_item_max_one() {
748        let items = vec!["solo"];
749        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 1));
750    }
751
752    #[test]
753    fn snapshot_chunk_large_list_by_7() {
754        let items: Vec<i32> = (1..=21).collect();
755        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 7));
756    }
757
758    #[test]
759    fn snapshot_chunk_1000_items_by_10() {
760        let items: Vec<i32> = (0..1000).collect();
761        let chunks = chunk_by_max_concurrent(&items, 10);
762        // Snapshot only the chunk lengths to keep it readable
763        let lens: Vec<usize> = chunks.iter().map(|c| c.len()).collect();
764        assert_debug_snapshot!(lens);
765    }
766
767    #[test]
768    fn snapshot_chunk_7_items_by_3() {
769        let items = vec![
770            "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf",
771        ];
772        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 3));
773    }
774
775    #[test]
776    fn snapshot_chunk_size_1_with_4_items() {
777        let items = vec!["w", "x", "y", "z"];
778        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 1));
779    }
780
781    #[test]
782    fn snapshot_chunk_10_items_by_5() {
783        let items: Vec<i32> = (1..=10).collect();
784        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 5));
785    }
786
787    #[test]
788    fn snapshot_dependency_like_items_by_2() {
789        let items = vec![
790            ("core", vec![]),
791            ("utils", vec!["core"]),
792            ("api", vec!["core", "utils"]),
793            ("cli", vec!["api"]),
794            ("web", vec!["api", "utils"]),
795        ];
796        assert_debug_snapshot!(chunk_by_max_concurrent(&items, 2));
797    }
798
799    #[test]
800    fn snapshot_chunk_13_items_by_4_with_remainder() {
801        let items: Vec<&str> = vec![
802            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
803        ];
804        assert_yaml_snapshot!(chunk_by_max_concurrent(&items, 4));
805    }
806
807    #[test]
808    fn snapshot_chunk_prime_37_by_6() {
809        let items: Vec<i32> = (0..37).collect();
810        let chunks = chunk_by_max_concurrent(&items, 6);
811        let lens: Vec<usize> = chunks.iter().map(|c| c.len()).collect();
812        assert_debug_snapshot!(lens);
813    }
814}