Skip to main content

fast_pull/base/
merge.rs

1//! Merging of [`ProgressEntry`](crate::ProgressEntry) ranges into a sorted list.
2
3use crate::ProgressEntry;
4
5/// Trait for merging a new [`ProgressEntry`] into a sorted list of existing entries.
6///
7/// Used to consolidate downloaded ranges and remove redundant gaps.
8pub trait Merge {
9    /// Merge `new` into the existing (sorted) progress list, coalescing overlaps
10    /// so the list stays sorted and gap-free where ranges touch.
11    ///
12    /// # Preconditions
13    ///
14    /// The list must already be **sorted by `start` and non-overlapping**. This
15    /// holds automatically for lists built exclusively through this method; it is
16    /// checked by a `debug_assert` for hand-built lists.
17    fn merge_progress(&mut self, new: ProgressEntry);
18}
19
20impl Merge for Vec<ProgressEntry> {
21    fn merge_progress(&mut self, new: ProgressEntry) {
22        debug_assert!(
23            self.windows(2).all(|w| w[0].end <= w[1].start),
24            "merge_progress requires a sorted, non-overlapping list; merge the entries first"
25        );
26        if new.start >= new.end {
27            return;
28        }
29        let i = self.partition_point(|x| x.end < new.start);
30        if i == self.len() {
31            self.push(new);
32            return;
33        }
34        if self[i].start <= new.start && self[i].end >= new.end {
35            return;
36        }
37        let mut current_merge = new;
38        let mut j = i;
39        while j < self.len() {
40            let entry = &self[j];
41            if entry.start > current_merge.end {
42                break;
43            }
44            current_merge.start = current_merge.start.min(entry.start);
45            current_merge.end = current_merge.end.max(entry.end);
46            j += 1;
47        }
48        if j > i {
49            self.drain(i..j);
50        }
51        self.insert(i, current_merge);
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_merge() {
61        #![allow(clippy::single_range_in_vec_init)]
62        let mut v = vec![1..5, 8..10];
63        v.merge_progress(5..10);
64        assert_eq!(v, vec![1..10]);
65        v.merge_progress(10..20);
66        assert_eq!(v, vec![1..20]);
67        v.merge_progress(30..40);
68        assert_eq!(v, vec![1..20, 30..40]);
69        v.merge_progress(21..40);
70        assert_eq!(v, vec![1..20, 21..40]);
71        v.merge_progress(19..21);
72        assert_eq!(v, vec![1..40]);
73        v.merge_progress(50..60);
74        assert_eq!(v, vec![1..40, 50..60]);
75        v.merge_progress(50..60);
76        assert_eq!(v, vec![1..40, 50..60]);
77        v.merge_progress(52..60);
78        assert_eq!(v, vec![1..40, 50..60]);
79        v.merge_progress(52..53);
80        assert_eq!(v, vec![1..40, 50..60]);
81        v.merge_progress(52..61);
82        assert_eq!(v, vec![1..40, 50..61]);
83        v.merge_progress(62..70);
84        assert_eq!(v, vec![1..40, 50..61, 62..70]);
85        v.merge_progress(40..62);
86        assert_eq!(v, vec![1..70]);
87        v.merge_progress(72..82);
88        assert_eq!(v, vec![1..70, 72..82]);
89        v.merge_progress(0..90);
90        assert_eq!(v, vec![0..90]);
91    }
92
93    #[test]
94    fn test_merge_empty_range_is_dropped() {
95        // An empty range contained inside an existing entry must be a no-op.
96        let mut v = vec![1..5, 10..20];
97        v.merge_progress(3..3);
98        assert_eq!(v, vec![1..5, 10..20]);
99
100        // An empty range landing in a gap or at the end must NOT be inserted
101        // as a degenerate entry.
102        v.merge_progress(7..7);
103        assert_eq!(v, vec![1..5, 10..20]);
104        v.merge_progress(25..25);
105        assert_eq!(v, vec![1..5, 10..20]);
106    }
107
108    #[test]
109    fn test_merge_before_front_with_gap() {
110        #![allow(clippy::single_range_in_vec_init)]
111        // `new` sits entirely before `self[0]`, leaving a gap -> inserted at front.
112        let mut v = vec![5..10];
113        v.merge_progress(1..3);
114        assert_eq!(v, vec![1..3, 5..10]);
115    }
116
117    #[test]
118    fn test_merge_extends_before_first_and_spans_gaps() {
119        #![allow(clippy::single_range_in_vec_init)]
120        // `new` starts before the first entry and spans across multiple gaps,
121        // absorbing every overlapping/touching entry into a single coalesced range.
122        let mut v = vec![1..5, 8..10, 12..15];
123        v.merge_progress(0..13);
124        assert_eq!(v, vec![0..15]);
125    }
126
127    #[test]
128    fn test_merge_reversed_range_is_dropped() {
129        #![allow(clippy::single_range_in_vec_init)]
130        // A reversed range (`start > end`) is invalid and must not enter the list.
131        let mut v = vec![10..20];
132        #[allow(clippy::reversed_empty_ranges)]
133        v.merge_progress(30..5);
134        assert_eq!(v, vec![10..20]);
135    }
136
137    #[test]
138    fn test_merge_into_empty_vec() {
139        #![allow(clippy::single_range_in_vec_init)]
140        let mut v: Vec<ProgressEntry> = vec![];
141        v.merge_progress(5..10);
142        assert_eq!(v, vec![5..10]);
143    }
144
145    #[test]
146    fn test_merge_superset_absorbs_all() {
147        #![allow(clippy::single_range_in_vec_init)]
148        let mut v = vec![1..5, 8..10, 20..30];
149        v.merge_progress(0..40);
150        assert_eq!(v, vec![0..40]);
151    }
152
153    #[test]
154    fn test_merge_exact_duplicate_is_noop() {
155        #![allow(clippy::single_range_in_vec_init)]
156        let mut v = vec![1..5];
157        v.merge_progress(1..5);
158        assert_eq!(v, vec![1..5]);
159    }
160
161    #[test]
162    fn test_merge_touching_right_extends() {
163        #![allow(clippy::single_range_in_vec_init)]
164        let mut v = vec![1..5];
165        v.merge_progress(5..8);
166        assert_eq!(v, vec![1..8]);
167    }
168
169    #[test]
170    fn test_merge_touching_left_extends() {
171        #![allow(clippy::single_range_in_vec_init)]
172        let mut v = vec![5..10];
173        v.merge_progress(1..5);
174        assert_eq!(v, vec![1..10]);
175    }
176
177    /// Simulate concurrent, out-of-order chunk delivery where each chunk is
178    /// *adjacent* (touching but not overlapping) the next — the worst case for
179    /// `download_complete`'s `x.len() == 1` check in `overwrite.rs`. This must
180    /// still coalesce into a single `[0..200]` entry, otherwise the download
181    /// would be wrongly reported as incomplete and the `.part` never renamed.
182    #[test]
183    fn test_merge_out_of_order_adjacent_coalesces_to_single() {
184        let mut v: Vec<ProgressEntry> = vec![];
185        // 4 chunks of 50 bytes on a 200-byte file, arriving in a scrambled order.
186        v.merge_progress(0..50);
187        v.merge_progress(150..200);
188        v.merge_progress(100..150);
189        v.merge_progress(50..100);
190        assert_eq!(v, vec![0..200]);
191
192        // And the canonical "fully covered, single entry" invariant holds for
193        // any interleaving that covers the whole span.
194        let mut w: Vec<ProgressEntry> = vec![];
195        for r in [0..70, 140..200, 70..140] {
196            w.merge_progress(r);
197        }
198        assert_eq!(w, vec![0..200]);
199        assert!(w.len() == 1 && w[0] == (0..200));
200    }
201
202    /// Independent reference implementation: sort, then sweep and coalesce.
203    ///
204    /// Used as an oracle to check that the incremental `merge_progress` always
205    /// produces the same covered set as a batch merge, regardless of insertion
206    /// order.
207    fn reference_merge(ranges: &[ProgressEntry]) -> Vec<ProgressEntry> {
208        let mut sorted: Vec<ProgressEntry> =
209            ranges.iter().filter(|r| r.start < r.end).cloned().collect();
210        if sorted.is_empty() {
211            return vec![];
212        }
213        sorted.sort_by_key(|r| r.start);
214        let mut merged: Vec<ProgressEntry> = vec![sorted[0].clone()];
215        for r in sorted.iter().skip(1) {
216            let last = merged.last_mut().unwrap();
217            if r.start <= last.end {
218                last.end = last.end.max(r.end); // overlapping or touching -> extend
219            } else {
220                merged.push(r.clone());
221            }
222        }
223        merged
224    }
225
226    #[test]
227    fn merge_agrees_with_reference_oracle() {
228        // Whatever the insertion order, the incremental merge must equal the
229        // covered set computed by the batch oracle.
230        let cases: &[&[ProgressEntry]] = &[
231            &[1..5, 8..10, 3..12],
232            &[0..10, 20..30, 5..25],
233            &[10..20, 0..5, 4..6, 19..21],
234            &[0..50, 40..60, 55..70, 10..45],
235            &[100..200, 0..50, 50..100],
236            &[0..10, 30..40, 10..30], // last entry exactly fills the gap
237        ];
238        for ranges in cases {
239            let mut v: Vec<ProgressEntry> = vec![];
240            for r in *ranges {
241                v.merge_progress(r.clone());
242            }
243            assert_eq!(v, reference_merge(ranges), "case {ranges:?}");
244        }
245    }
246
247    #[test]
248    fn merge_full_file_coalesces_to_single_regardless_of_order() {
249        // 50 four-byte chunks of a 200-byte file, merged in a shuffled order,
250        // must collapse into a single [0..200] entry. `download_complete` relies
251        // on this: it tests the list length instead of the covered byte count.
252        let chunks: Vec<ProgressEntry> = (0..50).map(|i| (i * 4)..(i * 4 + 4)).collect();
253        let mut order: Vec<usize> = (0..50).collect();
254        order.sort_by_key(|&i| (i % 7, i)); // deterministic shuffle
255        let mut v: Vec<ProgressEntry> = vec![];
256        for &i in &order {
257            v.merge_progress(chunks[i].clone());
258        }
259        assert_eq!(v, vec![0..200]);
260    }
261
262    #[test]
263    fn merge_new_fills_gap_and_coalesces_all_three() {
264        #![allow(clippy::single_range_in_vec_init)]
265        // A new entry that exactly fills the gap between two disjoint entries,
266        // touching both ends, coalesces all three into one.
267        let mut v = vec![0..10, 30..40];
268        v.merge_progress(10..30);
269        assert_eq!(v, vec![0..40]);
270    }
271
272    #[test]
273    fn merge_accepts_touching_entries() {
274        #![allow(clippy::single_range_in_vec_init)]
275        // `merge_progress` itself never leaves touching entries behind, but a
276        // hand-built list containing them still satisfies the "sorted and
277        // non-overlapping" precondition and merges correctly.
278        let mut v = vec![0..10, 10..20];
279        v.merge_progress(5..15);
280        assert_eq!(v, vec![0..20]);
281    }
282}