1use std::sync::Arc;
2
3use futures::StreamExt;
4use reqwest;
5
6use crate::constants;
7use crate::engine::download_cookie::CookieHelper;
8use crate::engine::download_progress::ProgressUpdater;
9use crate::engine::retry_policy::RetryPolicy;
10use crate::error::{Aria2Error, RecoverableError, Result};
11use crate::filesystem::disk_writer::{
12 CachedDiskWriter, DefaultDiskWriter, DiskWriter, SeekableDiskWriter,
13};
14use crate::filesystem::resume_helper::{ResumeHelper, ResumeState};
15use crate::rate_limiter::{RateLimiter, RateLimiterConfig, ThrottledWriter};
16use crate::request::request_group::RequestGroup;
17
18pub struct GapDownloadResult {
19 pub completed_gaps: Vec<(u64, u64)>,
20 pub error: Option<Aria2Error>,
21}
22
23pub struct SequentialDownloader {
24 client: Arc<reqwest::Client>,
25 output_path: std::path::PathBuf,
26 headers: Vec<(String, String)>,
27 cookie_helper: CookieHelper,
28 progress_updater: ProgressUpdater,
29 group: Arc<tokio::sync::RwLock<RequestGroup>>,
30}
31
32impl SequentialDownloader {
33 pub fn new(
34 client: Arc<reqwest::Client>,
35 output_path: std::path::PathBuf,
36 headers: Vec<(String, String)>,
37 cookie_helper: CookieHelper,
38 progress_updater: ProgressUpdater,
39 group: Arc<tokio::sync::RwLock<RequestGroup>>,
40 ) -> Self {
41 Self {
42 client,
43 output_path,
44 headers,
45 cookie_helper,
46 progress_updater,
47 group,
48 }
49 }
50
51 pub async fn execute(
52 &mut self,
53 uri: &str,
54 resume_state: &ResumeState,
55 total_length: u64,
56 ) -> Result<()> {
57 #[cfg(not(target_os = "linux"))]
58 let _ = total_length;
59
60 #[cfg(target_os = "linux")]
61 {
62 let no_proxy = {
63 let guard = self.group.read().await;
64 let opts = guard.options();
65 opts.http_proxy.is_none() && opts.all_proxy.is_none()
66 };
67 if !resume_state.should_resume
68 && total_length > 0
69 && no_proxy
70 && !uri.starts_with("https://")
71 && self.headers.is_empty()
72 && self.cookie_helper.build_cookie_header(uri).is_none()
73 {
74 match self.try_splice_sequential(uri, total_length).await {
75 Ok(()) => return Ok(()),
76 Err(e) => {
77 tracing::debug!(
78 "Splice download failed for {}, falling back to streaming: {}",
79 uri,
80 e
81 );
82 }
83 }
84 }
85 }
86
87 let url_parsed = reqwest::Url::parse(uri).ok();
88 let mut request = if let Some(range_header) = ResumeHelper::build_range_header(resume_state)
89 {
90 tracing::debug!("Resume download: {}", range_header);
91 self.client.get(uri).header("Range", range_header)
92 } else {
93 self.client.get(uri)
94 };
95
96 if let Some(ref url) = url_parsed {
97 let cookie_hdr = self.cookie_helper.build_cookie_header_from_url(url);
98 if !cookie_hdr.is_empty() {
99 request = request.header("Cookie", &cookie_hdr);
100 }
101 }
102
103 for (name, value) in &self.headers {
104 request = request.header(name, value);
105 }
106
107 let response = request.send().await.map_err(|e| {
108 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
109 message: format!("HTTP request failed: {}", e),
110 })
111 })?;
112
113 self.cookie_helper.extract_and_store_cookies(uri, &response);
114
115 let status = response.status();
116 if !status.is_success() && status.as_u16() != 206 {
117 if status.as_u16() >= 500 {
118 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
119 code: status.as_u16(),
120 }));
121 }
122 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
123 format!("HTTP error: {}", status),
124 )));
125 }
126
127 let resp_length = response.content_length().unwrap_or(0) as u64;
128 let actual_total = if resume_state.should_resume {
129 resume_state.start_offset + resp_length
130 } else {
131 resp_length
132 };
133 {
134 let mut g = self.group.write().await;
135 g.set_total_length(actual_total).await;
136 g.set_total_length_atomic(actual_total);
137 }
138
139 let start_offset = if resume_state.should_resume {
140 resume_state.start_offset
141 } else {
142 0
143 };
144
145 self.progress_updater.reset(start_offset);
146
147 let rate_limit = { self.group.read().await.options().max_download_limit };
148
149 let raw_writer = DefaultDiskWriter::new(&self.output_path);
150 let mut writer: Box<dyn DiskWriter> = match rate_limit {
151 Some(rate) if rate > 0 => {
152 let cfg = RateLimiterConfig::new(Some(rate), None);
153 let limiter = RateLimiter::new(&cfg);
154 tracing::debug!("Download speed limit enabled: {} bytes/s", rate);
155 {
156 let g = self.group.read().await;
157 g.set_rate_limiter(limiter.clone()).await;
158 }
159 Box::new(ThrottledWriter::new(raw_writer, limiter))
160 }
161 _ => Box::new(raw_writer),
162 };
163
164 let mut stream = response.bytes_stream();
165 let mut completed_bytes = start_offset;
166 let write_piece = constants::RATE_LIMITER_CHUNK_SIZE;
167
168 while let Some(chunk) = stream.next().await {
169 let data: bytes::Bytes = chunk.map_err(|e| {
170 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
171 message: e.to_string(),
172 })
173 })?;
174
175 let mut offset = 0usize;
176 while offset < data.len() {
177 let end = (offset + write_piece).min(data.len());
178 let piece = &data[offset..end];
179 writer.write(piece).await?;
180 completed_bytes += piece.len() as u64;
181 offset = end;
182
183 self.progress_updater
184 .update_progress(
185 completed_bytes,
186 constants::PROGRESS_UPDATE_BYTES as u64,
187 constants::HTTP_SPEED_UPDATE_INTERVAL_MS,
188 )
189 .await;
190 }
191 }
192
193 writer.finalize().await.ok();
194
195 let final_speed = {
196 let g = self.group.read().await;
197 let elapsed = g.elapsed_time().await;
198 match elapsed {
199 Some(d) if d.as_secs_f64() > 0.0 => {
200 (completed_bytes as f64 / d.as_secs_f64()) as u64
201 }
202 _ => 0,
203 }
204 };
205 {
206 let mut g = self.group.write().await;
207 g.update_progress(completed_bytes).await;
208 g.update_speed(final_speed, 0).await;
209 g.set_completed_length(completed_bytes);
210 g.set_download_speed_cached(final_speed);
211 g.complete().await?;
212 }
213
214 tracing::info!(
215 "Sequential download complete: {} ({} bytes)",
216 self.output_path.display(),
217 completed_bytes
218 );
219 self.cookie_helper.save_cookies_if_configured();
220 Ok(())
221 }
222
223 pub async fn execute_with_gaps(
224 &mut self,
225 uri: &str,
226 total_length: u64,
227 completed_ranges: &[(u64, u64)],
228 ) -> GapDownloadResult {
229 let gaps = Self::find_all_gaps(completed_ranges, total_length);
230 tracing::info!(
231 "Starting sequential download with gaps: uri={}, total={}, gaps={:?}",
232 uri,
233 total_length,
234 gaps
235 );
236
237 if gaps.is_empty() {
238 tracing::info!("No gaps to download, download complete");
239 return GapDownloadResult {
240 completed_gaps: Vec::new(),
241 error: None,
242 };
243 }
244
245 let url_parsed = reqwest::Url::parse(uri).ok();
246 let cookie_hdr = if let Some(ref url) = url_parsed {
247 let hdr = self.cookie_helper.build_cookie_header_from_url(url);
248 if hdr.is_empty() { None } else { Some(hdr) }
249 } else {
250 None
251 };
252
253 let mut completed_bytes = completed_ranges.iter().map(|(_, len)| len).sum::<u64>();
254 self.progress_updater.reset(completed_bytes);
255
256 let mut writer = CachedDiskWriter::new(&self.output_path, Some(total_length), None);
257
258 let rate_limit = { self.group.read().await.options().max_download_limit };
259 let limiter = rate_limit
260 .filter(|&r| r > 0)
261 .map(|r| RateLimiter::new(&RateLimiterConfig::new(Some(r), None)));
262 if let Some(ref lim) = limiter {
263 let g = self.group.read().await;
264 g.set_rate_limiter(lim.clone()).await;
265 }
266
267 let mut last_progress_update = completed_bytes;
268 let mut completed_gaps: Vec<(u64, u64)> = Vec::new();
269
270 for (gap_start, gap_length) in gaps {
271 let gap_end = gap_start + gap_length - 1;
272 let range_header = format!("bytes={}-{}", gap_start, gap_end);
273 tracing::debug!("Sequential Range request for gap: {}", range_header);
274
275 let mut request = self.client.get(uri).header("Range", &range_header);
276 if let Some(ref hdr) = cookie_hdr {
277 request = request.header("Cookie", hdr);
278 }
279 for (name, value) in &self.headers {
280 request = request.header(name, value);
281 }
282
283 let response = match request.send().await {
284 Ok(r) => r,
285 Err(e) => {
286 tracing::warn!(
287 "Gap download failed ({}), cleaning up partial data",
288 range_header
289 );
290 Self::cleanup_partial_gap(&mut writer, gap_start, 0).await;
291 return GapDownloadResult {
292 completed_gaps,
293 error: Some(Aria2Error::Recoverable(
294 RecoverableError::TemporaryNetworkFailure {
295 message: format!("HTTP request failed: {}", e),
296 },
297 )),
298 };
299 }
300 };
301
302 self.cookie_helper.extract_and_store_cookies(uri, &response);
303
304 let status = response.status();
305 if !status.is_success() && status.as_u16() != 206 {
306 tracing::warn!(
307 "Gap download failed with HTTP status {} ({}), cleaning up partial data",
308 status,
309 range_header
310 );
311 Self::cleanup_partial_gap(&mut writer, gap_start, 0).await;
312 let error = if status.as_u16() >= 500 {
313 Aria2Error::Recoverable(RecoverableError::ServerError {
314 code: status.as_u16(),
315 })
316 } else {
317 Aria2Error::Fatal(crate::error::FatalError::Config(format!(
318 "HTTP error: {}",
319 status
320 )))
321 };
322 return GapDownloadResult {
323 completed_gaps,
324 error: Some(error),
325 };
326 }
327
328 let mut stream = response.bytes_stream();
329 let mut stream_offset = gap_start;
330 let mut bytes_downloaded = 0u64;
331
332 while let Some(chunk_result) = stream.next().await {
333 let data: bytes::Bytes = match chunk_result {
334 Ok(d) => d,
335 Err(e) => {
336 tracing::warn!(
337 "Gap download failed during streaming ({}), cleaning up partial data",
338 range_header
339 );
340 Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
341 return GapDownloadResult {
342 completed_gaps,
343 error: Some(Aria2Error::Recoverable(
344 RecoverableError::TemporaryNetworkFailure {
345 message: e.to_string(),
346 },
347 )),
348 };
349 }
350 };
351
352 if let Some(ref lim) = limiter {
353 lim.acquire_download(data.len() as u64).await;
354 }
355
356 match writer.write_bytes_at(stream_offset, data.clone()).await {
357 Ok(_) => {}
358 Err(e) => {
359 tracing::warn!(
360 "Write failed during gap download ({}), cleaning up partial data",
361 range_header
362 );
363 Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
364 return GapDownloadResult {
365 completed_gaps,
366 error: Some(Aria2Error::Fatal(crate::error::FatalError::Config(
367 format!("Write failed: {}", e),
368 ))),
369 };
370 }
371 }
372
373 let data_len = data.len() as u64;
374 completed_bytes += data_len;
375 stream_offset += data_len;
376 bytes_downloaded += data_len;
377
378 if completed_bytes - last_progress_update >= constants::PROGRESS_UPDATE_BYTES as u64
379 {
380 self.progress_updater
381 .update_progress(
382 completed_bytes,
383 constants::PROGRESS_UPDATE_BYTES as u64,
384 constants::HTTP_SPEED_UPDATE_INTERVAL_MS,
385 )
386 .await;
387 last_progress_update = completed_bytes;
388 }
389 }
390
391 if bytes_downloaded == gap_length {
392 completed_gaps.push((gap_start, gap_length));
393 tracing::debug!(
394 "Gap download complete: {} ({} bytes)",
395 range_header,
396 bytes_downloaded
397 );
398 } else {
399 tracing::warn!(
400 "Gap download incomplete: expected {} bytes, got {} bytes",
401 gap_length,
402 bytes_downloaded
403 );
404 Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
405 }
406 }
407
408 if let Err(e) = writer.flush().await {
409 tracing::warn!("Flush failed during gap download: {}", e);
410 return GapDownloadResult {
411 completed_gaps,
412 error: Some(Aria2Error::Fatal(crate::error::FatalError::Config(
413 format!("Flush failed: {}", e),
414 ))),
415 };
416 }
417
418 let final_speed = {
419 let g = self.group.read().await;
420 let elapsed = g.elapsed_time().await;
421 match elapsed {
422 Some(d) if d.as_secs_f64() > 0.0 => {
423 (completed_bytes as f64 / d.as_secs_f64()) as u64
424 }
425 _ => 0,
426 }
427 };
428 {
429 let mut g = self.group.write().await;
430 g.set_total_length(completed_bytes).await;
431 g.set_total_length_atomic(completed_bytes);
432 g.update_progress(completed_bytes).await;
433 g.update_speed(final_speed, 0).await;
434 g.set_completed_length(completed_bytes);
435 g.set_download_speed_cached(final_speed);
436 if let Err(e) = g.complete().await {
437 tracing::warn!("Failed to complete request group: {}", e);
438 return GapDownloadResult {
439 completed_gaps,
440 error: Some(e),
441 };
442 }
443 }
444
445 tracing::info!(
446 "Sequential download with gaps complete: {} ({} bytes, {} gaps completed)",
447 self.output_path.display(),
448 completed_bytes,
449 completed_gaps.len()
450 );
451 self.cookie_helper.save_cookies_if_configured();
452 GapDownloadResult {
453 completed_gaps,
454 error: None,
455 }
456 }
457
458 async fn cleanup_partial_gap(
459 writer: &mut CachedDiskWriter,
460 gap_start: u64,
461 bytes_written: u64,
462 ) {
463 if bytes_written == 0 {
464 return;
465 }
466 let zero_data = vec![0u8; bytes_written as usize];
467 if let Err(e) = writer
468 .write_bytes_at(gap_start, bytes::Bytes::from(zero_data))
469 .await
470 {
471 tracing::warn!(
472 "Failed to cleanup partial gap at {} ({} bytes): {}",
473 gap_start,
474 bytes_written,
475 e
476 );
477 }
478 }
479
480 pub fn merge_ranges(ranges: &[(u64, u64)]) -> Vec<(u64, u64)> {
481 if ranges.is_empty() {
482 return Vec::new();
483 }
484
485 let mut sorted = ranges.to_vec();
486 sorted.sort_by_key(|r| r.0);
487
488 let mut merged = Vec::new();
489 let mut current = sorted[0];
490
491 for &(offset, length) in sorted.iter().skip(1) {
492 let current_end = current.0 + current.1;
493 let next_end = offset + length;
494
495 if offset <= current_end {
496 current = (current.0, std::cmp::max(current_end, next_end) - current.0);
497 } else {
498 merged.push(current);
499 current = (offset, length);
500 }
501 }
502 merged.push(current);
503 merged
504 }
505
506 pub fn find_all_gaps(completed_ranges: &[(u64, u64)], total_length: u64) -> Vec<(u64, u64)> {
507 let merged_ranges = Self::merge_ranges(completed_ranges);
508 let mut gaps = Vec::new();
509 if merged_ranges.is_empty() {
510 if total_length > 0 {
511 gaps.push((0, total_length));
512 }
513 return gaps;
514 }
515
516 let mut current = 0;
517 for &(offset, length) in &merged_ranges {
518 if offset > current {
519 gaps.push((current, offset - current));
520 }
521 current = std::cmp::max(current, offset + length);
522 }
523 if current < total_length {
524 gaps.push((current, total_length - current));
525 }
526 gaps
527 }
528
529 pub async fn execute_with_gaps_with_retry(
530 &mut self,
531 uri: &str,
532 total_length: u64,
533 completed_ranges: &[(u64, u64)],
534 retry_policy: &RetryPolicy,
535 ) -> Result<()> {
536 let mut last_err = None;
537 let mut accumulated_completed: Vec<(u64, u64)> = completed_ranges.to_vec();
538
539 for attempt in 0..=retry_policy.max_retries {
540 if attempt > 0
541 && let Some(wait) = retry_policy.compute_wait(attempt - 1)
542 {
543 tracing::info!(
544 "Sequential download with gaps retry #{} (waiting {:?}), {} ranges already completed...",
545 attempt,
546 wait,
547 accumulated_completed.len()
548 );
549 tokio::time::sleep(wait).await;
550 }
551
552 let result = self
553 .execute_with_gaps(uri, total_length, &accumulated_completed)
554 .await;
555
556 if !result.completed_gaps.is_empty() {
557 tracing::info!(
558 "Attempt #{} completed {} gaps",
559 attempt + 1,
560 result.completed_gaps.len()
561 );
562 accumulated_completed.extend(result.completed_gaps);
563 accumulated_completed = Self::merge_ranges(&accumulated_completed);
564 }
565
566 if result.error.is_none() {
567 return Ok(());
568 }
569
570 tracing::warn!(
571 "Sequential download with gaps attempt #{} failed: {}",
572 attempt + 1,
573 result.error.as_ref().unwrap()
574 );
575 last_err = result.error;
576
577 if retry_policy.is_exhausted(attempt)
578 || !retry_policy.should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
579 {
580 break;
581 }
582 }
583
584 Err(last_err.unwrap_or_else(|| {
585 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
586 message: "All retries failed".into(),
587 })
588 }))
589 }
590
591 pub async fn execute_with_retry(
592 &mut self,
593 uri: &str,
594 resume_state: &ResumeState,
595 total_length: u64,
596 retry_policy: &RetryPolicy,
597 ) -> Result<()> {
598 let mut last_err = None;
599
600 for attempt in 0..=retry_policy.max_retries {
601 if attempt > 0
602 && let Some(wait) = retry_policy.compute_wait(attempt - 1)
603 {
604 tracing::info!(
605 "Sequential download retry #{} (waiting {:?})...",
606 attempt,
607 wait
608 );
609 tokio::time::sleep(wait).await;
610 }
611
612 match self.execute(uri, resume_state, total_length).await {
613 Ok(()) => return Ok(()),
614 Err(e) => {
615 tracing::warn!("Sequential download attempt #{} failed: {}", attempt + 1, e);
616 last_err = Some(e);
617 if retry_policy.is_exhausted(attempt)
618 || !retry_policy
619 .should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
620 {
621 break;
622 }
623 }
624 }
625 }
626
627 Err(last_err.unwrap_or_else(|| {
628 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
629 message: "All retries failed".into(),
630 })
631 }))
632 }
633}
634
635impl SequentialDownloader {
636 #[cfg(target_os = "linux")]
637 async fn try_splice_sequential(&mut self, uri: &str, total_length: u64) -> Result<()> {
638 let file = std::fs::OpenOptions::new()
639 .write(true)
640 .create(true)
641 .truncate(true)
642 .open(&self.output_path)?;
643
644 let bytes = crate::http::splice_http::try_splice_download(uri, 0, total_length, &file, 0)
645 .await
646 .map_err(|e| Aria2Error::Io(format!("splice download failed: {e}")))?;
647
648 let final_speed = {
649 let g = self.group.read().await;
650 let elapsed = g.elapsed_time().await;
651 match elapsed {
652 Some(d) if d.as_secs_f64() > 0.0 => (bytes as f64 / d.as_secs_f64()) as u64,
653 _ => 0,
654 }
655 };
656
657 {
658 let mut g = self.group.write().await;
659 g.set_total_length(bytes).await;
660 g.set_total_length_atomic(bytes);
661 g.update_progress(bytes).await;
662 g.set_completed_length(bytes);
663 g.update_speed(final_speed, 0).await;
664 g.set_download_speed_cached(final_speed);
665 g.complete().await?;
666 }
667
668 tracing::info!(
669 "Sequential download (splice) complete: {} ({} bytes)",
670 self.output_path.display(),
671 bytes
672 );
673 self.cookie_helper.save_cookies_if_configured();
674 Ok(())
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::SequentialDownloader;
681
682 #[test]
683 fn test_merge_ranges_empty() {
684 let ranges = &[];
685 let result = SequentialDownloader::merge_ranges(ranges);
686 assert!(result.is_empty());
687 }
688
689 #[test]
690 fn test_merge_ranges_single() {
691 let ranges = &[(0, 100)];
692 let result = SequentialDownloader::merge_ranges(ranges);
693 assert_eq!(result, vec![(0, 100)]);
694 }
695
696 #[test]
697 fn test_merge_ranges_non_overlapping_sorted() {
698 let ranges = &[(0, 100), (200, 100), (400, 100)];
699 let result = SequentialDownloader::merge_ranges(ranges);
700 assert_eq!(result, vec![(0, 100), (200, 100), (400, 100)]);
701 }
702
703 #[test]
704 fn test_merge_ranges_non_overlapping_unsorted() {
705 let ranges = &[(200, 100), (0, 100), (400, 100)];
706 let result = SequentialDownloader::merge_ranges(ranges);
707 assert_eq!(result, vec![(0, 100), (200, 100), (400, 100)]);
708 }
709
710 #[test]
711 fn test_merge_ranges_overlapping_inner() {
712 let ranges = &[(0, 200), (50, 50)];
713 let result = SequentialDownloader::merge_ranges(ranges);
714 assert_eq!(result, vec![(0, 200)]);
715 }
716
717 #[test]
718 fn test_merge_ranges_overlapping_inner_unsorted() {
719 let ranges = &[(50, 50), (0, 200)];
720 let result = SequentialDownloader::merge_ranges(ranges);
721 assert_eq!(result, vec![(0, 200)]);
722 }
723
724 #[test]
725 fn test_merge_ranges_overlapping_partial() {
726 let ranges = &[(0, 100), (50, 150)];
727 let result = SequentialDownloader::merge_ranges(ranges);
728 assert_eq!(result, vec![(0, 200)]);
729 }
730
731 #[test]
732 fn test_merge_ranges_overlapping_partial_unsorted() {
733 let ranges = &[(50, 150), (0, 100)];
734 let result = SequentialDownloader::merge_ranges(ranges);
735 assert_eq!(result, vec![(0, 200)]);
736 }
737
738 #[test]
739 fn test_merge_ranges_adjacent() {
740 let ranges = &[(0, 100), (100, 100)];
741 let result = SequentialDownloader::merge_ranges(ranges);
742 assert_eq!(result, vec![(0, 200)]);
743 }
744
745 #[test]
746 fn test_merge_ranges_adjacent_unsorted() {
747 let ranges = &[(100, 100), (0, 100)];
748 let result = SequentialDownloader::merge_ranges(ranges);
749 assert_eq!(result, vec![(0, 200)]);
750 }
751
752 #[test]
753 fn test_merge_ranges_duplicate() {
754 let ranges = &[(0, 100), (0, 100)];
755 let result = SequentialDownloader::merge_ranges(ranges);
756 assert_eq!(result, vec![(0, 100)]);
757 }
758
759 #[test]
760 fn test_merge_ranges_multiple_overlapping() {
761 let ranges = &[(0, 100), (50, 150), (200, 100), (180, 150)];
762 let result = SequentialDownloader::merge_ranges(ranges);
763 assert_eq!(result, vec![(0, 330)]);
764 }
765
766 #[test]
767 fn test_merge_ranges_zero_length() {
768 let ranges = &[(0, 0), (100, 0), (200, 100)];
769 let result = SequentialDownloader::merge_ranges(ranges);
770 assert_eq!(result, vec![(0, 0), (100, 0), (200, 100)]);
771 }
772
773 #[test]
774 fn test_merge_ranges_complex() {
775 let ranges = &[(10, 5), (0, 20), (15, 25), (50, 10), (45, 20), (100, 50)];
776 let result = SequentialDownloader::merge_ranges(ranges);
777 assert_eq!(result, vec![(0, 40), (45, 20), (100, 50)]);
778 }
779
780 #[test]
781 fn test_find_all_gaps_empty_ranges() {
782 let ranges = &[];
783 let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
784 assert_eq!(gaps, vec![(0, 1000)]);
785 }
786
787 #[test]
788 fn test_find_all_gaps_no_gaps() {
789 let ranges = &[(0, 1000)];
790 let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
791 assert!(gaps.is_empty());
792 }
793
794 #[test]
795 fn test_find_all_gaps_single_gap() {
796 let ranges = &[(0, 500), (600, 400)];
797 let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
798 assert_eq!(gaps, vec![(500, 100)]);
799 }
800
801 #[test]
802 fn test_find_all_gaps_multiple_gaps() {
803 let ranges = &[(0, 100), (200, 100), (400, 200)];
804 let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
805 assert_eq!(gaps, vec![(100, 100), (300, 100), (600, 400)]);
806 }
807
808 #[test]
809 fn test_find_all_gaps_overlapping_ranges() {
810 let ranges = &[(0, 200), (100, 150), (300, 50)];
811 let gaps = SequentialDownloader::find_all_gaps(ranges, 500);
812 assert_eq!(gaps, vec![(250, 50), (350, 150)]);
813 }
814}