288 lines
9.9 KiB
Rust
288 lines
9.9 KiB
Rust
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);
|
|
}
|