use super::*; #[test] fn execute_runtime_retries_http_failure_then_completes() { let downloader = SequencedHttpDownloader::new(vec![ Ok(HttpResponseModel { status: 503, reason: "Service Unavailable".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: Vec::new(), }, body: ResponseBody::Empty, content_range: None, partial_content: false, checksum: None, redirected_from: None, }), Ok(HttpResponseModel { status: 200, reason: "OK".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: vec![aria2_rust_pro_protocol::HttpHeader { name: "content-length".to_owned(), value: "8".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }], }, body: ResponseBody::Inline(b"aaaaaaaa".to_vec()), content_range: None, partial_content: false, checksum: None, redirected_from: None, }), ]); let temp_dir = std::env::temp_dir().join("aria2-rust-pro-cli-retry-test"); let _ = fs::create_dir_all(&temp_dir); let config_path = temp_dir.join("aria2.conf"); fs::write(&config_path, "retry-wait=1\nmax-tries=2\n").expect("config should write"); let report = execute_runtime_with_downloader( Invocation::Run { config_path: Some(config_path.clone()), uris: vec!["http://example.com/retry.bin".to_owned()], }, &downloader, ) .expect("runtime should recover via retry"); assert_eq!(report.completed_download_count, 1); assert_eq!(report.first_status.as_deref(), Some("complete")); assert_eq!(report.first_completed_length, Some(8)); assert_eq!(report.first_connections, Some(1)); let _ = fs::remove_file(config_path); } #[test] fn execute_runtime_accepts_partial_content_as_complete_progress() { let downloader = SequencedHttpDownloader::new(vec![Ok(HttpResponseModel { status: 206, reason: "Partial Content".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: vec![ aria2_rust_pro_protocol::HttpHeader { name: "content-length".to_owned(), value: "5".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, aria2_rust_pro_protocol::HttpHeader { name: "content-range".to_owned(), value: "bytes 0-4/10".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, ], }, body: ResponseBody::Inline(b"12345".to_vec()), content_range: Some(aria2_rust_pro_protocol::ContentRangeSpec { unit: aria2_rust_pro_protocol::RangeUnit::Bytes, start: 0, end_inclusive: 4, total_size: Some(10), unsatisfied: false, }), partial_content: true, checksum: None, redirected_from: None, })]); let temp_dir = std::env::temp_dir().join("aria2-rust-pro-cli-partial-single-test"); let _ = fs::create_dir_all(&temp_dir); let config_path = temp_dir.join("aria2.conf"); fs::write(&config_path, "split=1\n").expect("config should write"); let report = execute_runtime_with_downloader( Invocation::Run { config_path: Some(config_path.clone()), uris: vec!["http://example.com/partial.bin".to_owned()], }, &downloader, ) .expect("runtime should accept 206 transfer"); assert_eq!(report.completed_download_count, 0); assert_eq!(report.first_status.as_deref(), Some("active")); assert_eq!(report.first_total_length, Some(10)); assert_eq!(report.first_completed_length, Some(5)); assert_eq!(report.first_connections, Some(1)); let _ = fs::remove_file(config_path); } #[expect( clippy::too_many_lines, reason = "partial-range regression fixture keeps the response sequence and assertions together" )] #[test] fn execute_runtime_advances_multi_step_partial_ranges_until_complete() { let downloader = SequencedHttpDownloader::new(vec![ Ok(HttpResponseModel { status: 206, reason: "Partial Content".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: vec![ aria2_rust_pro_protocol::HttpHeader { name: "content-length".to_owned(), value: "4".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, aria2_rust_pro_protocol::HttpHeader { name: "content-range".to_owned(), value: "bytes 0-3/10".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, ], }, body: ResponseBody::Inline(b"1234".to_vec()), content_range: Some(aria2_rust_pro_protocol::ContentRangeSpec { unit: aria2_rust_pro_protocol::RangeUnit::Bytes, start: 0, end_inclusive: 3, total_size: Some(10), unsatisfied: false, }), partial_content: true, checksum: None, redirected_from: None, }), Ok(HttpResponseModel { status: 206, reason: "Partial Content".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: vec![ aria2_rust_pro_protocol::HttpHeader { name: "content-length".to_owned(), value: "3".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, aria2_rust_pro_protocol::HttpHeader { name: "content-range".to_owned(), value: "bytes 4-6/10".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, ], }, body: ResponseBody::Inline(b"567".to_vec()), content_range: Some(aria2_rust_pro_protocol::ContentRangeSpec { unit: aria2_rust_pro_protocol::RangeUnit::Bytes, start: 4, end_inclusive: 6, total_size: Some(10), unsatisfied: false, }), partial_content: true, checksum: None, redirected_from: None, }), Ok(HttpResponseModel { status: 206, reason: "Partial Content".to_owned(), version: HttpVersion::Http11, headers: HttpResponseHeaders { headers: vec![ aria2_rust_pro_protocol::HttpHeader { name: "content-length".to_owned(), value: "2".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, aria2_rust_pro_protocol::HttpHeader { name: "content-range".to_owned(), value: "bytes 8-9/10".to_owned(), kind: aria2_rust_pro_protocol::HeaderKind::Response, }, ], }, body: ResponseBody::Inline(b"90".to_vec()), content_range: Some(aria2_rust_pro_protocol::ContentRangeSpec { unit: aria2_rust_pro_protocol::RangeUnit::Bytes, start: 8, end_inclusive: 9, total_size: Some(10), unsatisfied: false, }), partial_content: true, checksum: None, redirected_from: None, }), ]); let temp_dir = std::env::temp_dir().join("aria2-rust-pro-cli-multipart-test"); let _ = fs::create_dir_all(&temp_dir); let config_path = temp_dir.join("aria2.conf"); fs::write( &config_path, "max-tries=3\nsplit=3\nmin-split-size=4\npiece-length=4\n", ) .expect("config should write"); let report = execute_runtime_with_downloader( Invocation::Run { config_path: Some(config_path.clone()), uris: vec!["http://example.com/multipart.bin".to_owned()], }, &downloader, ) .expect("runtime should advance through multiple partial segments"); assert_eq!(report.completed_download_count, 1); assert_eq!(report.first_status.as_deref(), Some("complete")); assert_eq!(report.first_total_length, Some(10)); assert_eq!(report.first_completed_length, Some(10)); assert_eq!(report.first_connections, Some(1)); let recorded = downloader.recorded_requests(); let [bootstrap, first_followup, second_followup] = recorded.as_slice() else { panic!("expected exactly three recorded requests"); }; assert_eq!(bootstrap.request.range, None); let mut followup_ranges = [first_followup, second_followup] .iter() .map(|task| task.request.range) .collect::>(); followup_ranges.sort_by_key(|range| range.as_ref().map(|range| range.start)); assert_eq!( followup_ranges, vec![ Some(aria2_rust_pro_protocol::RangeSpec { start: 4, end_inclusive: Some(9), unit: aria2_rust_pro_protocol::RangeUnit::Bytes, }), Some(aria2_rust_pro_protocol::RangeSpec { start: 7, end_inclusive: Some(9), unit: aria2_rust_pro_protocol::RangeUnit::Bytes, }), ] ); assert_eq!(bootstrap.retry_attempts.len(), 0); assert_eq!(first_followup.retry_attempts.len(), 0); assert_eq!(second_followup.retry_attempts.len(), 1); assert!( first_followup .retry_attempts .iter() .all(|attempt| attempt.status == Some(206)) ); assert!( second_followup .retry_attempts .iter() .all(|attempt| attempt.status == Some(206)) ); let _ = fs::remove_file(config_path); }