#![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:?}"), } } }