930 lines
32 KiB
Rust
930 lines
32 KiB
Rust
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);
|
|
}
|