318 lines
12 KiB
Rust
318 lines
12 KiB
Rust
#![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:?}"),
|
|
}
|
|
}
|
|
}
|