use super::*; #[test] fn request_context_replace_uris_filters_blank_entries_and_duplicates() { let mut context = RequestContext::new(String::new()); assert_eq!(context.uri(), ""); assert!(context.uris().is_empty()); context.replace_uris(vec![ String::new(), "https://example.org/file.iso".to_owned(), "https://example.org/file.iso".to_owned(), " ".to_owned(), "https://mirror.example.org/file.iso".to_owned(), ]); assert_eq!(context.uri(), "https://example.org/file.iso"); assert_eq!( context.uris(), &[ "https://example.org/file.iso".to_owned(), "https://mirror.example.org/file.iso".to_owned(), ] ); } #[test] fn request_context_insert_and_append_reposition_existing_uris_without_duplicates() { let mut context = RequestContext::new("https://example.org/file.iso"); context.replace_uris(vec![ "https://example.org/file.iso".to_owned(), "https://mirror-a.example.org/file.iso".to_owned(), "https://mirror-b.example.org/file.iso".to_owned(), ]); context.insert_uri(0, "https://mirror-b.example.org/file.iso"); assert_eq!(context.uri(), "https://mirror-b.example.org/file.iso"); assert_eq!( context.uris(), &[ "https://mirror-b.example.org/file.iso".to_owned(), "https://example.org/file.iso".to_owned(), "https://mirror-a.example.org/file.iso".to_owned(), ] ); context.append_uri("https://example.org/file.iso"); context.append_uri(" "); assert_eq!(context.uri(), "https://mirror-b.example.org/file.iso"); assert_eq!( context.uris(), &[ "https://mirror-b.example.org/file.iso".to_owned(), "https://mirror-a.example.org/file.iso".to_owned(), "https://example.org/file.iso".to_owned(), ] ); } #[test] fn request_context_remove_last_uri_clears_primary_uri() { let mut context = RequestContext::new("https://example.org/last.iso"); assert!(context.remove_first_matching_uri("https://example.org/last.iso")); assert_eq!(context.uri(), ""); assert!(context.uris().is_empty()); assert!(!context.remove_first_matching_uri("https://example.org/last.iso")); } #[test] fn request_group_with_context_normalizes_stale_primary_and_uri_list() { let group = RequestGroup::with_context( DownloadId::new(0x77), RequestContext { source: None, uri: "https://stale.example.org/file.iso".to_owned(), uris: vec![ String::new(), "https://mirror-a.example.org/file.iso".to_owned(), "https://mirror-a.example.org/file.iso".to_owned(), "https://mirror-b.example.org/file.iso".to_owned(), ], referer: None, headers: Vec::new(), group_id: None, note: None, }, ); assert_eq!(group.uri(), "https://mirror-a.example.org/file.iso"); assert_eq!( group.uris(), &[ "https://mirror-a.example.org/file.iso".to_owned(), "https://mirror-b.example.org/file.iso".to_owned(), ] ); } #[test] fn request_group_tracks_retry_attempt_history() { let mut group = RequestGroup::new(DownloadId::new(0x1234), "https://example.test/file"); group.increment_retry_count(); group.push_retry_attempt(RetryAttempt { attempt: group.retry_count(), offset: 8192, length: Some(4096), error: Some("connection reset".to_string()), recoverable: true, }); assert_eq!(group.retry_count(), 1); let [attempt] = group.retry_attempts() else { panic!("retry_attempts should contain exactly one entry"); }; assert_eq!(attempt.offset, 8192); assert_eq!(attempt.length, Some(4096)); assert_eq!(attempt.error.as_deref(), Some("connection reset")); } #[test] fn request_group_resume_state_roundtrip() { let mut group = RequestGroup::new(DownloadId::new(0x66), "https://example.test/file"); group.set_resume_state(ResumeState { persisted: true, resume_offset: 32768, validated_length: Some(4096), segment_cursor: Some(PieceId(8)), }); let resume = group.resume_state().expect("resume state should exist"); assert!(resume.persisted); assert_eq!(resume.resume_offset, 32768); assert_eq!(resume.validated_length, Some(4096)); assert_eq!(resume.segment_cursor, Some(PieceId(8))); group.clear_resume_state(); assert!(group.resume_state().is_none()); } #[test] fn request_group_dht_token_roundtrip() { let mut group = RequestGroup::new(DownloadId::new(0x67), "magnet:?xt=urn:btih:token"); assert!(group.dht_token().is_none()); group.set_dht_token(Some(b"tok".to_vec())); assert_eq!(group.dht_token(), Some(&b"tok"[..])); group.set_dht_token(None); assert!(group.dht_token().is_none()); } #[test] fn request_group_clear_retry_attempts() { let mut group = RequestGroup::new(DownloadId::new(0x9), "https://example.test/file"); group.push_retry_attempt(RetryAttempt::new(1, 0)); group.push_retry_attempt(RetryAttempt::new(2, 1024)); assert_eq!(group.retry_attempts().len(), 2); group.clear_retry_attempts(); assert!(group.retry_attempts().is_empty()); } #[test] fn request_group_tracks_segment_assignments() { let mut group = RequestGroup::new(DownloadId::new(0xa), "https://example.test/file"); group.set_segment_assignments(vec![ SegmentAssignment::new(0, PieceRange::new(0, 1024)), SegmentAssignment::new(1, PieceRange::new(1024, 2048)), ]); assert_eq!(group.num_connections(), 2); let [_, second_assignment] = group.segment_assignments() else { panic!("segment_assignments should contain exactly two entries"); }; assert_eq!(second_assignment.range, PieceRange::new(1024, 2048)); group.clear_segment_assignments(); assert_eq!(group.num_connections(), 0); assert!(group.segment_assignments().is_empty()); } #[test] fn request_group_bt_runtime_state_roundtrip_with_full_payload() { let mut group = RequestGroup::new(DownloadId::new(0xb), "magnet:?xt=urn:btih:ABCDEF"); let bt = BtRuntimeState { info_hash: "0123456789ABCDEF0123456789ABCDEF01234567".to_owned(), name: Some("ubuntu.iso".to_owned()), magnet_uri: Some("magnet:?xt=urn:btih:0123456789ABCDEF0123456789ABCDEF01234567".to_owned()), metadata_only: true, metadata_size: Some(32_768), metadata_extension_ids: BTreeMap::from([("192.0.2.10:51413".to_owned(), 3_u8)]), metadata_piece_payloads: BTreeMap::from([(0_u32, b"metadata-piece-0".to_vec())]), creation_date: Some("2026-05-26T12:00:00Z".to_owned()), comment: Some("bt runtime".to_owned()), dht_nodes: vec!["router.bittorrent.com:6881".to_owned()], files: vec![ BtFileInfo { path: "ubuntu.iso".to_owned(), length: 2048, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "readme.txt".to_owned(), length: 128, piece_offset: Some(2), selected: false, }, ], trackers: vec![ BtTrackerInfo { url: "udp://tracker.example.org:6969/announce".to_owned(), tier: Some(0), id: Some("trk-0".to_owned()), seeders: Some(10), leechers: Some(3), }, BtTrackerInfo { url: "https://tracker2.example.org/announce".to_owned(), tier: Some(1), id: None, seeders: None, leechers: None, }, ], peers: vec![BtPeerInfo { peer_id: Some("-TR3000-ABCDEF123456".to_owned()), ip: "192.0.2.10".to_owned(), port: 51413, client_name: Some("Transmission".to_owned()), interested: true, choked: false, download_speed: 4096, upload_speed: 2048, seeder: false, }], }; group.set_bt(bt.clone()); let saved = group.bt().expect("bt runtime state should be set"); assert_eq!(saved, &bt); assert_eq!( saved.dht_nodes(), &["router.bittorrent.com:6881".to_owned()] ); assert_eq!(saved.files.len(), 2); assert_eq!(saved.trackers.len(), 2); assert_eq!(saved.peers.len(), 1); } #[test] fn request_group_bt_runtime_state_mutation_via_bt_mut() { let mut group = RequestGroup::new(DownloadId::new(0xc), "magnet:?xt=urn:btih:AAAA"); group.set_bt(BtRuntimeState { info_hash: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(), name: Some("seed".to_owned()), magnet_uri: Some("magnet:?xt=urn:btih:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned()), metadata_only: true, metadata_size: None, metadata_extension_ids: BTreeMap::new(), metadata_piece_payloads: BTreeMap::new(), creation_date: None, comment: None, dht_nodes: vec!["dht.transmissionbt.com:6881".to_owned()], files: vec![BtFileInfo { path: "seed.bin".to_owned(), length: 1, piece_offset: Some(0), selected: true, }], trackers: vec![], peers: vec![], }); let bt = group.bt_mut().expect("bt runtime state should be mutable"); bt.metadata_only = false; bt.comment = Some("metadata complete".to_owned()); bt.dht_nodes.push("router.utorrent.com:6881".to_owned()); bt.files.push(BtFileInfo { path: "extra.bin".to_owned(), length: 512, piece_offset: Some(1), selected: true, }); bt.trackers.push(BtTrackerInfo { url: "https://tracker.example.org/announce".to_owned(), tier: Some(0), id: Some("trk-a".to_owned()), seeders: Some(1), leechers: Some(0), }); bt.peers.push(BtPeerInfo { peer_id: None, ip: "198.51.100.20".to_owned(), port: 60000, client_name: None, interested: true, choked: true, download_speed: 0, upload_speed: 0, seeder: true, }); let after = group.bt().expect("bt runtime state should still exist"); assert!(!after.metadata_only); assert_eq!(after.comment.as_deref(), Some("metadata complete")); assert_eq!(after.dht_nodes.len(), 2); assert_eq!(after.files.len(), 2); assert_eq!(after.trackers.len(), 1); assert_eq!(after.peers.len(), 1); } #[test] fn request_group_bt_runtime_state_can_be_cleared() { let mut group = RequestGroup::new(DownloadId::new(0xd), "magnet:?xt=urn:btih:BBBB"); group.set_bt(BtRuntimeState { info_hash: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_owned(), name: None, magnet_uri: Some("magnet:?xt=urn:btih:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_owned()), metadata_only: true, metadata_size: None, metadata_extension_ids: BTreeMap::new(), metadata_piece_payloads: BTreeMap::new(), creation_date: None, comment: None, dht_nodes: Vec::new(), files: Vec::new(), trackers: Vec::new(), peers: Vec::new(), }); assert!(group.bt().is_some()); assert!(group.bt_mut().is_some()); group.clear_bt(); assert!(group.bt().is_none()); assert!(group.bt_mut().is_none()); } #[test] fn bt_runtime_state_reports_selected_file_helpers() { let bt = BtRuntimeState { info_hash: "0123456789abcdef0123456789abcdef01234567".to_owned(), name: Some("example".to_owned()), magnet_uri: None, metadata_only: false, metadata_size: None, metadata_extension_ids: BTreeMap::new(), metadata_piece_payloads: BTreeMap::new(), creation_date: None, comment: None, dht_nodes: vec![ "router.bittorrent.com:6881".to_owned(), "router.utorrent.com:6881".to_owned(), ], files: vec![ BtFileInfo { path: "selected.iso".to_owned(), length: 2048, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "ignored.txt".to_owned(), length: 128, piece_offset: Some(2048), selected: false, }, ], trackers: vec![], peers: vec![], }; let [first_file, ..] = bt.files() else { panic!("bt files should contain at least one entry"); }; assert!(first_file.is_selected()); assert_eq!(bt.dht_nodes().len(), 2); assert!(bt.has_selected_files()); assert_eq!(bt.selected_file_count(), 1); assert_eq!(bt.selected_total_length(), 2048); let selected_paths: Vec<_> = bt.selected_files().map(|file| file.path.as_str()).collect(); assert_eq!(selected_paths, vec!["selected.iso"]); } #[test] fn request_group_bt_share_state_roundtrip_and_accessors_work() { let mut group = RequestGroup::new(DownloadId::new(0xe), "magnet:?xt=urn:btih:CCCC"); group.set_bt_share_state(BtShareRuntimeState { seeding: true, share_ratio_milli: Some(1500), share_time_secs: 3600, seeding_time_secs: 900, seeding_started_at_secs: None, last_runtime_tick_secs: None, }); group.set_bt(BtRuntimeState { info_hash: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC".to_owned(), name: Some("payload".to_owned()), magnet_uri: None, metadata_only: false, metadata_size: None, metadata_extension_ids: BTreeMap::new(), metadata_piece_payloads: BTreeMap::new(), creation_date: None, comment: None, dht_nodes: vec!["router.bittorrent.com:6881".to_owned()], files: vec![BtFileInfo { path: "payload.bin".to_owned(), length: 4096, piece_offset: Some(0), selected: true, }], trackers: vec![], peers: vec![], }); assert!(group.bt_is_seeding()); assert_eq!(group.bt_share_ratio_milli(), Some(1500)); assert_eq!(group.bt_share_time_secs(), Some(3600)); assert_eq!(group.bt_seeding_time_secs(), Some(900)); assert_eq!(group.bt_selected_file_count(), Some(1)); assert_eq!(group.bt_selected_total_length(), Some(4096)); assert!(group.bt_has_selected_files()); let share = group .bt_share_state_mut() .expect("share state should exist"); share.add_share_time_secs(120); share.add_seeding_time_secs(30); share.set_seeding(false); assert!(!group.bt_is_seeding()); assert_eq!(group.bt_share_time_secs(), Some(3720)); assert_eq!(group.bt_seeding_time_secs(), Some(930)); group.clear_bt_share_state(); assert!(group.bt_share_state().is_none()); assert!(!group.bt_is_seeding()); } #[test] fn bt_share_runtime_state_tracks_seeding_runtime_and_ratio_derivation() { let mut state = BtShareRuntimeState::new(); state.start_seeding(100); state.tick_runtime(130); state.tick_runtime(170); state.stop_seeding(200); state.tick_runtime(250); state.refresh_share_ratio_from_lengths(6000, 4000); assert!(!state.is_seeding()); assert_eq!(state.share_time_secs(), 150); assert_eq!(state.seeding_time_secs(), 100); assert_eq!(state.share_ratio_milli(), Some(1500)); assert_eq!(BtShareRuntimeState::derive_share_ratio_milli(100, 0), None); } #[test] fn request_group_bt_runtime_tick_updates_true_seeding_and_share_runtime() { let mut group = RequestGroup::new(DownloadId::new(0x101), "magnet:?xt=urn:btih:RUNTIME"); group.set_total_length(2_048); group.set_completed_length(2_048); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![BtFileInfo { path: "payload.bin".to_owned(), length: 2_048, piece_offset: Some(0), selected: true, }], ..BtRuntimeState::default() }); let started = group.set_bt_seeding_state(true, Some(100)); assert!(group.bt_is_seeding()); assert!(group.bt_is_true_seeding()); assert!(started.seeding); assert_eq!(started.share_ratio_milli, Some(0)); let advanced = group.tick_bt_runtime_clock(130, true); assert_eq!(advanced.share_time_secs, 30); assert_eq!(advanced.seeding_time_secs, 30); assert!(advanced.seeding); let tick = group.apply_bt_runtime_tick(0, 512, 64, 128, 10, 10, true, Some(8)); assert_eq!(group.upload_length(), 512); assert_eq!(group.download_speed(), 64); assert_eq!(group.upload_speed(), 128); assert_eq!(group.num_connections(), 8); assert_eq!(tick.share_time_secs, 40); assert_eq!(tick.seeding_time_secs, 40); assert_eq!(tick.share_ratio_milli, Some(250)); assert!(tick.seeding); let stopped = group.tick_bt_runtime_clock(160, false); assert!(!group.bt_is_seeding()); assert!(!group.bt_is_true_seeding()); assert!(!stopped.seeding); assert_eq!(stopped.share_time_secs, 70); assert_eq!(stopped.seeding_time_secs, 70); } #[test] fn request_group_bt_share_ratio_base_length_follows_selected_payload() { let mut group = RequestGroup::new(DownloadId::new(0x102), "magnet:?xt=urn:btih:BASE"); group.set_total_length(10_000); group.set_completed_length(3_000); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![ BtFileInfo { path: "selected-a.bin".to_owned(), length: 2_000, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "selected-b.bin".to_owned(), length: 1_000, piece_offset: Some(2), selected: true, }, BtFileInfo { path: "ignored.bin".to_owned(), length: 7_000, piece_offset: Some(3), selected: false, }, ], ..BtRuntimeState::default() }); assert_eq!(group.bt_share_ratio_base_length(), Some(3_000)); group.set_completed_length(1_000); assert_eq!(group.bt_share_ratio_base_length(), Some(3_000)); group.set_completed_length(5_000); assert_eq!(group.bt_share_ratio_base_length(), Some(5_000)); } #[test] fn request_group_refresh_bt_share_runtime_uses_share_ratio_base_length() { let mut group = RequestGroup::new(DownloadId::new(0x103), "magnet:?xt=urn:btih:RATIO"); group.set_total_length(10_000); group.set_completed_length(6_000); group.set_upload_length(3_000); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![ BtFileInfo { path: "selected.bin".to_owned(), length: 4_000, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "ignored.bin".to_owned(), length: 6_000, piece_offset: Some(4), selected: false, }, ], ..BtRuntimeState::default() }); group.set_bt_share_state(BtShareRuntimeState::default()); let snapshot = group.refresh_bt_share_runtime(); assert_eq!(group.bt_share_ratio_base_length(), Some(6_000)); assert_eq!(snapshot.share_ratio_milli, Some(500)); assert_eq!(group.bt_share_ratio_milli(), Some(500)); } #[test] fn request_group_set_bt_refreshes_cached_share_ratio_after_selection_changes() { let mut group = RequestGroup::new(DownloadId::new(0x104), "magnet:?xt=urn:btih:SELECT"); group.set_total_length(10_000); group.set_completed_length(4_000); group.set_upload_length(2_000); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![ BtFileInfo { path: "disc-a.bin".to_owned(), length: 5_000, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "disc-b.bin".to_owned(), length: 5_000, piece_offset: Some(5), selected: true, }, ], ..BtRuntimeState::default() }); group.set_bt_share_state(BtShareRuntimeState::default()); assert_eq!( group.refresh_bt_share_runtime().share_ratio_milli, Some(200) ); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![ BtFileInfo { path: "disc-a.bin".to_owned(), length: 4_000, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "disc-b.bin".to_owned(), length: 6_000, piece_offset: Some(4), selected: false, }, ], ..BtRuntimeState::default() }); assert_eq!(group.bt_selected_total_length(), Some(4_000)); assert_eq!(group.bt_share_ratio_milli(), Some(500)); } #[test] fn request_group_set_upload_length_refreshes_cached_bt_share_ratio() { let mut group = RequestGroup::new(DownloadId::new(0x105), "magnet:?xt=urn:btih:UPLOAD"); group.set_total_length(4_000); group.set_completed_length(4_000); group.set_upload_length(1_000); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![BtFileInfo { path: "payload.bin".to_owned(), length: 4_000, piece_offset: Some(0), selected: true, }], ..BtRuntimeState::default() }); group.set_bt_share_state(BtShareRuntimeState::default()); assert_eq!( group.refresh_bt_share_runtime().share_ratio_milli, Some(250) ); group.set_upload_length(2_000); assert_eq!(group.bt_share_ratio_milli(), Some(500)); } #[test] fn request_group_bt_piece_helpers_report_counts_and_requestable_ids() { let mut group = RequestGroup::new(DownloadId::new(0xf), "magnet:?xt=urn:btih:DDDD"); group.set_piece_state(PieceId(0), PieceState::Verified); group.set_piece_state(PieceId(1), PieceState::Pending); group.set_piece_state(PieceId(2), PieceState::Queued); group.set_piece_state(PieceId(3), PieceState::Downloading); group.set_piece_state(PieceId(4), PieceState::Missing); group.set_piece_state(PieceId(5), PieceState::Skipped); assert_eq!(group.bt_verified_piece_count(), 1); assert_eq!(group.piece_state_counts(), (1, 1, 1, 1, 1, 1)); assert_eq!( group.bt_requestable_piece_ids(false, 8), vec![PieceId(1), PieceId(2), PieceId(4)] ); assert_eq!( group.bt_requestable_piece_ids(true, 8), vec![PieceId(1), PieceId(2), PieceId(4), PieceId(3)] ); } #[test] fn request_group_bt_effective_target_and_remaining_length_follow_selection() { let mut group = RequestGroup::new(DownloadId::new(0x10), "magnet:?xt=urn:btih:EEEE"); group.set_total_length(10_000); group.set_completed_length(4_000); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![ BtFileInfo { path: "wanted-a.bin".to_owned(), length: 3_000, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "wanted-b.bin".to_owned(), length: 2_000, piece_offset: Some(3), selected: true, }, BtFileInfo { path: "ignored.bin".to_owned(), length: 5_000, piece_offset: Some(5), selected: false, }, ], ..BtRuntimeState::default() }); assert_eq!(group.bt_effective_target_length(), Some(5_000)); assert_eq!(group.bt_remaining_work_length(), Some(1_000)); group.set_completed_length(9_000); assert_eq!(group.bt_remaining_work_length(), Some(0)); } #[test] fn request_group_bt_piece_block_update_tracks_piece_progress_and_selected_span() { let mut group = RequestGroup::new(DownloadId::new(0x11), "magnet:?xt=urn:btih:FFFF"); group.set_total_length(4_096); group.set_piece_length(1_024); group.set_bt(BtRuntimeState { metadata_only: false, files: vec![BtFileInfo { path: "wanted.bin".to_owned(), length: 2_500, piece_offset: Some(0), selected: true, }], ..BtRuntimeState::default() }); let partial = group.apply_bt_piece_block_update(BtPieceBlockUpdate { piece_id: PieceId(2), completed_blocks: 2, total_blocks: 4, }); assert_eq!(group.piece_state(PieceId(2)), Some(PieceState::Downloading)); assert_eq!(group.completed_length(), 0); assert_eq!(partial.completed_length_delta, 0); assert_eq!(partial.previous_state, None); assert_eq!(partial.next_state, PieceState::Downloading); assert_eq!(partial.piece_span_length, 452); assert_eq!(partial.block_completion_milli, 500); assert!(!partial.transitioned_to_verified); let verified = group.apply_bt_piece_block_update(BtPieceBlockUpdate { piece_id: PieceId(2), completed_blocks: 4, total_blocks: 4, }); assert_eq!(group.piece_state(PieceId(2)), Some(PieceState::Verified)); assert_eq!(group.completed_length(), 452); assert_eq!(verified.completed_length_delta, 452); assert_eq!(verified.previous_state, Some(PieceState::Downloading)); assert_eq!(verified.next_state, PieceState::Verified); assert_eq!(verified.block_completion_milli, 1000); assert!(verified.transitioned_to_verified); let missing = group.apply_bt_piece_block_update(BtPieceBlockUpdate { piece_id: PieceId(2), completed_blocks: 0, total_blocks: 0, }); assert_eq!(group.piece_state(PieceId(2)), Some(PieceState::Missing)); assert_eq!(group.completed_length(), 0); assert_eq!(missing.completed_length_delta, -452); assert_eq!(missing.previous_state, Some(PieceState::Verified)); assert_eq!(missing.next_state, PieceState::Missing); assert_eq!(missing.piece_span_length, 452); assert_eq!(missing.block_completion_milli, 0); } #[test] fn request_group_bt_peer_and_availability_updates_report_runtime_stats() { let mut group = RequestGroup::new(DownloadId::new(0x12), "magnet:?xt=urn:btih:9999"); group.set_piece_state(PieceId(0), PieceState::Missing); group.set_piece_state(PieceId(1), PieceState::Queued); group.set_piece_state(PieceId(2), PieceState::Verified); group.set_bt(BtRuntimeState { info_hash: "9999".to_owned(), metadata_only: false, ..BtRuntimeState::default() }); let available = group.apply_bt_piece_availability_update(BtPieceAvailabilityUpdate { piece_id: PieceId(0), peers_with_piece: 3, }); assert_eq!(available.available_piece_count, 1); assert_eq!(available.peers_with_piece, 3); assert!(available.piece_is_requestable); assert!(!available.piece_is_verified); let verified_piece = group.apply_bt_piece_availability_update(BtPieceAvailabilityUpdate { piece_id: PieceId(2), peers_with_piece: 5, }); assert_eq!(verified_piece.available_piece_count, 2); assert!(!verified_piece.piece_is_requestable); assert!(verified_piece.piece_is_verified); let peer_a = group.apply_bt_peer_update(BtPeerInfo { peer_id: Some("peer-a".to_owned()), ip: "127.0.0.1".to_owned(), port: 6881, client_name: Some("client-a".to_owned()), interested: true, choked: false, download_speed: 512, upload_speed: 64, seeder: false, }); assert_eq!(peer_a.peer_count, 1); assert_eq!(peer_a.seeder_count, 0); assert_eq!(peer_a.leecher_count, 1); assert_eq!(peer_a.total_download_speed, 512); assert_eq!(peer_a.total_upload_speed, 64); assert!(!peer_a.replaced_existing); let peer_b = group.apply_bt_peer_update(BtPeerInfo { peer_id: Some("peer-a".to_owned()), ip: "127.0.0.1".to_owned(), port: 6881, client_name: Some("client-a2".to_owned()), interested: false, choked: true, download_speed: 1_024, upload_speed: 256, seeder: true, }); assert_eq!(peer_b.peer_count, 1); assert_eq!(peer_b.seeder_count, 1); assert_eq!(peer_b.leecher_count, 0); assert_eq!(peer_b.total_download_speed, 1_024); assert_eq!(peer_b.total_upload_speed, 256); assert!(peer_b.replaced_existing); } #[test] fn request_group_segment_runtime_stats_capture_assignment_load() { let mut group = RequestGroup::new(DownloadId::new(0x13), "https://example.org/segments.bin"); group.set_segment_assignments(vec![ SegmentAssignment { slot: 0, range: PieceRange::new(0, 1024), completed_length: 512, state: SegmentState::Active, }, SegmentAssignment { slot: 1, range: PieceRange::new(1024, 2048), completed_length: 1024, state: SegmentState::Complete, }, SegmentAssignment { slot: 2, range: PieceRange::new(2048, 3072), completed_length: 128, state: SegmentState::Retrying, }, ]); let stats = group.segment_runtime_stats(); assert_eq!(stats.segment_count, 3); assert_eq!(stats.active_count, 1); assert_eq!(stats.retrying_count, 1); assert_eq!(stats.complete_count, 1); assert_eq!(stats.planned_bytes, 3072); assert_eq!(stats.completed_bytes, 1664); assert_eq!(stats.remaining_bytes, 1408); assert_eq!(stats.covered_range, Some(PieceRange::new(0, 3072))); } #[test] fn request_group_bt_pressure_snapshot_reports_requestable_and_scarcity() { let mut group = RequestGroup::new(DownloadId::new(0x14), "magnet:?xt=urn:btih:PRESSURE2"); group.set_piece_state(PieceId(0), PieceState::Verified); group.set_piece_state(PieceId(1), PieceState::Pending); group.set_piece_state(PieceId(2), PieceState::Downloading); group.set_piece_state(PieceId(3), PieceState::Queued); group.set_piece_state(PieceId(4), PieceState::Missing); group.set_bt(BtRuntimeState { metadata_only: false, peers: vec![ BtPeerInfo { peer_id: Some("peer-a".to_owned()), ip: "198.51.100.10".to_owned(), port: 6881, client_name: None, interested: true, choked: false, download_speed: 256, upload_speed: 64, seeder: false, }, BtPeerInfo { peer_id: Some("peer-b".to_owned()), ip: "198.51.100.11".to_owned(), port: 6882, client_name: None, interested: false, choked: true, download_speed: 0, upload_speed: 32, seeder: true, }, ], ..BtRuntimeState::default() }); group.apply_bt_piece_availability_update(BtPieceAvailabilityUpdate { piece_id: PieceId(1), peers_with_piece: 1, }); group.apply_bt_piece_availability_update(BtPieceAvailabilityUpdate { piece_id: PieceId(3), peers_with_piece: 3, }); let pressure = group .bt_pressure_snapshot() .expect("bt pressure snapshot should exist"); assert_eq!(pressure.total_pieces, 5); assert_eq!(pressure.requestable_pieces, 3); assert_eq!(pressure.active_pieces, 2); assert_eq!(pressure.available_requestable_pieces, 2); assert_eq!(pressure.scarce_requestable_pieces, 1); assert_eq!(pressure.peer_count, 2); assert_eq!(pressure.seeder_count, 1); assert_eq!(pressure.leecher_count, 1); }