chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
#![expect(
|
||||
missing_docs,
|
||||
reason = "integration test scenarios are documented by case names rather than item-level rustdoc"
|
||||
)]
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use aria2_rust_pro_cli::{
|
||||
BtStatusReport, Invocation, TransferSelection, execute_runtime, execute_runtime_with_downloader,
|
||||
};
|
||||
use aria2_rust_pro_compat as _;
|
||||
use aria2_rust_pro_core as _;
|
||||
use aria2_rust_pro_protocol::FixtureHttpDownloader;
|
||||
use aria2_rust_pro_storage as _;
|
||||
use aria2_rust_pro_tests as _;
|
||||
use criterion as _;
|
||||
|
||||
#[path = "../test_support/support.rs"]
|
||||
mod support;
|
||||
|
||||
fn unique_temp_path(stem: &str) -> PathBuf {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time should be after unix epoch")
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("aria2-rust-pro-tests-{stem}-{unique}"))
|
||||
}
|
||||
|
||||
fn write_config(path: &Path, download_dir: &Path) {
|
||||
fs::write(path, format!("dir={}", download_dir.display())).expect("config should write");
|
||||
}
|
||||
|
||||
fn assert_bt_snapshot(
|
||||
bt: &BtStatusReport,
|
||||
expected_metadata_only: bool,
|
||||
expected_total_length: Option<u64>,
|
||||
) {
|
||||
assert_eq!(bt.is_bt, Some(true));
|
||||
assert_eq!(bt.metadata_only, Some(expected_metadata_only));
|
||||
assert_eq!(bt.share_time, Some(0));
|
||||
assert_eq!(bt.share_ratio.as_deref(), Some("0.000"));
|
||||
assert_eq!(bt.share_ratio_progress.as_deref(), Some("0.000"));
|
||||
assert_eq!(bt.share_ratio_remaining.as_deref(), Some("0.000"));
|
||||
assert!(
|
||||
bt.announce_list_tier_count.unwrap_or_default() >= 1,
|
||||
"bt snapshot should retain at least one announce tier"
|
||||
);
|
||||
if !expected_metadata_only {
|
||||
assert!(
|
||||
bt.magnet_uri
|
||||
.as_deref()
|
||||
.is_some_and(|uri| uri.starts_with("magnet:?xt=urn:btih:"))
|
||||
);
|
||||
assert_eq!(expected_total_length, Some(32_768));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_magnet_runtime_report_keeps_bt_snapshot_visible() {
|
||||
let report = execute_runtime(Invocation::Run {
|
||||
config_path: None,
|
||||
uris: vec![String::from(
|
||||
"magnet:?xt=urn:btih:00112233445566778899aabbccddeeff00112233&dn=dht-metadata.iso&tr=http%3A%2F%2Ftracker.example.org%2Fannounce&tr=udp%3A%2F%2Ftracker.example.org%3A6969",
|
||||
)],
|
||||
})
|
||||
.expect("runtime should execute for a bt magnet");
|
||||
|
||||
assert_eq!(report.accepted_uri_count, 1);
|
||||
assert_eq!(report.tracked_download_count, 1);
|
||||
assert_eq!(report.completed_download_count, 0);
|
||||
assert_eq!(report.transfer_kinds, vec![TransferSelection::Magnet]);
|
||||
assert_eq!(report.recognized_schemes, vec![String::from("magnet")]);
|
||||
assert_eq!(report.first_total_length, Some(0));
|
||||
assert_eq!(report.first_completed_length, Some(0));
|
||||
|
||||
let bt = report
|
||||
.first_bt_status
|
||||
.as_ref()
|
||||
.expect("magnet foreground execution should surface a bt snapshot");
|
||||
assert_bt_snapshot(bt, true, report.first_total_length);
|
||||
assert_eq!(bt.num_seeders, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_local_torrent_file_runs_as_bt_session_not_plain_uri() {
|
||||
let temp_root = unique_temp_path("local-torrent");
|
||||
let download_dir = temp_root.join("downloads");
|
||||
let torrent_path = temp_root.join("fixture.torrent");
|
||||
let config_path = temp_root.join("aria2.conf");
|
||||
|
||||
fs::create_dir_all(&download_dir).expect("download directory should create");
|
||||
support::write_torrent_fixture(&torrent_path);
|
||||
write_config(&config_path, &download_dir);
|
||||
|
||||
let report = execute_runtime_with_downloader(
|
||||
Invocation::Run {
|
||||
config_path: Some(config_path),
|
||||
uris: vec![torrent_path.to_string_lossy().into_owned()],
|
||||
},
|
||||
&FixtureHttpDownloader::new(),
|
||||
)
|
||||
.expect("runtime should accept a local torrent input");
|
||||
|
||||
assert_eq!(report.accepted_uri_count, 1);
|
||||
assert_eq!(report.tracked_download_count, 1);
|
||||
assert_eq!(report.completed_download_count, 0);
|
||||
assert_eq!(report.transfer_kinds, vec![TransferSelection::Torrent]);
|
||||
assert_eq!(report.first_total_length, Some(32_768));
|
||||
assert_eq!(report.first_completed_length, Some(0));
|
||||
|
||||
let bt = report
|
||||
.first_bt_status
|
||||
.as_ref()
|
||||
.expect("local torrent foreground execution should expose bt status");
|
||||
assert_bt_snapshot(bt, false, report.first_total_length);
|
||||
|
||||
assert!(
|
||||
!download_dir.join("fixture.torrent").exists(),
|
||||
"torrent metainfo should bootstrap a bt session instead of being persisted as the final download artifact"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_remote_torrent_url_bootstraps_bt_session_instead_of_saving_metainfo_payload() {
|
||||
let temp_root = unique_temp_path("remote-torrent");
|
||||
let download_dir = temp_root.join("downloads");
|
||||
let config_path = temp_root.join("aria2.conf");
|
||||
fs::create_dir_all(&download_dir).expect("download directory should create");
|
||||
write_config(&config_path, &download_dir);
|
||||
|
||||
let torrent_url = "https://tracker.example.org/files/ubuntu.torrent";
|
||||
let mut downloader = FixtureHttpDownloader::new();
|
||||
downloader.register(torrent_url, support::BT_TORRENT_FIXTURE_BYTES);
|
||||
|
||||
let report = execute_runtime_with_downloader(
|
||||
Invocation::Run {
|
||||
config_path: Some(config_path),
|
||||
uris: vec![torrent_url.to_owned()],
|
||||
},
|
||||
&downloader,
|
||||
)
|
||||
.expect("runtime should accept a remote torrent url");
|
||||
|
||||
assert_eq!(report.accepted_uri_count, 1);
|
||||
assert_eq!(report.tracked_download_count, 1);
|
||||
assert_eq!(report.completed_download_count, 0);
|
||||
assert_eq!(report.transfer_kinds, vec![TransferSelection::Torrent]);
|
||||
assert_eq!(report.recognized_schemes, vec![String::from("https")]);
|
||||
assert_eq!(report.first_total_length, Some(32_768));
|
||||
assert_eq!(report.first_completed_length, Some(0));
|
||||
|
||||
let bt = report
|
||||
.first_bt_status
|
||||
.as_ref()
|
||||
.expect("remote torrent bootstrap should expose bt status");
|
||||
assert_bt_snapshot(bt, false, report.first_total_length);
|
||||
|
||||
assert!(
|
||||
!download_dir.join("ubuntu.torrent").exists(),
|
||||
"remote torrent bootstrap should not leave the .torrent payload behind as the downloaded artifact"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#![expect(
|
||||
missing_docs,
|
||||
reason = "integration test scenarios are documented by case names rather than item-level rustdoc"
|
||||
)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aria2_rust_pro_cli as _;
|
||||
use aria2_rust_pro_compat as _;
|
||||
use aria2_rust_pro_core as _;
|
||||
use aria2_rust_pro_protocol::{
|
||||
DhtMessageModel, DhtNodeModel,
|
||||
torrent::{PeerWireExtensionHandshakeModel, PeerWireMessageKind, PeerWireMetadataMessageModel},
|
||||
};
|
||||
use aria2_rust_pro_rpc::{InProcessRpcDispatcher, RpcMethod, RpcValue};
|
||||
use aria2_rust_pro_storage as _;
|
||||
use aria2_rust_pro_tests as _;
|
||||
use criterion as _;
|
||||
|
||||
#[path = "../test_support/support.rs"]
|
||||
mod support;
|
||||
|
||||
fn tell_status(dispatcher: &mut InProcessRpcDispatcher, gid: &str) -> BTreeMap<String, RpcValue> {
|
||||
support::rpc_result_object(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2TellStatus,
|
||||
vec![RpcValue::String(gid.to_owned())],
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_files(dispatcher: &mut InProcessRpcDispatcher, gid: &str) -> Vec<RpcValue> {
|
||||
support::rpc_result_array(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2GetFiles,
|
||||
vec![RpcValue::String(gid.to_owned())],
|
||||
)))
|
||||
}
|
||||
|
||||
fn first_file<'a>(files: &'a [RpcValue], context: &str) -> &'a BTreeMap<String, RpcValue> {
|
||||
match files.first() {
|
||||
Some(RpcValue::Object(file)) => file,
|
||||
other => panic!("unexpected first file payload for {context}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn seed_peer_and_promote_metadata(
|
||||
dispatcher: &mut InProcessRpcDispatcher,
|
||||
gid: &str,
|
||||
info_hash_hex: &str,
|
||||
) {
|
||||
dispatcher
|
||||
.apply_dht_get_peers_result(
|
||||
gid,
|
||||
&DhtNodeModel {
|
||||
node_id: String::new(),
|
||||
address: "203.0.113.77".to_owned(),
|
||||
port: 51413,
|
||||
},
|
||||
&DhtMessageModel::get_peers_response(
|
||||
b"gp".to_vec(),
|
||||
vec![0x55; 20],
|
||||
Some(b"dht-token".to_vec()),
|
||||
None,
|
||||
vec![support::compact_peer([198, 51, 100, 42], 51415)],
|
||||
),
|
||||
)
|
||||
.expect("dht get_peers should seed a connectable peer");
|
||||
|
||||
let connector = support::FakePeerWireConnector::new(support::peer_wire_handshake_and_frames(
|
||||
support::decode_hex_20(info_hash_hex),
|
||||
*b"-AZ2060-META-PROMO01",
|
||||
&[
|
||||
PeerWireMessageKind::Unchoke,
|
||||
PeerWireMessageKind::Extension(
|
||||
PeerWireExtensionHandshakeModel {
|
||||
extensions: BTreeMap::from([("ut_metadata".to_owned(), 3_u8)]),
|
||||
client_name: Some("libtorrent/2.0.11".to_owned()),
|
||||
metadata_size: Some(
|
||||
u32::try_from(support::BT_TORRENT_FIXTURE_BYTES.len())
|
||||
.expect("fixture metadata length should fit u32"),
|
||||
),
|
||||
request_queue: Some(64),
|
||||
}
|
||||
.to_peer_wire_message(),
|
||||
),
|
||||
PeerWireMessageKind::Extension(
|
||||
PeerWireMetadataMessageModel::data(
|
||||
0,
|
||||
u32::try_from(support::BT_TORRENT_FIXTURE_BYTES.len())
|
||||
.expect("fixture metadata length should fit u32"),
|
||||
support::BT_TORRENT_FIXTURE_BYTES.to_vec(),
|
||||
)
|
||||
.to_peer_wire_message(3),
|
||||
),
|
||||
],
|
||||
));
|
||||
dispatcher
|
||||
.execute_peer_wire_exchange(gid, &connector)
|
||||
.expect("peer-wire metadata exchange should promote the magnet session");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magnet_piece_observation_clears_metadata_only_and_pending_snapshot_flags() {
|
||||
let mut dispatcher = InProcessRpcDispatcher::new();
|
||||
let reference_gid = support::add_torrent(&mut dispatcher);
|
||||
let reference_status = tell_status(&mut dispatcher, &reference_gid);
|
||||
let magnet_uri = support::rpc_string_field(&reference_status, "magnetUri");
|
||||
let info_hash = support::rpc_string_field(&reference_status, "infoHash");
|
||||
|
||||
let gid = support::add_magnet(&mut dispatcher, &magnet_uri);
|
||||
let before = tell_status(&mut dispatcher, &gid);
|
||||
assert!(support::rpc_bool_field(&before, "metadataOnly"));
|
||||
assert_eq!(support::rpc_string_field(&before, "magnetUri"), magnet_uri);
|
||||
|
||||
let before_snapshot = dispatcher
|
||||
.bt_runtime_coordinator_snapshot(&gid)
|
||||
.expect("snapshot should inspect metadata-only magnet state");
|
||||
assert!(before_snapshot.metadata_only);
|
||||
assert!(before_snapshot.metadata_exchange_pending);
|
||||
|
||||
seed_peer_and_promote_metadata(&mut dispatcher, &gid, &info_hash);
|
||||
|
||||
let after = tell_status(&mut dispatcher, &gid);
|
||||
assert!(!support::rpc_bool_field(&after, "metadataOnly"));
|
||||
assert_eq!(support::rpc_string_field(&after, "magnetUri"), magnet_uri);
|
||||
|
||||
let after_snapshot = dispatcher
|
||||
.bt_runtime_coordinator_snapshot(&gid)
|
||||
.expect("snapshot should inspect promoted magnet state");
|
||||
assert!(!after_snapshot.metadata_only);
|
||||
assert!(!after_snapshot.metadata_exchange_pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promoted_magnet_session_should_match_reference_torrent_file_surface() {
|
||||
let mut dispatcher = InProcessRpcDispatcher::new();
|
||||
let reference_gid = support::add_torrent(&mut dispatcher);
|
||||
let reference_status = tell_status(&mut dispatcher, &reference_gid);
|
||||
let reference_files = get_files(&mut dispatcher, &reference_gid);
|
||||
let reference_file = first_file(&reference_files, "reference torrent");
|
||||
let magnet_uri = support::rpc_string_field(&reference_status, "magnetUri");
|
||||
let info_hash = support::rpc_string_field(&reference_status, "infoHash");
|
||||
|
||||
let gid = support::add_magnet(&mut dispatcher, &magnet_uri);
|
||||
seed_peer_and_promote_metadata(&mut dispatcher, &gid, &info_hash);
|
||||
|
||||
let promoted_status = tell_status(&mut dispatcher, &gid);
|
||||
assert!(
|
||||
!support::rpc_bool_field(&promoted_status, "metadataOnly"),
|
||||
"metadata promotion should leave metadata-only mode before comparing the public torrent surface"
|
||||
);
|
||||
assert_eq!(
|
||||
support::rpc_u64_field(&promoted_status, "totalLength"),
|
||||
support::rpc_u64_field(&reference_status, "totalLength"),
|
||||
"promoted magnet sessions should hydrate the same totalLength visible on the equivalent .torrent bootstrap"
|
||||
);
|
||||
|
||||
let promoted_files = get_files(&mut dispatcher, &gid);
|
||||
let promoted_file = first_file(&promoted_files, "promoted magnet");
|
||||
assert_eq!(
|
||||
promoted_file.get("path"),
|
||||
reference_file.get("path"),
|
||||
"promoted magnet sessions should expose the real torrent file path instead of a synthetic placeholder row"
|
||||
);
|
||||
assert_eq!(
|
||||
promoted_file.get("length"),
|
||||
reference_file.get("length"),
|
||||
"promoted magnet sessions should expose the same per-file length as the equivalent .torrent bootstrap"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
#![expect(
|
||||
missing_docs,
|
||||
reason = "integration test scenarios are documented by case names rather than item-level rustdoc"
|
||||
)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aria2_rust_pro_cli as _;
|
||||
use aria2_rust_pro_compat as _;
|
||||
use aria2_rust_pro_core as _;
|
||||
use aria2_rust_pro_protocol::{
|
||||
DhtMessageModel, TrackerRequestModel, TrackerTransport,
|
||||
torrent::{
|
||||
DhtMessageBody, DhtQueryModel, PeerWireBitfieldModel, PeerWireMessageKind,
|
||||
PeerWirePieceBlockModel,
|
||||
},
|
||||
};
|
||||
use aria2_rust_pro_rpc::{InProcessRpcDispatcher, RpcMethod, RpcValue};
|
||||
use aria2_rust_pro_storage as _;
|
||||
use aria2_rust_pro_tests as _;
|
||||
use criterion as _;
|
||||
|
||||
#[path = "../test_support/support.rs"]
|
||||
mod support;
|
||||
|
||||
#[test]
|
||||
#[expect(
|
||||
clippy::too_many_lines,
|
||||
reason = "integration scenario keeps one end-to-end bt surface in a single readable flow"
|
||||
)]
|
||||
fn tracker_dht_peer_wire_and_seeding_updates_remain_rpc_visible_across_public_dispatcher_api() {
|
||||
let tracker_server =
|
||||
support::LocalTrackerServer::spawn(support::compact_peer([198, 51, 100, 9], 51413), 900);
|
||||
let tracker = support::tracker_transport();
|
||||
let mut dispatcher = InProcessRpcDispatcher::new();
|
||||
let gid = support::add_torrent(&mut dispatcher);
|
||||
|
||||
let bootstrap_status =
|
||||
support::rpc_result_object(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2TellStatus,
|
||||
vec![RpcValue::String(gid.clone())],
|
||||
)));
|
||||
let info_hash = support::rpc_string_field(&bootstrap_status, "infoHash");
|
||||
let magnet_uri = support::rpc_string_field(&bootstrap_status, "magnetUri");
|
||||
let announce = tracker
|
||||
.announce(&TrackerRequestModel {
|
||||
announce_url: tracker_server.announce_url().to_owned(),
|
||||
info_hash: info_hash.clone(),
|
||||
peer_id: "0123456789abcdef0123456789abcdef01234567".to_owned(),
|
||||
port: 6881,
|
||||
uploaded: 0,
|
||||
downloaded: 0,
|
||||
left: 32_768,
|
||||
event: Some("started".to_owned()),
|
||||
compact: true,
|
||||
numwant: Some(10),
|
||||
})
|
||||
.expect("live tracker announce should succeed");
|
||||
dispatcher
|
||||
.apply_tracker_announce_result(&gid, &announce)
|
||||
.expect("tracker announce should update the bt view");
|
||||
|
||||
let dht_get_peers = support::RecordingDhtTransport::new(DhtMessageModel::get_peers_response(
|
||||
b"gp".to_vec(),
|
||||
vec![0x55; 20],
|
||||
Some(b"dht-token".to_vec()),
|
||||
Some(support::compact_node(0x44, [203, 0, 113, 99], 51414)),
|
||||
vec![support::compact_peer([198, 51, 100, 10], 51415)],
|
||||
));
|
||||
dispatcher
|
||||
.execute_dht_get_peers(&gid, &dht_get_peers)
|
||||
.expect("dht get_peers should succeed");
|
||||
|
||||
let dht_find_node = support::RecordingDhtTransport::new(DhtMessageModel::find_node_response(
|
||||
b"fn".to_vec(),
|
||||
vec![0x66; 20],
|
||||
vec![aria2_rust_pro_protocol::torrent::DhtCompactNodeModel {
|
||||
node_id: [0x77; 20],
|
||||
address: [203, 0, 113, 100],
|
||||
port: 51416,
|
||||
}],
|
||||
));
|
||||
dispatcher
|
||||
.execute_dht_find_node(&gid, &dht_find_node)
|
||||
.expect("dht find_node should succeed");
|
||||
|
||||
let dht_announce = support::RecordingDhtTransport::new(DhtMessageModel::ping_response(
|
||||
b"ap".to_vec(),
|
||||
vec![0x88; 20],
|
||||
));
|
||||
dispatcher
|
||||
.execute_dht_announce_peer(&gid, &dht_announce)
|
||||
.expect("dht announce_peer should succeed after token handoff");
|
||||
|
||||
let info_hash_bytes = support::decode_hex_20(&info_hash);
|
||||
let remote_peer_id = *b"-AZ2060-MODERN-PEER!";
|
||||
let connector = support::FakePeerWireConnector::new(support::peer_wire_handshake_and_frames(
|
||||
info_hash_bytes,
|
||||
remote_peer_id,
|
||||
&[
|
||||
PeerWireMessageKind::Unchoke,
|
||||
PeerWireMessageKind::Bitfield(PeerWireBitfieldModel::from_piece_flags(&[true, true])),
|
||||
PeerWireMessageKind::Piece(PeerWirePieceBlockModel {
|
||||
piece_index: 0,
|
||||
block_offset: 0,
|
||||
block: vec![0x5a; 16_384],
|
||||
}),
|
||||
],
|
||||
));
|
||||
dispatcher
|
||||
.execute_peer_wire_exchange(&gid, &connector)
|
||||
.expect("peer-wire exchange should succeed");
|
||||
|
||||
dispatcher
|
||||
.apply_bt_runtime_tick(&gid, 16_384, 0, 512, 0, 0, 0, false, Some(8))
|
||||
.expect("runtime tick should finish the remaining payload");
|
||||
dispatcher
|
||||
.set_bt_seeding_state(&gid, true, Some(1_000))
|
||||
.expect("completed torrent should enter seeding");
|
||||
dispatcher
|
||||
.tick_bt_runtime_clock(&gid, 1_030, true)
|
||||
.expect("share clock should advance");
|
||||
dispatcher
|
||||
.apply_bt_runtime_tick(&gid, 0, 16_384, 90, 180, 5, 5, true, Some(8))
|
||||
.expect("upload tick should populate visible share fields");
|
||||
let _ = dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2ChangeOption,
|
||||
vec![
|
||||
RpcValue::String(gid.clone()),
|
||||
RpcValue::Object(BTreeMap::from([(
|
||||
"select-file".to_owned(),
|
||||
RpcValue::String("1".to_owned()),
|
||||
)])),
|
||||
],
|
||||
));
|
||||
|
||||
let status = support::rpc_result_object(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2TellStatus,
|
||||
vec![RpcValue::String(gid.clone())],
|
||||
)));
|
||||
assert!(support::rpc_bool_field(&status, "isBt"));
|
||||
assert!(!support::rpc_bool_field(&status, "metadataOnly"));
|
||||
assert_eq!(support::rpc_string_field(&status, "magnetUri"), magnet_uri);
|
||||
assert_eq!(support::rpc_u64_field(&status, "completedLength"), 32_768);
|
||||
assert_eq!(support::rpc_u64_field(&status, "connections"), 8);
|
||||
assert_eq!(support::rpc_string_field(&status, "shareRatio"), "0.500");
|
||||
assert_eq!(support::rpc_u64_field(&status, "shareTime"), 35);
|
||||
assert!(support::rpc_bool_field(&status, "seeder"));
|
||||
assert_eq!(support::rpc_u64_field(&status, "numSeeders"), 1);
|
||||
|
||||
let files = support::rpc_result_array(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2GetFiles,
|
||||
vec![RpcValue::String(gid.clone())],
|
||||
)));
|
||||
match files.first() {
|
||||
Some(RpcValue::Object(file)) => {
|
||||
assert_eq!(
|
||||
file.get("selected"),
|
||||
Some(&RpcValue::String("true".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
file.get("path"),
|
||||
Some(&RpcValue::String("ubuntu.iso".to_owned()))
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected file payload after bt orchestration: {other:?}"),
|
||||
}
|
||||
|
||||
let peers = support::rpc_result_array(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2GetPeers,
|
||||
vec![RpcValue::String(gid.clone())],
|
||||
)));
|
||||
match peers.first() {
|
||||
Some(RpcValue::Object(peer)) => {
|
||||
assert_eq!(
|
||||
peer.get("peerId"),
|
||||
Some(&RpcValue::String(
|
||||
"2d415a323036302d4d4f4445524e2d5045455221".to_owned()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
peer.get("peerChoking"),
|
||||
Some(&RpcValue::String("false".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
peer.get("seeder"),
|
||||
Some(&RpcValue::String("true".to_owned()))
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected peer payload after bt orchestration: {other:?}"),
|
||||
}
|
||||
|
||||
let seen_announce = dht_announce.seen();
|
||||
assert_eq!(
|
||||
seen_announce.len(),
|
||||
1,
|
||||
"announce_peer should send exactly one query"
|
||||
);
|
||||
match &seen_announce
|
||||
.first()
|
||||
.expect("announce_peer should record one query")
|
||||
.1
|
||||
.body
|
||||
{
|
||||
DhtMessageBody::Query(DhtQueryModel::AnnouncePeer(query)) => {
|
||||
assert_eq!(query.info_hash, info_hash_bytes);
|
||||
assert_eq!(query.token, b"dht-token".to_vec());
|
||||
assert_eq!(query.port, 6881);
|
||||
}
|
||||
other => panic!("unexpected announce_peer query payload: {other:?}"),
|
||||
}
|
||||
|
||||
let seen_peer_wire = connector.seen();
|
||||
assert_eq!(
|
||||
seen_peer_wire.len(),
|
||||
1,
|
||||
"peer-wire transport should see one request"
|
||||
);
|
||||
assert!(
|
||||
seen_peer_wire
|
||||
.first()
|
||||
.expect("peer-wire transport should record one request")
|
||||
.payload
|
||||
.len()
|
||||
> 68,
|
||||
"peer-wire request should include the handshake plus follow-up frames"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bt_status_surfaces_remain_monotonic_under_public_runtime_tick_pressure() {
|
||||
let mut dispatcher = InProcessRpcDispatcher::new();
|
||||
let gid = support::add_torrent(&mut dispatcher);
|
||||
let mut last_completed = 0_u64;
|
||||
let mut last_share_time = 0_u64;
|
||||
|
||||
for round in 0..48_u64 {
|
||||
let downloaded_delta = if round < 16 { 2_048 } else { 0 };
|
||||
let uploaded_delta = if round >= 16 { 512 } else { 0 };
|
||||
let seeding = round >= 16;
|
||||
dispatcher
|
||||
.apply_bt_runtime_tick(
|
||||
&gid,
|
||||
downloaded_delta,
|
||||
uploaded_delta,
|
||||
256 + round,
|
||||
128 + round,
|
||||
u64::from(seeding),
|
||||
u64::from(seeding),
|
||||
seeding,
|
||||
Some(2 + u32::try_from(round % 5).expect("round modulo 5 should fit u32")),
|
||||
)
|
||||
.expect("runtime tick should succeed under repeated probing");
|
||||
if round == 16 {
|
||||
dispatcher
|
||||
.set_bt_seeding_state(&gid, true, Some(2_000))
|
||||
.expect("completed torrent should enter seeding");
|
||||
}
|
||||
if seeding {
|
||||
dispatcher
|
||||
.tick_bt_runtime_clock(&gid, 2_000 + round, true)
|
||||
.expect("runtime clock should advance while seeding");
|
||||
}
|
||||
|
||||
let status = support::rpc_result_object(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2TellStatus,
|
||||
vec![
|
||||
RpcValue::String(gid.clone()),
|
||||
RpcValue::Array(vec![
|
||||
RpcValue::String("status".to_owned()),
|
||||
RpcValue::String("completedLength".to_owned()),
|
||||
RpcValue::String("connections".to_owned()),
|
||||
RpcValue::String("files".to_owned()),
|
||||
RpcValue::String("bitfield".to_owned()),
|
||||
RpcValue::String("isBt".to_owned()),
|
||||
RpcValue::String("shareRatio".to_owned()),
|
||||
RpcValue::String("shareTime".to_owned()),
|
||||
RpcValue::String("seeder".to_owned()),
|
||||
]),
|
||||
],
|
||||
)));
|
||||
let completed = support::rpc_u64_field(&status, "completedLength");
|
||||
let share_time = support::rpc_u64_field(&status, "shareTime");
|
||||
assert!(support::rpc_bool_field(&status, "isBt"));
|
||||
assert!(
|
||||
completed >= last_completed,
|
||||
"completedLength should stay monotonic under repeated probes"
|
||||
);
|
||||
assert!(
|
||||
share_time >= last_share_time,
|
||||
"shareTime should stay monotonic under repeated probes"
|
||||
);
|
||||
assert!(
|
||||
status.contains_key("bitfield"),
|
||||
"bt tellStatus should retain bitfield visibility under pressure"
|
||||
);
|
||||
assert!(
|
||||
status.contains_key("files"),
|
||||
"bt tellStatus should retain files visibility under pressure"
|
||||
);
|
||||
last_completed = completed;
|
||||
last_share_time = share_time;
|
||||
|
||||
let files = support::rpc_result_array(dispatcher.dispatch_json(support::rpc_request(
|
||||
RpcMethod::Aria2GetFiles,
|
||||
vec![RpcValue::String(gid.clone())],
|
||||
)));
|
||||
assert_eq!(files.len(), 1, "torrent fixture should expose one file");
|
||||
match files.first() {
|
||||
Some(RpcValue::Object(file)) => {
|
||||
assert!(file.contains_key("selected"));
|
||||
assert!(file.contains_key("completedLength"));
|
||||
}
|
||||
other => panic!("unexpected getFiles payload under pressure: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user