use super::*; use crate::{ piece::{PieceId, PieceState}, request::{BtFileInfo, BtPeerInfo, BtRuntimeState, DownloadId}, }; #[test] fn segment_plan_uses_runtime_limits() { let runtime = RuntimeConfig { split: 8, max_connections_per_server: 3, max_connection_per_server: 2, min_split_size: 1024, ..RuntimeConfig::default() }; let plan = SegmentPlan::from_runtime(&runtime); assert_eq!(plan.split, 8); assert_eq!(plan.max_connections_per_server, 3); assert_eq!(plan.min_split_size, 1024); } #[test] fn plan_active_segments_respects_remaining_size_and_limits() { let scheduler = Scheduler::new(); let runtime = RuntimeConfig { split: 6, max_connections_per_server: 4, max_connection_per_server: 4, min_split_size: 1024, ..RuntimeConfig::default() }; let mut group = RequestGroup::new(DownloadId::new(1), "https://example.org/file.bin"); group.set_status(DownloadStatus::Active); group.set_total_length(10 * 1024); group.set_completed_length(2 * 1024); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 4); group.set_completed_length(9 * 1024 + 900); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 1); group.set_completed_length(group.total_length()); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 0); } #[test] fn plan_active_segments_returns_zero_for_non_runnable_states() { let scheduler = Scheduler::new(); let runtime = RuntimeConfig::default(); let mut group = RequestGroup::new(DownloadId::new(2), "https://example.org/file.bin"); group.set_total_length(2048); group.set_completed_length(0); group.set_status(DownloadStatus::Paused); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 0); group.set_status(DownloadStatus::Error); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 0); } #[test] fn plan_active_segments_returns_zero_for_bt_metadata_only() { let scheduler = Scheduler::new(); let runtime = RuntimeConfig { split: 4, max_connections_per_server: 4, max_connection_per_server: 4, min_split_size: 1024, ..RuntimeConfig::default() }; let mut group = RequestGroup::new(DownloadId::new(3), "magnet:?xt=urn:btih:ABC"); group.set_status(DownloadStatus::Active); group.set_total_length(8 * 1024); group.set_completed_length(1024); group.set_bt(BtRuntimeState { metadata_only: true, ..BtRuntimeState::default() }); assert_eq!(scheduler.plan_active_segments(&group, &runtime), 0); } #[test] fn plan_active_segments_uses_bt_selected_files_length() { let scheduler = Scheduler::new(); let runtime = RuntimeConfig { split: 8, max_connections_per_server: 8, max_connection_per_server: 8, min_split_size: 1024, ..RuntimeConfig::default() }; let mut group = RequestGroup::new(DownloadId::new(4), "magnet:?xt=urn:btih:DEF"); group.set_status(DownloadStatus::Active); group.set_total_length(10 * 1024); group.set_completed_length(3500); group.set_bt(BtRuntimeState { files: vec![ BtFileInfo { path: "wanted.bin".to_owned(), length: 4 * 1024, piece_offset: Some(0), selected: true, }, BtFileInfo { path: "unwanted.bin".to_owned(), length: 6 * 1024, piece_offset: Some(4), selected: false, }, ], ..BtRuntimeState::default() }); // Selected total is 4096; after completed 3500 only 596 bytes remain, // so the scheduler should avoid over-planning and keep a single segment. assert_eq!(scheduler.plan_active_segments(&group, &runtime), 1); } #[test] fn scheduler_selects_bt_piece_candidates_and_endgame_behavior() { let scheduler = Scheduler::new(); let mut group = RequestGroup::new(DownloadId::new(5), "magnet:?xt=urn:btih:FFF"); group.set_piece_state(PieceId(0), PieceState::Verified); group.set_piece_state(PieceId(1), PieceState::Pending); group.set_piece_state(PieceId(2), PieceState::Missing); group.set_piece_state(PieceId(3), PieceState::Downloading); group.set_piece_state(PieceId(4), PieceState::Queued); let normal = scheduler.select_bt_piece_candidates( &group, BtPieceSelectionOptions { max_candidates: 8, include_downloading_in_endgame: true, endgame_mode: false, }, ); assert_eq!(normal, vec![PieceId(1), PieceId(2), PieceId(4)]); let endgame = scheduler.select_bt_piece_candidates( &group, BtPieceSelectionOptions { max_candidates: 8, include_downloading_in_endgame: true, endgame_mode: true, }, ); assert_eq!( endgame, vec![PieceId(1), PieceId(2), PieceId(4), PieceId(3)] ); } #[test] fn scheduler_plans_bt_piece_ranges_from_runtime_piece_length() { let scheduler = Scheduler::new(); let runtime = RuntimeConfig { piece_length: 1024, ..RuntimeConfig::default() }; let mut group = RequestGroup::new(DownloadId::new(6), "magnet:?xt=urn:btih:GGG"); group.set_piece_state(PieceId(2), PieceState::Pending); group.set_piece_state(PieceId(5), PieceState::Missing); let ranges = scheduler.plan_bt_piece_request_ranges( &group, &runtime, BtPieceSelectionOptions { max_candidates: 2, include_downloading_in_endgame: false, endgame_mode: false, }, ); assert_eq!( ranges, vec![PieceRange::new(2048, 3072), PieceRange::new(5120, 6144)] ); } #[test] fn runtime_schedule_state_reports_endgame_readiness() { assert!(RuntimeScheduleState::is_endgame_ready(1, 3)); assert!(RuntimeScheduleState::is_endgame_ready(3, 3)); assert!(!RuntimeScheduleState::is_endgame_ready(4, 3)); assert!(!RuntimeScheduleState::is_endgame_ready(0, 3)); } #[test] fn scheduler_activity_counters_track_ticks_and_decisions() { let mut scheduler = Scheduler::new(); assert_eq!(scheduler.activity_counters().tick_count, 0); assert_eq!(scheduler.activity_counters().schedule_run_count, 0); scheduler.record_schedule_run(); let _ = scheduler.tick(); scheduler.record_decision(&ScheduleDecision::Queue(DownloadId::new(0x21))); scheduler.record_decision(&ScheduleDecision::RunNow(DownloadId::new(0x21))); scheduler.record_decision(&ScheduleDecision::RetryLater(DownloadId::new(0x21))); scheduler.record_decision(&ScheduleDecision::Noop); let counters = scheduler.activity_counters(); assert_eq!(counters.tick_count, 1); assert_eq!(counters.schedule_run_count, 1); assert_eq!(counters.queue_decision_count, 1); assert_eq!(counters.run_now_decision_count, 1); assert_eq!(counters.retry_later_decision_count, 1); assert_eq!(counters.noop_decision_count, 1); assert_eq!(counters.last_decision, Some(ScheduleDecisionKind::Noop)); } #[test] fn scheduler_records_last_planning_observation_for_bt_pressure() { let mut scheduler = Scheduler::new(); let runtime = RuntimeConfig { split: 5, max_connections_per_server: 3, max_connection_per_server: 3, min_split_size: 1024, ..RuntimeConfig::default() }; let mut group = RequestGroup::new(DownloadId::new(0x22), "magnet:?xt=urn:btih:PRESSURE"); group.set_status(DownloadStatus::Active); group.set_total_length(6 * 1024); group.set_completed_length(1024); 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, files: vec![BtFileInfo { path: "payload.bin".to_owned(), length: 6 * 1024, piece_offset: Some(0), selected: true, }], peers: vec![ BtPeerInfo { peer_id: Some("peer-a".to_owned()), ip: "192.0.2.1".to_owned(), port: 6881, client_name: None, interested: true, choked: false, download_speed: 64, upload_speed: 32, seeder: false, }, BtPeerInfo { peer_id: Some("peer-b".to_owned()), ip: "192.0.2.2".to_owned(), port: 6882, client_name: None, interested: false, choked: true, download_speed: 0, upload_speed: 16, seeder: true, }, ], ..BtRuntimeState::default() }); group.apply_bt_piece_availability_update(crate::request::BtPieceAvailabilityUpdate { piece_id: PieceId(1), peers_with_piece: 1, }); group.apply_bt_piece_availability_update(crate::request::BtPieceAvailabilityUpdate { piece_id: PieceId(3), peers_with_piece: 2, }); let planned = scheduler.plan_active_segments(&group, &runtime); scheduler.observe_plan(&group, &runtime, planned); let observation = scheduler .last_planning_observation() .expect("planning observation should be recorded"); assert_eq!(observation.gid, DownloadId::new(0x22)); assert_eq!(observation.planned_segments, 3); assert_eq!(observation.remaining_bytes, 5 * 1024); assert_eq!(observation.requestable_pieces, 3); assert_eq!(observation.active_piece_count, 2); assert_eq!(observation.available_requestable_pieces, 2); assert_eq!(observation.scarce_requestable_pieces, 1); assert_eq!(observation.peer_count, 2); assert!(observation.bt_endgame_ready); }