1528 lines
51 KiB
Rust
1528 lines
51 KiB
Rust
use std::{
|
|
fs,
|
|
path::PathBuf,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use aria2_rust_pro_storage::{load_session_file, save_session_file};
|
|
|
|
use crate::{
|
|
engine::{DownloadEngine, QueuePositionMode},
|
|
error::{CoreError, Result},
|
|
piece::{PieceId, PieceState},
|
|
request::{
|
|
BtFileInfo, BtPeerInfo, BtRuntimeState, BtTrackerInfo, DownloadStatus, RequestGroup,
|
|
ResumeState, SegmentState,
|
|
},
|
|
runtime::RuntimeConfig,
|
|
scheduler::ScheduleDecision,
|
|
session::SaveSessionTarget,
|
|
};
|
|
|
|
fn temp_session_path(name: &str) -> PathBuf {
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("clock should be monotonic enough for test naming")
|
|
.as_nanos();
|
|
let root = std::env::temp_dir().join(format!(
|
|
"aria2-rust-pro-core-test-{}-{nanos}",
|
|
std::process::id()
|
|
));
|
|
fs::create_dir_all(&root).expect("temp dir should be creatable");
|
|
root.join(name)
|
|
}
|
|
|
|
#[test]
|
|
fn engine_load_session_restores_saved_options() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let path = PathBuf::from("engine-session.txt");
|
|
|
|
engine.set_option("max-download-result", "500");
|
|
engine.save_session(SaveSessionTarget::Path(path.clone()))?;
|
|
|
|
engine.set_option("max-download-result", "10");
|
|
engine.load_session(SaveSessionTarget::Path(path))?;
|
|
|
|
assert_eq!(
|
|
engine
|
|
.session()
|
|
.global_options()
|
|
.get(&"max-download-result".into())
|
|
.and_then(|value| value.as_text()),
|
|
Some("500")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn resume_moves_paused_download_back_to_waiting() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("https://example.org/resume.bin").gid();
|
|
engine.pause(gid).expect("pause should succeed");
|
|
engine.resume(gid).expect("resume should succeed");
|
|
|
|
let group = engine
|
|
.registry()
|
|
.get(gid)
|
|
.expect("group should still exist after resume");
|
|
assert_eq!(group.status(), &DownloadStatus::Waiting);
|
|
}
|
|
|
|
#[test]
|
|
fn resume_rejects_non_paused_downloads() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("https://example.org/not-paused.bin").gid();
|
|
|
|
assert_eq!(
|
|
engine.resume(gid),
|
|
Err(CoreError::InvalidState("download cannot be unpaused now"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn engine_save_session_writes_session_and_control_files() -> Result<()> {
|
|
let session_path = temp_session_path("session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut engine = DownloadEngine::with_runtime(runtime);
|
|
let gid = engine.add_uri("https://example.org/files/ubuntu.iso").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_option("dir", "D:/downloads");
|
|
group.set_option("out", "ubuntu.iso");
|
|
group.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group.set_piece_state(PieceId(1), PieceState::Downloading);
|
|
|
|
engine.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let session_file = load_session_file(&session_path).expect("session file should load");
|
|
assert_eq!(session_file.entries.len(), 1);
|
|
let entry = session_file
|
|
.entries
|
|
.first()
|
|
.expect("session file should contain one entry");
|
|
assert_eq!(entry.uri, "https://example.org/files/ubuntu.iso");
|
|
assert_eq!(
|
|
entry.uris,
|
|
vec!["https://example.org/files/ubuntu.iso".to_owned()]
|
|
);
|
|
assert_eq!(
|
|
entry.target_path,
|
|
PathBuf::from("D:/downloads").join("ubuntu.iso")
|
|
);
|
|
let control_path = entry
|
|
.metadata_path
|
|
.clone()
|
|
.expect("control metadata path should be present");
|
|
assert!(control_path.exists());
|
|
|
|
let control = aria2_rust_pro_storage::read_aria2_control_file(&control_path)
|
|
.expect("control file should load");
|
|
assert_eq!(
|
|
control
|
|
.files
|
|
.first()
|
|
.expect("control file should contain one file entry")
|
|
.path,
|
|
PathBuf::from("D:/downloads").join("ubuntu.iso")
|
|
);
|
|
assert_eq!(control.piece_states.len(), 2);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn engine_save_and_load_session_round_trips_multiple_uris() -> Result<()> {
|
|
let session_path = temp_session_path("multi-uri-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("https://example.org/rebuild/file.bin").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.context_mut().replace_uris(vec![
|
|
"https://example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror1.example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror2.example.org/rebuild/file.bin".to_owned(),
|
|
]);
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let session_file = load_session_file(&session_path).expect("session file should load");
|
|
assert_eq!(
|
|
session_file
|
|
.entries
|
|
.first()
|
|
.expect("session file should contain one entry")
|
|
.uris,
|
|
vec![
|
|
"https://example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror1.example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror2.example.org/rebuild/file.bin".to_owned(),
|
|
]
|
|
);
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
assert_eq!(
|
|
loaded.uris(),
|
|
&[
|
|
"https://example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror1.example.org/rebuild/file.bin".to_owned(),
|
|
"https://mirror2.example.org/rebuild/file.bin".to_owned(),
|
|
]
|
|
);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn engine_load_session_rebuilds_registry_from_saved_file() -> Result<()> {
|
|
let session_path = temp_session_path("reload-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("https://example.org/rebuild/file.bin").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_option("dir", "C:/aria2-work");
|
|
group.set_option("out", "file.bin");
|
|
group.set_status(DownloadStatus::Paused);
|
|
group.set_piece_state(PieceId(3), PieceState::Verified);
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
assert_eq!(reader.registry().len(), 1);
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
assert_eq!(loaded.uri(), "https://example.org/rebuild/file.bin");
|
|
assert_eq!(loaded.status(), &DownloadStatus::Paused);
|
|
assert_eq!(
|
|
loaded
|
|
.options()
|
|
.get(&"out".into())
|
|
.and_then(|value| value.as_text()),
|
|
Some("file.bin")
|
|
);
|
|
assert_eq!(loaded.piece_state(PieceId(3)), Some(PieceState::Verified));
|
|
assert_eq!(reader.progress_snapshot(loaded_gid)?.completed_length, 1024);
|
|
assert_eq!(reader.progress_snapshot(loaded_gid)?.total_length, 4096);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_derives_lengths_and_eta_from_runtime_state() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("https://example.org/runtime.bin").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_piece_length(1024);
|
|
group.set_total_length(3072);
|
|
group.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group.set_piece_state(PieceId(1), PieceState::Downloading);
|
|
group.set_completed_length(1536);
|
|
group.set_download_speed(512);
|
|
group.set_upload_length(128);
|
|
group.set_num_connections(3);
|
|
group.set_status(DownloadStatus::Active);
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(snapshot.total_length, 3072);
|
|
assert_eq!(snapshot.completed_length, 1536);
|
|
assert_eq!(snapshot.download_speed, 512);
|
|
assert_eq!(snapshot.upload_length, 128);
|
|
assert_eq!(snapshot.num_connections, 3);
|
|
assert_eq!(snapshot.eta_seconds, Some(3));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn global_stat_sums_snapshot_lengths_and_speeds() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid1 = engine.add_uri("https://example.org/a.bin").gid();
|
|
let gid2 = engine.add_uri("https://example.org/b.bin").gid();
|
|
|
|
let group1 = engine.handle_mut(gid1).expect("group 1 should exist");
|
|
group1.set_piece_length(1024);
|
|
group1.set_total_length(2048);
|
|
group1.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group1.set_completed_length(1536);
|
|
group1.set_download_speed(100);
|
|
group1.set_status(DownloadStatus::Active);
|
|
|
|
let group2 = engine.handle_mut(gid2).expect("group 2 should exist");
|
|
group2.set_piece_length(1024);
|
|
group2.set_total_length(1024);
|
|
group2.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group2.set_completed_length(1024);
|
|
group2.set_download_speed(50);
|
|
group2.set_upload_length(25);
|
|
group2.set_upload_speed(25);
|
|
group2.set_status(DownloadStatus::Paused);
|
|
|
|
let stat = engine.get_global_stat();
|
|
assert_eq!(stat.total_length, 3072);
|
|
assert_eq!(stat.completed_length, 2560);
|
|
assert_eq!(stat.download_speed, 150);
|
|
assert_eq!(stat.upload_speed, 25);
|
|
}
|
|
|
|
#[test]
|
|
fn tell_waiting_includes_paused_and_tell_stopped_excludes_paused() {
|
|
let mut engine = DownloadEngine::new();
|
|
let waiting_gid = engine.add_uri("https://example.org/waiting.bin").gid();
|
|
let paused_gid = engine.add_uri("https://example.org/paused.bin").gid();
|
|
let complete_gid = engine.add_uri("https://example.org/complete.bin").gid();
|
|
|
|
engine.pause(paused_gid).expect("pause should succeed");
|
|
engine
|
|
.complete(complete_gid)
|
|
.expect("complete should succeed");
|
|
|
|
let waiting = engine
|
|
.tell_waiting()
|
|
.into_iter()
|
|
.map(super::DownloadHandle::gid)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(waiting.len(), 2);
|
|
assert!(waiting.contains(&waiting_gid));
|
|
assert!(waiting.contains(&paused_gid));
|
|
|
|
let stopped = engine
|
|
.tell_stopped()
|
|
.into_iter()
|
|
.map(super::DownloadHandle::gid)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(stopped, vec![complete_gid]);
|
|
}
|
|
|
|
#[test]
|
|
fn change_position_reorders_waiting_queue_with_set_cur_and_end_modes() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid0 = engine.add_uri("https://example.org/0.bin").gid();
|
|
let gid1 = engine.add_uri("https://example.org/1.bin").gid();
|
|
let gid2 = engine.add_uri("https://example.org/2.bin").gid();
|
|
let gid3 = engine.add_uri("https://example.org/3.bin").gid();
|
|
let gid4 = engine.add_uri("https://example.org/4.bin").gid();
|
|
|
|
assert_eq!(
|
|
engine
|
|
.change_position(gid1, 4, QueuePositionMode::Set)
|
|
.expect("set move should succeed"),
|
|
4
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.change_position(gid2, 3, QueuePositionMode::Set)
|
|
.expect("set move should succeed"),
|
|
3
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.change_position(gid2, 1, QueuePositionMode::Set)
|
|
.expect("set move should succeed"),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.change_position(gid1, 1, QueuePositionMode::Cur)
|
|
.expect("cur move should succeed"),
|
|
4
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.change_position(gid0, -2, QueuePositionMode::End)
|
|
.expect("end move should succeed"),
|
|
2
|
|
);
|
|
|
|
let waiting = engine
|
|
.tell_waiting()
|
|
.into_iter()
|
|
.map(super::DownloadHandle::gid)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(waiting, vec![gid2, gid3, gid0, gid4, gid1]);
|
|
}
|
|
|
|
#[test]
|
|
fn pause_active_download_moves_it_to_front_of_waiting_queue() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid0 = engine.add_uri("https://example.org/0.bin").gid();
|
|
let gid1 = engine.add_uri("https://example.org/1.bin").gid();
|
|
let _ = engine.schedule_once();
|
|
|
|
engine.pause(gid0).expect("pause should succeed");
|
|
|
|
let waiting = engine
|
|
.tell_waiting()
|
|
.into_iter()
|
|
.map(super::DownloadHandle::gid)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(waiting, vec![gid0, gid1]);
|
|
}
|
|
|
|
#[test]
|
|
fn pause_rejects_already_paused_downloads() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine
|
|
.add_uri("https://example.org/already-paused.bin")
|
|
.gid();
|
|
engine.pause(gid).expect("initial pause should succeed");
|
|
|
|
assert_eq!(
|
|
engine.pause(gid),
|
|
Err(CoreError::InvalidState("download cannot be paused now"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_download_result_only_removes_stopped_entries() {
|
|
let mut engine = DownloadEngine::new();
|
|
let waiting_gid = engine.add_uri("https://example.org/waiting.bin").gid();
|
|
let complete_gid = engine.add_uri("https://example.org/complete.bin").gid();
|
|
let error_gid = engine.add_uri("https://example.org/error.bin").gid();
|
|
|
|
engine
|
|
.complete(complete_gid)
|
|
.expect("complete transition should succeed");
|
|
engine
|
|
.fail(error_gid)
|
|
.expect("error transition should succeed");
|
|
|
|
engine
|
|
.remove_download_result(complete_gid)
|
|
.expect("stopped result should be removable");
|
|
|
|
assert!(engine.registry().get(complete_gid).is_none());
|
|
assert!(engine.registry().get(error_gid).is_some());
|
|
assert!(engine.registry().get(waiting_gid).is_some());
|
|
assert_eq!(
|
|
engine.remove_download_result(waiting_gid),
|
|
Err(CoreError::InvalidState(
|
|
"download result is not available for active or waiting downloads",
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn purge_download_results_removes_only_stopped_entries() {
|
|
let mut engine = DownloadEngine::new();
|
|
let waiting_gid = engine.add_uri("https://example.org/waiting.bin").gid();
|
|
let paused_gid = engine.add_uri("https://example.org/paused.bin").gid();
|
|
let complete_gid = engine.add_uri("https://example.org/complete.bin").gid();
|
|
let removed_gid = engine.add_uri("https://example.org/removed.bin").gid();
|
|
let error_gid = engine.add_uri("https://example.org/error.bin").gid();
|
|
|
|
engine.pause(paused_gid).expect("pause should succeed");
|
|
engine
|
|
.complete(complete_gid)
|
|
.expect("complete transition should succeed");
|
|
engine
|
|
.remove(removed_gid)
|
|
.expect("remove transition should succeed");
|
|
engine
|
|
.fail(error_gid)
|
|
.expect("error transition should succeed");
|
|
|
|
assert_eq!(engine.purge_download_results(), 3);
|
|
assert!(engine.registry().get(waiting_gid).is_some());
|
|
assert!(engine.registry().get(paused_gid).is_some());
|
|
assert!(engine.registry().get(complete_gid).is_none());
|
|
assert!(engine.registry().get(removed_gid).is_none());
|
|
assert!(engine.registry().get(error_gid).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_prefers_verified_piece_progress_when_larger() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine
|
|
.add_uri("https://example.org/verified-dominates.bin")
|
|
.gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_piece_length(1024);
|
|
group.set_total_length(4096);
|
|
group.set_completed_length(512);
|
|
group.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group.set_piece_state(PieceId(1), PieceState::Verified);
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(snapshot.completed_length, 2048);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn save_and_load_session_round_trips_retry_and_completed_metrics() -> Result<()> {
|
|
let session_path = temp_session_path("metrics-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("https://example.org/metrics.bin").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_piece_length(1024);
|
|
group.set_total_length(4096);
|
|
group.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group.set_completed_length(3072);
|
|
group.increment_retry_count();
|
|
group.increment_retry_count();
|
|
group.set_status(DownloadStatus::Active);
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
assert_eq!(loaded.retry_count(), 2);
|
|
assert_eq!(loaded.completed_length(), 3072);
|
|
assert_eq!(reader.progress_snapshot(loaded_gid)?.completed_length, 3072);
|
|
assert_eq!(loaded.status(), &DownloadStatus::Active);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn save_and_load_session_round_trips_bt_selected_file_state() -> Result<()> {
|
|
let session_path = temp_session_path("bt-selected-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("magnet:?xt=urn:btih:abcdef").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "abcdef".to_owned(),
|
|
name: Some("linux-iso-pack".to_owned()),
|
|
metadata_only: false,
|
|
files: vec![
|
|
BtFileInfo {
|
|
path: "disc1.iso".to_owned(),
|
|
length: 1024,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
},
|
|
BtFileInfo {
|
|
path: "disc2.iso".to_owned(),
|
|
length: 2048,
|
|
piece_offset: Some(1024),
|
|
selected: false,
|
|
},
|
|
],
|
|
..BtRuntimeState::default()
|
|
});
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
let bt = loaded.bt().expect("bt runtime state should round-trip");
|
|
assert_eq!(bt.info_hash, "abcdef");
|
|
assert_eq!(bt.files.len(), 2);
|
|
assert!(
|
|
bt.files
|
|
.first()
|
|
.expect("bt file list should contain the first file")
|
|
.selected
|
|
);
|
|
assert!(
|
|
!bt.files
|
|
.get(1)
|
|
.expect("bt file list should contain the second file")
|
|
.selected
|
|
);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn save_and_load_session_round_trips_paused_bt_group_state() -> Result<()> {
|
|
let session_path = temp_session_path("bt-paused-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("magnet:?xt=urn:btih:123456").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Paused);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "123456".to_owned(),
|
|
metadata_only: true,
|
|
files: vec![BtFileInfo {
|
|
path: "metadata.part".to_owned(),
|
|
length: 512,
|
|
piece_offset: None,
|
|
selected: true,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
assert_eq!(loaded.status(), &DownloadStatus::Paused);
|
|
let bt = loaded.bt().expect("bt runtime state should round-trip");
|
|
assert_eq!(bt.info_hash, "123456");
|
|
assert_eq!(bt.files.len(), 1);
|
|
let first_file = bt
|
|
.files
|
|
.first()
|
|
.expect("bt file list should contain the metadata placeholder");
|
|
assert_eq!(first_file.path, "metadata.part");
|
|
assert!(first_file.selected);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn load_session_recovers_partial_progress_from_control_file_when_session_metadata_lacks_it()
|
|
-> Result<()> {
|
|
let session_path = temp_session_path("control-resume-session.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer
|
|
.add_uri("https://example.org/control-resume.bin")
|
|
.gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_piece_length(1024);
|
|
group.set_total_length(4096);
|
|
group.set_piece_state(PieceId(0), PieceState::Verified);
|
|
group.set_piece_state(PieceId(1), PieceState::Downloading);
|
|
group.set_completed_length(1536);
|
|
group.increment_retry_count();
|
|
group.increment_retry_count();
|
|
group.set_status(DownloadStatus::Active);
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut session_file = load_session_file(&session_path)
|
|
.map_err(|_| CoreError::StorageUnavailable("failed to read session file"))?;
|
|
let entry = session_file
|
|
.entries
|
|
.first_mut()
|
|
.expect("saved session should contain one entry");
|
|
let metadata = entry
|
|
.metadata
|
|
.as_mut()
|
|
.expect("saved session entry should contain metadata");
|
|
metadata.remove("completed_length");
|
|
metadata.remove("retry_count");
|
|
save_session_file(&session_path, &session_file)
|
|
.map_err(|_| CoreError::StorageUnavailable("failed to write session file"))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
let snapshot = reader.progress_snapshot(loaded_gid)?;
|
|
|
|
assert_eq!(loaded.status(), &DownloadStatus::Active);
|
|
assert_eq!(loaded.piece_state(PieceId(0)), Some(PieceState::Verified));
|
|
assert_eq!(
|
|
loaded.piece_state(PieceId(1)),
|
|
Some(PieceState::Downloading)
|
|
);
|
|
assert_eq!(loaded.completed_length(), 1536);
|
|
assert_eq!(loaded.retry_count(), 2);
|
|
assert_eq!(snapshot.total_length, 4096);
|
|
assert_eq!(snapshot.completed_length, 1536);
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_once_sets_active_segments_and_runtime_bridge_state() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("https://example.org/split.bin").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_total_length(8 * 1024);
|
|
group.set_completed_length(2 * 1024);
|
|
group.set_piece_length(1024);
|
|
group.set_status(DownloadStatus::Waiting);
|
|
|
|
let decision = engine.schedule_once();
|
|
assert_eq!(decision, ScheduleDecision::Queue(gid));
|
|
|
|
let group = engine.handle_mut(gid).expect("group should exist");
|
|
assert_eq!(group.status(), &DownloadStatus::Active);
|
|
assert_eq!(group.num_connections(), 1);
|
|
assert_eq!(group.segment_assignments().len(), 1);
|
|
assert_eq!(
|
|
group
|
|
.segment_assignments()
|
|
.first()
|
|
.expect("one segment assignment should exist")
|
|
.range,
|
|
crate::piece::PieceRange::new(2 * 1024, 8 * 1024)
|
|
);
|
|
|
|
let bridge = engine.session().bridge();
|
|
assert!(bridge.segment_plan.is_some());
|
|
assert_eq!(bridge.completed_length, 2048);
|
|
assert_eq!(bridge.retry_count, 0);
|
|
assert_eq!(bridge.active_segments, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_once_propagates_error_as_retry_with_backpressure() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("https://example.org/error.bin").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_total_length(4096);
|
|
group.set_completed_length(1024);
|
|
group.set_status(DownloadStatus::Error);
|
|
|
|
let decision = engine.schedule_once();
|
|
assert_eq!(decision, ScheduleDecision::RetryLater(gid));
|
|
|
|
let group = engine.handle_mut(gid).expect("group should exist");
|
|
assert_eq!(group.status(), &DownloadStatus::Waiting);
|
|
assert_eq!(group.num_connections(), 0);
|
|
assert!(group.segment_assignments().is_empty());
|
|
assert_eq!(group.retry_count(), 1);
|
|
assert_eq!(group.retry_attempts().len(), 1);
|
|
assert_eq!(
|
|
group
|
|
.retry_attempts()
|
|
.first()
|
|
.expect("one retry attempt should exist")
|
|
.error
|
|
.as_deref(),
|
|
Some("schedule-retry:error-state")
|
|
);
|
|
|
|
let bridge = engine.session().bridge();
|
|
assert!(bridge.segment_plan.is_some());
|
|
assert_eq!(bridge.retry_count, 1);
|
|
assert_eq!(bridge.retry_history.len(), 1);
|
|
assert_eq!(bridge.completed_length, 1024);
|
|
assert_eq!(bridge.active_segments, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn tell_stopped_orders_downloads_by_stop_sequence() {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid_a = engine.add_uri("https://example.org/a.bin").gid();
|
|
let gid_b = engine.add_uri("https://example.org/b.bin").gid();
|
|
let gid_c = engine.add_uri("https://example.org/c.bin").gid();
|
|
let gid_d = engine.add_uri("https://example.org/d.bin").gid();
|
|
|
|
engine
|
|
.complete(gid_c)
|
|
.expect("complete transition should succeed");
|
|
engine
|
|
.remove(gid_a)
|
|
.expect("remove transition should succeed");
|
|
engine.fail(gid_d).expect("error transition should succeed");
|
|
engine
|
|
.complete(gid_b)
|
|
.expect("complete transition should succeed");
|
|
|
|
let gids = engine
|
|
.tell_stopped()
|
|
.into_iter()
|
|
.map(super::DownloadHandle::gid)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(gids, vec![gid_c, gid_a, gid_d, gid_b]);
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_once_materializes_multiple_piece_aligned_segments() {
|
|
let mut engine = DownloadEngine::with_runtime(RuntimeConfig {
|
|
split: 4,
|
|
max_connections_per_server: 4,
|
|
max_connection_per_server: 4,
|
|
min_split_size: 1024,
|
|
piece_length: 1024,
|
|
..RuntimeConfig::default()
|
|
});
|
|
let gid = engine
|
|
.add_uri("https://example.org/multi-segment.bin")
|
|
.gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_total_length(10 * 1024);
|
|
group.set_completed_length(2 * 1024);
|
|
group.set_piece_length(1024);
|
|
group.set_status(DownloadStatus::Waiting);
|
|
|
|
let decision = engine.schedule_once();
|
|
assert_eq!(decision, ScheduleDecision::Queue(gid));
|
|
|
|
let group = engine.handle_mut(gid).expect("group should exist");
|
|
let assignments = group.segment_assignments();
|
|
assert_eq!(assignments.len(), 4);
|
|
let first = assignments
|
|
.first()
|
|
.expect("first segment assignment should exist");
|
|
let second = assignments
|
|
.get(1)
|
|
.expect("second segment assignment should exist");
|
|
let third = assignments
|
|
.get(2)
|
|
.expect("third segment assignment should exist");
|
|
let fourth = assignments
|
|
.get(3)
|
|
.expect("fourth segment assignment should exist");
|
|
assert_eq!(first.range, crate::piece::PieceRange::new(2048, 4096));
|
|
assert_eq!(second.range, crate::piece::PieceRange::new(4096, 6144));
|
|
assert_eq!(third.range, crate::piece::PieceRange::new(6144, 8192));
|
|
assert_eq!(fourth.range, crate::piece::PieceRange::new(8192, 10240));
|
|
assert!(
|
|
assignments
|
|
.iter()
|
|
.all(|segment| segment.state == SegmentState::Active)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_once_starts_segments_from_resume_offset_when_ahead_of_completed_length() {
|
|
let mut engine = DownloadEngine::with_runtime(RuntimeConfig {
|
|
split: 3,
|
|
max_connections_per_server: 3,
|
|
max_connection_per_server: 3,
|
|
min_split_size: 1024,
|
|
piece_length: 1024,
|
|
..RuntimeConfig::default()
|
|
});
|
|
let gid = engine
|
|
.add_uri("https://example.org/resume-segment.bin")
|
|
.gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_total_length(6 * 1024);
|
|
group.set_completed_length(1024);
|
|
group.set_piece_length(1024);
|
|
group.set_resume_state(ResumeState {
|
|
persisted: true,
|
|
resume_offset: 3 * 1024,
|
|
validated_length: Some(1024),
|
|
segment_cursor: Some(PieceId(3)),
|
|
});
|
|
group.set_status(DownloadStatus::Waiting);
|
|
|
|
let decision = engine.schedule_once();
|
|
assert_eq!(decision, ScheduleDecision::Queue(gid));
|
|
|
|
let group = engine.handle_mut(gid).expect("group should exist");
|
|
let assignments = group.segment_assignments();
|
|
assert_eq!(assignments.len(), 3);
|
|
assert_eq!(
|
|
assignments
|
|
.first()
|
|
.expect("first resumed segment assignment should exist")
|
|
.range
|
|
.start,
|
|
3 * 1024
|
|
);
|
|
assert_eq!(
|
|
assignments
|
|
.get(2)
|
|
.expect("third resumed segment assignment should exist")
|
|
.range
|
|
.end,
|
|
6 * 1024
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn progress_snapshot_does_not_force_bt_complete_without_selected_payload() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("magnet:?xt=urn:btih:falsecomplete").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Complete);
|
|
group.set_piece_length(1024);
|
|
group.set_total_length(4096);
|
|
group.set_completed_length(0);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "falsecomplete".to_owned(),
|
|
metadata_only: true,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 4096,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(snapshot.total_length, 4096);
|
|
assert_eq!(snapshot.completed_length, 0);
|
|
assert!(!snapshot.seeding);
|
|
assert!(!snapshot.bt_true_seeding);
|
|
assert_eq!(snapshot.share_ratio_milli, None);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bt_share_ratio_round_trip_and_pause_resume_state_survive_session_load() -> Result<()> {
|
|
let session_path = temp_session_path("bt-share-roundtrip.txt");
|
|
let runtime = RuntimeConfig::default().with_session_path(session_path.clone());
|
|
let mut writer = DownloadEngine::with_runtime(runtime.clone());
|
|
let gid = writer.add_uri("magnet:?xt=urn:btih:sharetest").gid();
|
|
let group = writer
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Paused);
|
|
group.set_total_length(5000);
|
|
group.set_completed_length(3000);
|
|
group.set_upload_length(1500);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "sharetest".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![
|
|
BtFileInfo {
|
|
path: "a.bin".to_owned(),
|
|
length: 2000,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
},
|
|
BtFileInfo {
|
|
path: "b.bin".to_owned(),
|
|
length: 3000,
|
|
piece_offset: Some(2000),
|
|
selected: false,
|
|
},
|
|
],
|
|
..BtRuntimeState::default()
|
|
});
|
|
writer.save_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
|
|
let mut reader = DownloadEngine::with_runtime(runtime);
|
|
reader.load_session(SaveSessionTarget::Path(session_path.clone()))?;
|
|
let loaded_gid = reader
|
|
.registry()
|
|
.handles()
|
|
.next()
|
|
.expect("loaded registry should contain one gid")
|
|
.gid();
|
|
let loaded = reader
|
|
.registry()
|
|
.get(loaded_gid)
|
|
.expect("loaded group should exist");
|
|
assert_eq!(loaded.status(), &DownloadStatus::Paused);
|
|
let snapshot = reader.progress_snapshot(loaded_gid)?;
|
|
assert_eq!(snapshot.share_ratio_milli, Some(500));
|
|
|
|
let root = session_path
|
|
.parent()
|
|
.expect("session path should have a parent")
|
|
.to_path_buf();
|
|
let _ = fs::remove_dir_all(root);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn apply_bt_peer_snapshot_replaces_runtime_peer_view() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("magnet:?xt=urn:btih:peer-snapshot").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_num_connections(7);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "peer-snapshot".to_owned(),
|
|
metadata_only: false,
|
|
..BtRuntimeState::default()
|
|
});
|
|
|
|
engine.apply_bt_peer_snapshot(
|
|
gid,
|
|
vec![
|
|
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: 111,
|
|
upload_speed: 222,
|
|
seeder: false,
|
|
},
|
|
BtPeerInfo {
|
|
peer_id: Some("peer-b".to_owned()),
|
|
ip: "127.0.0.2".to_owned(),
|
|
port: 6882,
|
|
client_name: Some("client-b".to_owned()),
|
|
interested: false,
|
|
choked: true,
|
|
download_speed: 0,
|
|
upload_speed: 64,
|
|
seeder: true,
|
|
},
|
|
],
|
|
)?;
|
|
|
|
let saved = engine
|
|
.registry()
|
|
.get(gid)
|
|
.and_then(RequestGroup::bt)
|
|
.expect("bt runtime state should exist");
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(saved.peers.len(), 2);
|
|
assert_eq!(
|
|
saved
|
|
.peers
|
|
.first()
|
|
.expect("first saved peer should exist")
|
|
.ip,
|
|
"127.0.0.1"
|
|
);
|
|
assert!(
|
|
saved
|
|
.peers
|
|
.get(1)
|
|
.expect("second saved peer should exist")
|
|
.seeder
|
|
);
|
|
assert_eq!(snapshot.num_connections, 2);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn apply_bt_tracker_snapshot_updates_existing_tracker_and_can_append_new_one() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("magnet:?xt=urn:btih:tracker-snapshot").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "tracker-snapshot".to_owned(),
|
|
metadata_only: false,
|
|
trackers: vec![BtTrackerInfo {
|
|
url: "http://tracker.example.org/announce".to_owned(),
|
|
tier: Some(0),
|
|
id: None,
|
|
seeders: None,
|
|
leechers: None,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
|
|
engine.apply_bt_tracker_snapshot(
|
|
gid,
|
|
"http://tracker.example.org/announce",
|
|
Some("session-a".to_owned()),
|
|
Some(12),
|
|
Some(4),
|
|
)?;
|
|
engine.apply_bt_tracker_snapshot(
|
|
gid,
|
|
"udp://tracker.example.org:6969/announce",
|
|
Some("session-b".to_owned()),
|
|
Some(18),
|
|
Some(6),
|
|
)?;
|
|
|
|
let saved = engine
|
|
.registry()
|
|
.get(gid)
|
|
.and_then(RequestGroup::bt)
|
|
.expect("bt runtime state should exist");
|
|
assert_eq!(saved.trackers.len(), 2);
|
|
let first_tracker = saved
|
|
.trackers
|
|
.first()
|
|
.expect("first saved tracker should exist");
|
|
let second_tracker = saved
|
|
.trackers
|
|
.get(1)
|
|
.expect("second saved tracker should exist");
|
|
assert_eq!(first_tracker.id.as_deref(), Some("session-a"));
|
|
assert_eq!(first_tracker.seeders, Some(12));
|
|
assert_eq!(first_tracker.leechers, Some(4));
|
|
assert_eq!(
|
|
second_tracker.url,
|
|
"udp://tracker.example.org:6969/announce"
|
|
);
|
|
assert_eq!(second_tracker.id.as_deref(), Some("session-b"));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn record_bt_runtime_tick_updates_share_time_upload_and_completed_lengths() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("magnet:?xt=urn:btih:bt-runtime-tick").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Active);
|
|
group.set_total_length(4096);
|
|
group.set_completed_length(1024);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "bt-runtime-tick".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 4096,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
|
|
engine.record_bt_runtime_tick(gid, 2048, 1536, 30, 12, true)?;
|
|
let loaded = engine
|
|
.registry()
|
|
.get(gid)
|
|
.expect("group should still exist");
|
|
assert_eq!(loaded.completed_length(), 3072);
|
|
assert_eq!(loaded.upload_length(), 1536);
|
|
assert_eq!(loaded.upload_speed(), 0);
|
|
assert!(loaded.bt_is_seeding());
|
|
assert_eq!(loaded.bt_share_time_secs(), Some(30));
|
|
assert_eq!(loaded.bt_seeding_time_secs(), Some(12));
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(snapshot.completed_length, 3072);
|
|
assert_eq!(snapshot.upload_length, 1536);
|
|
assert_eq!(snapshot.share_ratio_milli, Some(375));
|
|
assert_eq!(snapshot.share_time_secs, Some(30));
|
|
assert_eq!(snapshot.seeding_time_secs, Some(12));
|
|
assert!(!snapshot.bt_true_seeding);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bt_update_helpers_feed_progress_snapshot_runtime_metrics() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine
|
|
.add_uri("magnet:?xt=urn:btih:bt-progress-metrics")
|
|
.gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Active);
|
|
group.set_total_length(4 * 1024);
|
|
group.set_piece_length(1024);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "bt-progress-metrics".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 4 * 1024,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
group.set_piece_state(PieceId(0), PieceState::Pending);
|
|
group.set_piece_state(PieceId(1), PieceState::Missing);
|
|
group.set_piece_state(PieceId(2), PieceState::Queued);
|
|
|
|
let piece = engine.apply_bt_piece_block_update(
|
|
gid,
|
|
crate::request::BtPieceBlockUpdate {
|
|
piece_id: PieceId(0),
|
|
completed_blocks: 4,
|
|
total_blocks: 4,
|
|
},
|
|
)?;
|
|
assert!(piece.transitioned_to_verified);
|
|
assert_eq!(piece.completed_length_delta, 1024);
|
|
|
|
let availability = engine.apply_bt_piece_availability_update(
|
|
gid,
|
|
crate::request::BtPieceAvailabilityUpdate {
|
|
piece_id: PieceId(2),
|
|
peers_with_piece: 6,
|
|
},
|
|
)?;
|
|
assert_eq!(availability.available_piece_count, 1);
|
|
|
|
let peer = engine.apply_bt_peer_update(
|
|
gid,
|
|
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: 700,
|
|
upload_speed: 350,
|
|
seeder: false,
|
|
},
|
|
)?;
|
|
assert_eq!(peer.peer_count, 1);
|
|
assert_eq!(peer.total_download_speed, 700);
|
|
assert_eq!(peer.total_upload_speed, 350);
|
|
|
|
engine.apply_bt_piece_block_update(
|
|
gid,
|
|
crate::request::BtPieceBlockUpdate {
|
|
piece_id: PieceId(1),
|
|
completed_blocks: 1,
|
|
total_blocks: 4,
|
|
},
|
|
)?;
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert_eq!(snapshot.completed_length, 1024);
|
|
assert_eq!(snapshot.remaining_length(), 3 * 1024);
|
|
assert_eq!(snapshot.num_connections, 1);
|
|
assert_eq!(snapshot.download_speed, 700);
|
|
assert_eq!(snapshot.upload_speed, 350);
|
|
assert_eq!(snapshot.bt_total_peers, 1);
|
|
assert_eq!(snapshot.bt_seeders, 0);
|
|
assert_eq!(snapshot.bt_leechers, 1);
|
|
assert_eq!(snapshot.bt_available_pieces, 1);
|
|
assert_eq!(snapshot.bt_verified_pieces, 1);
|
|
assert_eq!(snapshot.bt_downloading_pieces, 1);
|
|
assert_eq!(snapshot.bt_queued_pieces, 1);
|
|
assert_eq!(snapshot.bt_missing_pieces, 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bt_runtime_helpers_drive_true_seeding_and_share_runtime_snapshot() -> Result<()> {
|
|
let mut engine = DownloadEngine::new();
|
|
let gid = engine.add_uri("magnet:?xt=urn:btih:bt-live-share").gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Active);
|
|
group.set_total_length(2_048);
|
|
group.set_completed_length(2_048);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "bt-live-share".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 2_048,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
|
|
let started = engine.set_bt_seeding_state(gid, true, Some(1_000))?;
|
|
assert!(started.seeding);
|
|
assert_eq!(started.share_ratio_milli, Some(0));
|
|
|
|
let advanced = engine.tick_bt_runtime_clock(gid, 1_040, true)?;
|
|
assert!(advanced.seeding);
|
|
assert_eq!(advanced.share_time_secs, 40);
|
|
assert_eq!(advanced.seeding_time_secs, 40);
|
|
|
|
let tick = engine.apply_bt_runtime_tick(gid, 0, 1_024, 90, 180, 5, 5, true, Some(16))?;
|
|
assert!(tick.seeding);
|
|
assert_eq!(tick.upload_length, 1_024);
|
|
assert_eq!(tick.download_speed, 90);
|
|
assert_eq!(tick.upload_speed, 180);
|
|
assert_eq!(tick.num_connections, 16);
|
|
assert_eq!(tick.share_ratio_milli, Some(500));
|
|
assert_eq!(tick.share_time_secs, 45);
|
|
assert_eq!(tick.seeding_time_secs, 45);
|
|
|
|
let snapshot = engine.progress_snapshot(gid)?;
|
|
assert!(snapshot.seeding);
|
|
assert!(snapshot.bt_true_seeding);
|
|
assert_eq!(snapshot.share_ratio_milli, Some(500));
|
|
assert_eq!(snapshot.share_time_secs, Some(45));
|
|
assert_eq!(snapshot.seeding_time_secs, Some(45));
|
|
assert_eq!(snapshot.bt_selected_payload_length, 2_048);
|
|
assert_eq!(snapshot.bt_remaining_payload_length, 0);
|
|
assert_eq!(snapshot.upload_speed, 180);
|
|
assert_eq!(snapshot.num_connections, 16);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn download_runtime_snapshot_exposes_segment_and_bt_pressure_metrics() -> Result<()> {
|
|
let mut engine = DownloadEngine::with_runtime(RuntimeConfig {
|
|
split: 4,
|
|
max_connections_per_server: 4,
|
|
max_connection_per_server: 4,
|
|
min_split_size: 1024,
|
|
piece_length: 1024,
|
|
..RuntimeConfig::default()
|
|
});
|
|
let gid = engine
|
|
.add_uri("magnet:?xt=urn:btih:download-runtime-snapshot")
|
|
.gid();
|
|
let group = engine
|
|
.handle_mut(gid)
|
|
.expect("newly added group should exist");
|
|
group.set_status(DownloadStatus::Active);
|
|
group.set_total_length(5 * 1024);
|
|
group.set_completed_length(1024);
|
|
group.set_piece_length(1024);
|
|
group.set_download_speed(900);
|
|
group.set_upload_speed(120);
|
|
group.set_bt(BtRuntimeState {
|
|
info_hash: "download-runtime-snapshot".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 5 * 1024,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
peers: vec![BtPeerInfo {
|
|
peer_id: Some("peer-a".to_owned()),
|
|
ip: "203.0.113.10".to_owned(),
|
|
port: 6881,
|
|
client_name: None,
|
|
interested: true,
|
|
choked: false,
|
|
download_speed: 900,
|
|
upload_speed: 120,
|
|
seeder: false,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
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.apply_bt_piece_availability_update(crate::request::BtPieceAvailabilityUpdate {
|
|
piece_id: PieceId(1),
|
|
peers_with_piece: 1,
|
|
});
|
|
|
|
let decision = engine.schedule_once();
|
|
assert_eq!(decision, ScheduleDecision::RunNow(gid));
|
|
|
|
let snapshot = engine.download_runtime_snapshot(gid)?;
|
|
assert_eq!(snapshot.gid, gid);
|
|
assert_eq!(snapshot.effective_download_limit, None);
|
|
assert_eq!(snapshot.effective_upload_limit, None);
|
|
assert_eq!(snapshot.segment_stats.segment_count, 4);
|
|
assert_eq!(snapshot.segment_stats.remaining_bytes, 4 * 1024);
|
|
assert_eq!(
|
|
snapshot
|
|
.bt_pressure
|
|
.as_ref()
|
|
.map(|pressure| pressure.requestable_pieces),
|
|
Some(2)
|
|
);
|
|
assert_eq!(
|
|
snapshot
|
|
.bt_pressure
|
|
.as_ref()
|
|
.map(|pressure| pressure.scarce_requestable_pieces),
|
|
Some(1)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_instrumentation_snapshot_aggregates_scheduler_and_resource_state() {
|
|
let mut engine = DownloadEngine::with_runtime(RuntimeConfig {
|
|
split: 3,
|
|
max_connections_per_server: 3,
|
|
max_connection_per_server: 3,
|
|
min_split_size: 1024,
|
|
piece_length: 1024,
|
|
..RuntimeConfig::default()
|
|
});
|
|
let active_gid = engine.add_uri("https://example.org/a.bin").gid();
|
|
let waiting_gid = engine.add_uri("magnet:?xt=urn:btih:runtime-global").gid();
|
|
|
|
let active = engine
|
|
.handle_mut(active_gid)
|
|
.expect("active group should exist");
|
|
active.set_status(DownloadStatus::Active);
|
|
active.set_total_length(4 * 1024);
|
|
active.set_completed_length(1024);
|
|
active.set_piece_length(1024);
|
|
|
|
let waiting = engine
|
|
.handle_mut(waiting_gid)
|
|
.expect("waiting group should exist");
|
|
waiting.set_status(DownloadStatus::Waiting);
|
|
waiting.set_total_length(3 * 1024);
|
|
waiting.set_completed_length(0);
|
|
waiting.set_piece_length(1024);
|
|
waiting.set_bt(BtRuntimeState {
|
|
info_hash: "runtime-global".to_owned(),
|
|
metadata_only: false,
|
|
files: vec![BtFileInfo {
|
|
path: "payload.bin".to_owned(),
|
|
length: 3 * 1024,
|
|
piece_offset: Some(0),
|
|
selected: true,
|
|
}],
|
|
peers: vec![BtPeerInfo {
|
|
peer_id: Some("peer-a".to_owned()),
|
|
ip: "203.0.113.20".to_owned(),
|
|
port: 6881,
|
|
client_name: None,
|
|
interested: true,
|
|
choked: false,
|
|
download_speed: 100,
|
|
upload_speed: 20,
|
|
seeder: false,
|
|
}],
|
|
..BtRuntimeState::default()
|
|
});
|
|
waiting.set_piece_state(PieceId(0), PieceState::Pending);
|
|
waiting.set_piece_state(PieceId(1), PieceState::Downloading);
|
|
waiting.set_piece_state(PieceId(2), PieceState::Missing);
|
|
waiting.apply_bt_piece_availability_update(crate::request::BtPieceAvailabilityUpdate {
|
|
piece_id: PieceId(0),
|
|
peers_with_piece: 1,
|
|
});
|
|
|
|
let _ = engine.schedule_once();
|
|
let runtime = engine.runtime_instrumentation_snapshot();
|
|
|
|
assert_eq!(runtime.download_count, 2);
|
|
assert_eq!(runtime.active_download_count, 1);
|
|
assert_eq!(runtime.waiting_download_count, 1);
|
|
assert_eq!(runtime.total_active_segments, 3);
|
|
assert_eq!(runtime.total_requestable_pieces, 2);
|
|
assert_eq!(runtime.total_scarce_requestable_pieces, 1);
|
|
assert_eq!(runtime.configured_disk_cache_bytes, 16 * 1024 * 1024);
|
|
assert_eq!(runtime.scheduler_counters.schedule_run_count, 1);
|
|
assert!(runtime.last_scheduler_plan.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn progress_and_global_stat_apply_speed_limits_from_runtime_and_group_options() -> Result<()> {
|
|
let mut engine = DownloadEngine::with_runtime(RuntimeConfig {
|
|
max_overall_download_limit: Some(1_200),
|
|
max_download_limit: Some(900),
|
|
max_overall_upload_limit: Some(600),
|
|
max_upload_limit: Some(500),
|
|
..RuntimeConfig::default()
|
|
});
|
|
|
|
let gid_a = engine.add_uri("https://example.org/a.bin").gid();
|
|
let gid_b = engine.add_uri("https://example.org/b.bin").gid();
|
|
|
|
let group_a = engine.handle_mut(gid_a).expect("group a should exist");
|
|
group_a.set_status(DownloadStatus::Active);
|
|
group_a.set_download_speed(2_000);
|
|
group_a.set_upload_speed(900);
|
|
group_a.set_option("max-download-limit", "700");
|
|
group_a.set_option("max-upload-limit", "200");
|
|
|
|
let group_b = engine.handle_mut(gid_b).expect("group b should exist");
|
|
group_b.set_status(DownloadStatus::Active);
|
|
group_b.set_download_speed(2_000);
|
|
group_b.set_upload_speed(900);
|
|
|
|
let snapshot_a = engine.progress_snapshot(gid_a)?;
|
|
let snapshot_b = engine.progress_snapshot(gid_b)?;
|
|
assert_eq!(snapshot_a.download_speed, 600);
|
|
assert_eq!(snapshot_a.upload_speed, 200);
|
|
assert_eq!(snapshot_b.download_speed, 600);
|
|
assert_eq!(snapshot_b.upload_speed, 300);
|
|
|
|
let runtime_a = engine.download_runtime_snapshot(gid_a)?;
|
|
assert_eq!(runtime_a.effective_download_limit, Some(600));
|
|
assert_eq!(runtime_a.effective_upload_limit, Some(200));
|
|
assert_eq!(runtime_a.download_speed, 600);
|
|
assert_eq!(runtime_a.upload_speed, 200);
|
|
|
|
let stat = engine.get_global_stat();
|
|
assert_eq!(stat.download_speed, 1_200);
|
|
assert_eq!(stat.upload_speed, 500);
|
|
Ok(())
|
|
}
|