679 lines
20 KiB
Rust
679 lines
20 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fs,
|
|
sync::Mutex,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use aria2_rust_pro_core::{
|
|
BtFileInfo, BtPeerInfo, BtPieceAvailabilityUpdate, BtRuntimeState, DownloadId, PieceId,
|
|
PieceState, RuntimeConfig,
|
|
};
|
|
use aria2_rust_pro_protocol::{
|
|
DhtMessageModel, DhtNodeModel, DhtTransport, TrackerRequestModel, TrackerResponseModel,
|
|
TrackerScrapeModel, TrackerTransport,
|
|
torrent::{
|
|
DhtGetPeersQueryModel, DhtMessageBody, DhtQueryModel, PeerWireBitfieldModel,
|
|
PeerWireExtensionHandshakeModel, PeerWireHandshakeModel, PeerWireMessageKind,
|
|
PeerWireMetadataMessageModel, PeerWireMetadataMessageType, PeerWirePieceBlockModel,
|
|
TorrentMessageModel, parse_torrent_metadata,
|
|
},
|
|
transport::{
|
|
PeerWireTransportConnector, PeerWireTransportRequest, PeerWireTransportResponse,
|
|
TransportEndpoint, TransportError, TransportErrorKind, TransportScheme,
|
|
},
|
|
};
|
|
use aria2_rust_pro_storage::load_session_file;
|
|
use base64::Engine;
|
|
|
|
use super::{
|
|
BtRuntimeCoordinatorAction, BtRuntimeCoordinatorStepStatus, InProcessRpcDispatcher,
|
|
bt_metadata_piece_span, decode_hex_string_exact,
|
|
};
|
|
use crate::{
|
|
jsonrpc::{JsonRpcRequest, jsonrpc_request_from_json, jsonrpc_response_to_json},
|
|
methods::RpcMethod,
|
|
model::{RpcError, RpcMeta, RpcValue},
|
|
xmlrpc::{XmlRpcMember, XmlRpcMethodCall, XmlRpcParam, XmlRpcValue},
|
|
};
|
|
|
|
#[doc(hidden)]
|
|
fn temp_session_path(name: &str) -> std::path::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-rpc-test-{}-{nanos}",
|
|
std::process::id()
|
|
));
|
|
fs::create_dir_all(&root).expect("temp dir should be creatable");
|
|
root.join(name)
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn request(method: RpcMethod, params: Vec<RpcValue>) -> JsonRpcRequest {
|
|
JsonRpcRequest {
|
|
jsonrpc: Some("2.0".to_owned()),
|
|
id: None,
|
|
method: method.as_str().to_owned(),
|
|
params,
|
|
meta: RpcMeta::default(),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn request_with_method_name(method: &str, params: Vec<RpcValue>) -> JsonRpcRequest {
|
|
JsonRpcRequest {
|
|
jsonrpc: Some("2.0".to_owned()),
|
|
id: None,
|
|
method: method.to_owned(),
|
|
params,
|
|
meta: RpcMeta::default(),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn add_uri(dispatcher: &mut InProcessRpcDispatcher, uri: &str) -> String {
|
|
let response = dispatcher.dispatch_json(request(
|
|
RpcMethod::Aria2AddUri,
|
|
vec![RpcValue::String(uri.to_owned())],
|
|
));
|
|
match response.result {
|
|
Some(RpcValue::String(gid)) => gid,
|
|
other => panic!("unexpected addUri result: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[test]
|
|
fn add_uri_direct_registers_uri_and_options_without_jsonrpc_roundtrip() {
|
|
let mut dispatcher = InProcessRpcDispatcher::new();
|
|
let gid = dispatcher
|
|
.add_uri_direct(
|
|
vec![
|
|
"https://example.org/direct-a.iso".to_owned(),
|
|
"https://example.org/direct-b.iso".to_owned(),
|
|
],
|
|
vec![("split".to_owned(), RpcValue::String("8".to_owned()))],
|
|
)
|
|
.expect("direct addUri should register");
|
|
let group = dispatcher
|
|
.engine
|
|
.handle_mut(download_id(&gid))
|
|
.expect("direct addUri group should exist");
|
|
|
|
assert_eq!(group.uri(), "https://example.org/direct-a.iso");
|
|
assert_eq!(
|
|
group.uris(),
|
|
&[
|
|
"https://example.org/direct-a.iso".to_owned(),
|
|
"https://example.org/direct-b.iso".to_owned(),
|
|
]
|
|
);
|
|
assert_eq!(group.option_limit("split"), Some(8));
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn download_id(gid: &str) -> DownloadId {
|
|
DownloadId::parse_hex(gid).expect("gid should parse into DownloadId")
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn compact_peer(ip: [u8; 4], port: u16) -> Vec<u8> {
|
|
let mut bytes = Vec::with_capacity(6);
|
|
bytes.extend_from_slice(&ip);
|
|
bytes.extend_from_slice(&port.to_be_bytes());
|
|
bytes
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn compact_node(node_id_byte: u8, ip: [u8; 4], port: u16) -> Vec<u8> {
|
|
let mut bytes = vec![node_id_byte; 20];
|
|
bytes.extend_from_slice(&ip);
|
|
bytes.extend_from_slice(&port.to_be_bytes());
|
|
bytes
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Debug)]
|
|
struct FakeDhtTransport {
|
|
#[doc(hidden)]
|
|
response: DhtMessageModel,
|
|
#[doc(hidden)]
|
|
seen: Mutex<Vec<(DhtNodeModel, DhtMessageModel)>>,
|
|
}
|
|
|
|
impl FakeDhtTransport {
|
|
#[doc(hidden)]
|
|
fn new(response: DhtMessageModel) -> Self {
|
|
Self {
|
|
response,
|
|
seen: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen(&self) -> Vec<(DhtNodeModel, DhtMessageModel)> {
|
|
self.seen
|
|
.lock()
|
|
.expect("seen requests mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
impl DhtTransport for FakeDhtTransport {
|
|
#[doc(hidden)]
|
|
fn send_message(
|
|
&self,
|
|
node: &DhtNodeModel,
|
|
message: &DhtMessageModel,
|
|
) -> Result<DhtMessageModel, TransportError> {
|
|
self.seen
|
|
.lock()
|
|
.expect("seen requests mutex should not be poisoned")
|
|
.push((node.clone(), message.clone()));
|
|
Ok(self.response.clone())
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Debug)]
|
|
struct FakeTrackerTransport {
|
|
#[doc(hidden)]
|
|
announce_response: TrackerResponseModel,
|
|
#[doc(hidden)]
|
|
scrape_response: Option<TrackerScrapeModel>,
|
|
#[doc(hidden)]
|
|
seen_announces: Mutex<Vec<TrackerRequestModel>>,
|
|
#[doc(hidden)]
|
|
seen_scrapes: Mutex<Vec<String>>,
|
|
}
|
|
|
|
impl FakeTrackerTransport {
|
|
#[doc(hidden)]
|
|
fn new(
|
|
announce_response: TrackerResponseModel,
|
|
scrape_response: Option<TrackerScrapeModel>,
|
|
) -> Self {
|
|
Self {
|
|
announce_response,
|
|
scrape_response,
|
|
seen_announces: Mutex::new(Vec::new()),
|
|
seen_scrapes: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen_announces(&self) -> Vec<TrackerRequestModel> {
|
|
self.seen_announces
|
|
.lock()
|
|
.expect("tracker announce mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen_scrapes(&self) -> Vec<String> {
|
|
self.seen_scrapes
|
|
.lock()
|
|
.expect("tracker scrape mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
impl TrackerTransport for FakeTrackerTransport {
|
|
#[doc(hidden)]
|
|
fn announce(
|
|
&self,
|
|
request: &TrackerRequestModel,
|
|
) -> Result<TrackerResponseModel, TransportError> {
|
|
self.seen_announces
|
|
.lock()
|
|
.expect("tracker announce mutex should not be poisoned")
|
|
.push(request.clone());
|
|
Ok(self.announce_response.clone())
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn scrape(&self, url: &str) -> Result<TrackerScrapeModel, TransportError> {
|
|
self.seen_scrapes
|
|
.lock()
|
|
.expect("tracker scrape mutex should not be poisoned")
|
|
.push(url.to_owned());
|
|
self.scrape_response.clone().ok_or_else(|| TransportError {
|
|
kind: TransportErrorKind::ProtocolViolation,
|
|
message: "scrape unavailable".to_owned(),
|
|
source: None,
|
|
context: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Debug)]
|
|
struct FakePeerWireConnector {
|
|
#[doc(hidden)]
|
|
response_payload: Vec<u8>,
|
|
#[doc(hidden)]
|
|
seen: Mutex<Vec<PeerWireTransportRequest>>,
|
|
}
|
|
|
|
impl FakePeerWireConnector {
|
|
#[doc(hidden)]
|
|
fn new(response_payload: Vec<u8>) -> Self {
|
|
Self {
|
|
response_payload,
|
|
seen: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen(&self) -> Vec<PeerWireTransportRequest> {
|
|
self.seen
|
|
.lock()
|
|
.expect("peer-wire seen requests mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Debug)]
|
|
struct SequencedPeerWireConnector {
|
|
#[doc(hidden)]
|
|
response_payloads: Mutex<Vec<Vec<u8>>>,
|
|
#[doc(hidden)]
|
|
seen: Mutex<Vec<PeerWireTransportRequest>>,
|
|
}
|
|
|
|
impl SequencedPeerWireConnector {
|
|
#[doc(hidden)]
|
|
fn new(response_payloads: Vec<Vec<u8>>) -> Self {
|
|
Self {
|
|
response_payloads: Mutex::new(response_payloads),
|
|
seen: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen(&self) -> Vec<PeerWireTransportRequest> {
|
|
self.seen
|
|
.lock()
|
|
.expect("sequenced peer-wire seen requests mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
impl PeerWireTransportConnector for SequencedPeerWireConnector {
|
|
#[doc(hidden)]
|
|
fn connect_peer_wire(
|
|
&self,
|
|
request: &PeerWireTransportRequest,
|
|
) -> Result<PeerWireTransportResponse, TransportError> {
|
|
self.seen
|
|
.lock()
|
|
.expect("sequenced peer-wire seen requests mutex should not be poisoned")
|
|
.push(request.clone());
|
|
let payload = self
|
|
.response_payloads
|
|
.lock()
|
|
.expect("sequenced peer-wire payload mutex should not be poisoned")
|
|
.remove(0);
|
|
Ok(PeerWireTransportResponse {
|
|
endpoint: TransportEndpoint {
|
|
scheme: TransportScheme::BitTorrent,
|
|
address: request.endpoint.address.clone(),
|
|
},
|
|
payload,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl PeerWireTransportConnector for FakePeerWireConnector {
|
|
#[doc(hidden)]
|
|
fn connect_peer_wire(
|
|
&self,
|
|
request: &PeerWireTransportRequest,
|
|
) -> Result<PeerWireTransportResponse, TransportError> {
|
|
self.seen
|
|
.lock()
|
|
.expect("peer-wire seen requests mutex should not be poisoned")
|
|
.push(request.clone());
|
|
Ok(PeerWireTransportResponse {
|
|
endpoint: TransportEndpoint {
|
|
scheme: TransportScheme::BitTorrent,
|
|
address: request.endpoint.address.clone(),
|
|
},
|
|
payload: self.response_payload.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Debug)]
|
|
struct RoutedDhtTransport {
|
|
#[doc(hidden)]
|
|
get_peers_response: DhtMessageModel,
|
|
#[doc(hidden)]
|
|
announce_peer_response: DhtMessageModel,
|
|
#[doc(hidden)]
|
|
find_node_response: Option<DhtMessageModel>,
|
|
#[doc(hidden)]
|
|
ping_response: Option<DhtMessageModel>,
|
|
#[doc(hidden)]
|
|
seen: Mutex<Vec<(DhtNodeModel, DhtMessageModel)>>,
|
|
}
|
|
|
|
impl RoutedDhtTransport {
|
|
#[doc(hidden)]
|
|
fn new(get_peers_response: DhtMessageModel, announce_peer_response: DhtMessageModel) -> Self {
|
|
Self {
|
|
get_peers_response,
|
|
announce_peer_response,
|
|
find_node_response: None,
|
|
ping_response: None,
|
|
seen: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn seen(&self) -> Vec<(DhtNodeModel, DhtMessageModel)> {
|
|
self.seen
|
|
.lock()
|
|
.expect("routed dht seen mutex should not be poisoned")
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
impl DhtTransport for RoutedDhtTransport {
|
|
#[doc(hidden)]
|
|
fn send_message(
|
|
&self,
|
|
node: &DhtNodeModel,
|
|
message: &DhtMessageModel,
|
|
) -> Result<DhtMessageModel, TransportError> {
|
|
self.seen
|
|
.lock()
|
|
.expect("routed dht seen mutex should not be poisoned")
|
|
.push((node.clone(), message.clone()));
|
|
match &message.body {
|
|
DhtMessageBody::Query(DhtQueryModel::GetPeers(_)) => {
|
|
Ok(self.get_peers_response.clone())
|
|
}
|
|
DhtMessageBody::Query(DhtQueryModel::AnnouncePeer(_)) => {
|
|
Ok(self.announce_peer_response.clone())
|
|
}
|
|
DhtMessageBody::Query(DhtQueryModel::FindNode(_)) => self
|
|
.find_node_response
|
|
.clone()
|
|
.ok_or_else(|| TransportError {
|
|
kind: TransportErrorKind::ProtocolViolation,
|
|
message: "find_node unavailable".to_owned(),
|
|
source: None,
|
|
context: None,
|
|
}),
|
|
DhtMessageBody::Query(DhtQueryModel::Ping(_)) => {
|
|
self.ping_response.clone().ok_or_else(|| TransportError {
|
|
kind: TransportErrorKind::ProtocolViolation,
|
|
message: "ping unavailable".to_owned(),
|
|
source: None,
|
|
context: None,
|
|
})
|
|
}
|
|
_ => Err(TransportError {
|
|
kind: TransportErrorKind::ProtocolViolation,
|
|
message: "unexpected dht method for routed transport".to_owned(),
|
|
source: None,
|
|
context: None,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn peer_wire_handshake_and_frames(
|
|
info_hash: [u8; 20],
|
|
peer_id: [u8; 20],
|
|
frames: &[PeerWireMessageKind],
|
|
) -> Vec<u8> {
|
|
let mut bytes = PeerWireHandshakeModel::new(info_hash, peer_id).serialize();
|
|
for frame in frames {
|
|
bytes.extend_from_slice(
|
|
&TorrentMessageModel::from_peer_wire_kind(frame.clone())
|
|
.serialize_peer_wire_frame()
|
|
.expect("peer-wire frame should serialize"),
|
|
);
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn peer_from_ip(ip: &str, port: u16) -> BtPeerInfo {
|
|
BtPeerInfo {
|
|
peer_id: None,
|
|
ip: ip.to_owned(),
|
|
port,
|
|
client_name: None,
|
|
interested: false,
|
|
choked: true,
|
|
download_speed: 0,
|
|
upload_speed: 0,
|
|
seeder: false,
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn single_file_torrent_bytes(name: &str, comment_len: usize) -> Vec<u8> {
|
|
let comment = "x".repeat(comment_len);
|
|
format!(
|
|
"d8:announce35:http://tracker.example.org/announce7:comment{}:{}4:infod6:lengthi2048e4:name{}:{}12:piece lengthi1024e6:pieces20:aaaaaaaaaaaaaaaaaaaaee",
|
|
comment.len(),
|
|
comment,
|
|
name.len(),
|
|
name
|
|
)
|
|
.into_bytes()
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[test]
|
|
fn bt_runtime_coordinator_snapshot_surfaces_partial_magnet_runtime_and_recommended_actions() {
|
|
let mut dispatcher = InProcessRpcDispatcher::new();
|
|
let gid = add_uri(
|
|
&mut dispatcher,
|
|
"magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678&tr=http%3A%2F%2Ftracker.example.org%2Fannounce",
|
|
);
|
|
let download_id = download_id(&gid);
|
|
{
|
|
let group = dispatcher
|
|
.engine
|
|
.handle_mut(download_id)
|
|
.expect("download group should exist");
|
|
group.set_piece_length(1_024);
|
|
group.set_total_length(4_096);
|
|
group.set_piece_state(PieceId(0), PieceState::Pending);
|
|
group.set_piece_state(PieceId(1), PieceState::Missing);
|
|
group.set_dht_token(Some(b"cached-token".to_vec()));
|
|
let bt = group.bt_mut().expect("bt runtime state should exist");
|
|
bt.dht_nodes = vec!["bad-node".to_owned(), "127.0.0.9:6881".to_owned()];
|
|
bt.peers = vec![peer_from_ip("127.0.0.7", 51413)];
|
|
}
|
|
|
|
let snapshot = dispatcher
|
|
.bt_runtime_coordinator_snapshot(&gid)
|
|
.expect("snapshot should inspect bt runtime");
|
|
|
|
assert!(snapshot.metadata_only);
|
|
assert!(snapshot.metadata_exchange_pending);
|
|
assert_eq!(snapshot.tracker_count, 1);
|
|
assert_eq!(snapshot.dht_node_count, 2);
|
|
assert_eq!(snapshot.addressable_dht_node_count, 1);
|
|
assert_eq!(snapshot.peer_count, 1);
|
|
assert_eq!(snapshot.connectable_peer_count, 1);
|
|
assert_eq!(snapshot.requestable_piece_count, 2);
|
|
assert_eq!(
|
|
snapshot.recommended_actions,
|
|
vec![
|
|
BtRuntimeCoordinatorAction::TrackerAnnounce,
|
|
BtRuntimeCoordinatorAction::DhtGetPeers,
|
|
BtRuntimeCoordinatorAction::DhtAnnouncePeer,
|
|
BtRuntimeCoordinatorAction::PeerWireExchange,
|
|
]
|
|
);
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[test]
|
|
fn drive_bt_runtime_once_executes_newly_unlocked_bt_steps_within_one_iteration() {
|
|
let mut dispatcher = InProcessRpcDispatcher::new();
|
|
let gid = add_uri(
|
|
&mut dispatcher,
|
|
"magnet:?xt=urn:btih:fedcba9876543210fedcba9876543210fedcba98&tr=http%3A%2F%2Ftracker.example.org%2Fannounce",
|
|
);
|
|
let download_id = download_id(&gid);
|
|
{
|
|
let group = dispatcher
|
|
.engine
|
|
.handle_mut(download_id)
|
|
.expect("download group should exist");
|
|
group.set_piece_length(1_024);
|
|
group.set_total_length(2_048);
|
|
group.set_piece_state(PieceId(0), PieceState::Pending);
|
|
group.set_piece_state(PieceId(1), PieceState::Missing);
|
|
let bt = group.bt_mut().expect("bt runtime state should exist");
|
|
bt.dht_nodes = vec!["127.0.0.11:6881".to_owned()];
|
|
}
|
|
|
|
let tracker = FakeTrackerTransport::new(
|
|
TrackerResponseModel {
|
|
peers: aria2_rust_pro_protocol::TrackerPeerListModel {
|
|
interval_sec: 1_800,
|
|
peers: vec![aria2_rust_pro_protocol::torrent::TorrentPeerModel {
|
|
ip: "127.0.0.21".to_owned(),
|
|
port: 51_413,
|
|
peer_id: Some(*b"12345678901234567890"),
|
|
client_name: Some("tracker-peer".to_owned()),
|
|
interested: false,
|
|
choked: false,
|
|
}],
|
|
min_interval_sec: None,
|
|
tracker_id: Some("tracker-id".to_owned()),
|
|
},
|
|
scrape: None,
|
|
},
|
|
Some(TrackerScrapeModel {
|
|
complete: Some(5),
|
|
downloaded: Some(8),
|
|
incomplete: Some(3),
|
|
files: Vec::new(),
|
|
}),
|
|
);
|
|
let dht = RoutedDhtTransport::new(
|
|
DhtMessageModel::get_peers_response(
|
|
b"gp".to_vec(),
|
|
vec![0x99; 20],
|
|
Some(b"announce-token".to_vec()),
|
|
Some(compact_node(0x77, [127, 0, 0, 31], 6882)),
|
|
Vec::new(),
|
|
),
|
|
DhtMessageModel::ping_response(b"ap".to_vec(), vec![0x55; 20]),
|
|
);
|
|
let connector = FakePeerWireConnector::new(peer_wire_handshake_and_frames(
|
|
[
|
|
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
|
|
0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98,
|
|
],
|
|
*b"-PC0001-LOOP-PEER-01",
|
|
&[PeerWireMessageKind::Unchoke],
|
|
));
|
|
|
|
let report = dispatcher
|
|
.drive_bt_runtime_once(
|
|
&gid,
|
|
Some(&tracker),
|
|
Some(&dht),
|
|
Some(&connector),
|
|
Some(1_050),
|
|
)
|
|
.expect("coordinator loop should run");
|
|
|
|
assert!(report.initial_snapshot.metadata_exchange_pending);
|
|
assert!(report.final_snapshot.metadata_exchange_pending);
|
|
assert_eq!(
|
|
report
|
|
.steps
|
|
.iter()
|
|
.map(|step| (step.action, step.status))
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
(
|
|
BtRuntimeCoordinatorAction::AdvanceClock,
|
|
BtRuntimeCoordinatorStepStatus::Executed,
|
|
),
|
|
(
|
|
BtRuntimeCoordinatorAction::TrackerAnnounce,
|
|
BtRuntimeCoordinatorStepStatus::Executed,
|
|
),
|
|
(
|
|
BtRuntimeCoordinatorAction::DhtGetPeers,
|
|
BtRuntimeCoordinatorStepStatus::Executed,
|
|
),
|
|
(
|
|
BtRuntimeCoordinatorAction::DhtAnnouncePeer,
|
|
BtRuntimeCoordinatorStepStatus::Executed,
|
|
),
|
|
(
|
|
BtRuntimeCoordinatorAction::PeerWireExchange,
|
|
BtRuntimeCoordinatorStepStatus::Executed,
|
|
),
|
|
]
|
|
);
|
|
|
|
assert_eq!(tracker.seen_announces().len(), 1);
|
|
assert_eq!(
|
|
tracker.seen_scrapes(),
|
|
vec!["http://tracker.example.org/announce".to_owned()]
|
|
);
|
|
assert_eq!(
|
|
dht.seen().len(),
|
|
2,
|
|
"get_peers plus announce_peer should run"
|
|
);
|
|
assert_eq!(
|
|
connector.seen().len(),
|
|
1,
|
|
"peer-wire should run after peers arrive"
|
|
);
|
|
|
|
let final_snapshot = dispatcher
|
|
.bt_runtime_coordinator_snapshot(&gid)
|
|
.expect("final snapshot should remain readable");
|
|
assert!(final_snapshot.has_dht_token);
|
|
assert!(final_snapshot.connectable_peer_count >= 1);
|
|
assert!(final_snapshot.addressable_dht_node_count >= 2);
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn xml_request(method_name: &str) -> XmlRpcMethodCall {
|
|
XmlRpcMethodCall {
|
|
method_name: method_name.to_owned(),
|
|
params: Vec::new(),
|
|
meta: RpcMeta::default(),
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn xml_request_with_params(method_name: &str, params: Vec<XmlRpcValue>) -> XmlRpcMethodCall {
|
|
XmlRpcMethodCall {
|
|
method_name: method_name.to_owned(),
|
|
params: params
|
|
.into_iter()
|
|
.map(|value| XmlRpcParam { value })
|
|
.collect(),
|
|
meta: RpcMeta::default(),
|
|
}
|
|
}
|
|
|
|
mod bt_and_extension;
|
|
mod protocol_surface;
|
|
mod queue_and_options;
|