Files
soulseek-rs-modded/soulseek-rs-lib/tests/resume_e2e.rs
T
Michel de Graaf 460031b532 feat(distributed): serve children in the search tree
The client read the distributed network but never carried it: a peer
dialling with a D connection was logged and dropped, and AcceptChildren
was hardcoded false, so this client took from the tree without ever
relaying a search for anyone else.

With accept_children set, peers hang from us: each is told where our
branch sits when it connects and again when the branch moves, every
search that reaches us is passed down, and a child that goes away loses
its slot. Ten at a time, matching Nicotine+, since each costs a socket
and a copy of the network's whole search stream — which is also why it is
opt-in, through the new accept_children config key (ignored without a
listener, since a child would have nothing to dial).
2026-09-09 16:47:55 +00:00

410 lines
13 KiB
Rust

//! Server-independent end-to-end coverage for downloads that resume.
//!
//! The subject is a real `Client` with its real peer listener and actor stack.
//! A loopback socket stands in for a Soulseek peer and speaks the public wire
//! format. The configured server is deliberately idle: peer transfers do not
//! require a login once the control connection is registered.
use std::fs;
use std::io::{Read, Write};
use std::net::{Shutdown, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};
use soulseek_rs::message::Message;
use soulseek_rs::{
Client, ClientSettings, ClientVersion, DownloadStatus, PeerAddress,
};
const IO_TIMEOUT: Duration = Duration::from_secs(10);
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new(label: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"soulseek-resume-e2e-{}-{label}",
std::process::id()
));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).expect("create scratch directory");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
struct Subject {
client: Client,
peer_addr: String,
// Keeping a server listener alive is enough for ServerActor's TCP connect.
// No login or server messages are needed by the peer-transfer path.
_idle_server: TcpListener,
}
impl Subject {
fn new(username: &str) -> Self {
let idle_server =
TcpListener::bind("127.0.0.1:0").expect("bind idle server");
let server_addr =
idle_server.local_addr().expect("idle server address");
let mut client = Client::with_settings(ClientSettings {
username: username.to_string(),
password: "unused".to_string(),
server_address: PeerAddress::new(
"127.0.0.1".to_string(),
server_addr.port(),
),
enable_listen: true,
listen_port: 0,
shared_directories: Vec::new(),
accept_children: false,
version: ClientVersion::default(),
});
client.connect().expect("connect subject");
let port = client.listen_port().expect("subject listener port");
Self {
client,
peer_addr: format!("127.0.0.1:{port}"),
_idle_server: idle_server,
}
}
}
struct ControlPeer {
username: String,
subject_addr: String,
stream: TcpStream,
}
impl ControlPeer {
fn register(subject: &Subject, username: &str) -> Self {
let mut stream = TcpStream::connect(&subject.peer_addr)
.expect("connect peer control socket");
stream
.set_read_timeout(Some(IO_TIMEOUT))
.expect("control read timeout");
stream
.set_write_timeout(Some(IO_TIMEOUT))
.expect("control write timeout");
// Code 15/16 is a positive registration fence. Receiving the response
// proves the inbound P connection has reached the peer actor and can
// receive QueueUpload messages from `Client::download`.
stream
.write_all(&peer_init_bytes(username, "P", 0))
.expect("send P PeerInit");
stream
.write_all(&Message::new().write_int32(15).get_buffer())
.expect("send UserInfoRequest registration fence");
stream.flush().expect("flush registration fence");
let _ = expect_code(&mut stream, 16);
Self {
username: username.to_string(),
subject_addr: subject.peer_addr.clone(),
stream,
}
}
fn accept_queue_and_offer(&mut self, filename: &str, size: u64) -> u32 {
let mut queue = expect_code(&mut self.stream, 43);
queue.set_pointer(8);
assert_eq!(queue.read_string(), filename, "queued remote filename");
let token = 700_000;
let mut request = Message::new();
request
.write_int32(40)
.write_int32(1) // upload from the peer to the subject
.write_int32(token)
.write_string(filename)
.write_int64(size);
self.stream
.write_all(&request.get_buffer())
.expect("send TransferRequest");
self.stream.flush().expect("flush TransferRequest");
let mut response = expect_code(&mut self.stream, 41);
response.set_pointer(8);
assert_eq!(response.read_int32(), token, "response transfer token");
assert_eq!(response.read_int8(), 1, "transfer should be allowed");
token
}
fn open_file_transfer(&self, token: u32) -> (TcpStream, u64) {
let mut stream = TcpStream::connect(&self.subject_addr)
.expect("connect peer file socket");
stream
.set_read_timeout(Some(IO_TIMEOUT))
.expect("file read timeout");
stream
.set_write_timeout(Some(IO_TIMEOUT))
.expect("file write timeout");
let mut init = peer_init_bytes(&self.username, "F", token);
init.extend_from_slice(&token.to_le_bytes());
stream.write_all(&init).expect("send F PeerInit and token");
stream.flush().expect("flush F PeerInit and token");
let mut offset = [0u8; 8];
stream
.read_exact(&mut offset)
.expect("read START_DOWNLOAD offset");
(stream, u64::from_le_bytes(offset))
}
}
fn peer_init_bytes(
username: &str,
connection_type: &str,
token: u32,
) -> Vec<u8> {
Message::new()
.write_int8(1)
.write_string(username)
.write_string(connection_type)
.write_int32(token)
.get_buffer()
}
fn read_framed(stream: &mut TcpStream) -> std::io::Result<Message> {
let mut len = [0u8; 4];
stream.read_exact(&mut len)?;
let mut payload = vec![0u8; u32::from_le_bytes(len) as usize];
stream.read_exact(&mut payload)?;
let mut bytes = len.to_vec();
bytes.extend_from_slice(&payload);
Ok(Message::new_with_data(bytes))
}
fn expect_code(stream: &mut TcpStream, expected: u32) -> Message {
let deadline = Instant::now() + IO_TIMEOUT;
loop {
let message = read_framed(stream).unwrap_or_else(|error| {
panic!("read peer code {expected}: {error}")
});
if message.get_message_code() == expected {
return message;
}
assert!(
Instant::now() < deadline,
"timed out waiting for peer code {expected}"
);
}
}
fn start_download(
subject: &Subject,
control: &mut ControlPeer,
filename: &str,
content_len: usize,
destination: &Path,
) -> (Receiver<DownloadStatus>, TcpStream, u64) {
let (_download, statuses) = subject
.client
.download(
filename.to_string(),
control.username.clone(),
content_len as u64,
destination.display().to_string(),
)
.expect("queue download");
let token = control.accept_queue_and_offer(filename, content_len as u64);
let (file_stream, offset) = control.open_file_transfer(token);
(statuses, file_stream, offset)
}
fn wait_for_status(
statuses: &Receiver<DownloadStatus>,
description: &str,
wanted: impl Fn(&DownloadStatus) -> bool,
) -> DownloadStatus {
let deadline = Instant::now() + IO_TIMEOUT;
loop {
if let Ok(status) = statuses.recv_timeout(Duration::from_millis(100))
&& wanted(&status)
{
return status;
}
assert!(
Instant::now() < deadline,
"status channel did not report {description}"
);
}
}
fn assert_stored_status(
subject: &Subject,
filename: &str,
wanted: impl Fn(&DownloadStatus) -> bool,
) {
let status = subject
.client
.get_all_downloads()
.into_iter()
.find(|download| download.filename == filename)
.unwrap_or_else(|| panic!("download {filename} is missing from store"))
.status;
assert!(wanted(&status), "unexpected stored status: {status:?}");
}
fn expect_downloader_close(stream: &mut TcpStream) {
let mut unexpected = [0u8; 1];
assert_eq!(
stream
.read(&mut unexpected)
.expect("wait for downloader to close the F socket"),
0,
"the downloader sends no bytes after its offset"
);
}
fn binary_payload(len: usize) -> Vec<u8> {
(0..len).map(|index| index as u8).collect()
}
#[test]
fn an_actual_short_transfer_resumes_opaque_bytes() {
let subject = Subject::new("resume_binary_subject");
let mut control = ControlPeer::register(&subject, "resume_binary_peer");
let destination = ScratchDir::new("binary-tracer");
let content = binary_payload(3 * 64 * 1024 + 257);
let filename = "opaque.bin";
let interrupt_at = 64 * 1024 + 1;
let part_path = destination.path().join(format!("{filename}.part"));
let final_path = destination.path().join(filename);
assert!(content.contains(&0));
assert!(content.contains(&u8::MAX));
assert!(std::str::from_utf8(&content).is_err());
let (first_statuses, mut first, first_offset) = start_download(
&subject,
&mut control,
filename,
content.len(),
destination.path(),
);
assert_eq!(first_offset, 0);
first
.write_all(&content[..interrupt_at])
.expect("write short first transfer");
first.flush().expect("flush short first transfer");
first
.shutdown(Shutdown::Write)
.expect("interrupt first transfer");
drop(first);
let _ = wait_for_status(&first_statuses, "Failed", |status| {
matches!(status, DownloadStatus::Failed(_))
});
assert_stored_status(&subject, filename, |status| {
matches!(status, DownloadStatus::Failed(_))
});
assert!(!final_path.exists());
assert_eq!(
fs::read(&part_path).expect("read retained partial file"),
content[..interrupt_at]
);
let (retry_statuses, mut retry, retry_offset) = start_download(
&subject,
&mut control,
filename,
content.len(),
destination.path(),
);
assert_eq!(retry_offset, interrupt_at as u64);
retry
.write_all(&content[interrupt_at..])
.expect("write retry tail");
retry.flush().expect("flush retry tail");
expect_downloader_close(&mut retry);
drop(retry);
let _ = wait_for_status(&retry_statuses, "Completed", |status| {
matches!(status, DownloadStatus::Completed)
});
assert_stored_status(&subject, filename, |status| {
matches!(status, DownloadStatus::Completed)
});
assert_eq!(fs::read(&final_path).unwrap(), content);
assert!(!part_path.exists());
}
#[test]
fn a_live_download_pauses_and_resumes_on_the_same_connection() {
let subject = Subject::new("resume_pause_subject");
let mut control = ControlPeer::register(&subject, "resume_pause_peer");
let destination = ScratchDir::new("pause-resume");
let filename = "paused.bin";
let content = binary_payload(5 * 64 * 1024 + 257);
let part_path = destination.path().join(format!("{filename}.part"));
let (statuses, mut peer, offset) = start_download(
&subject,
&mut control,
filename,
content.len(),
destination.path(),
);
assert_eq!(offset, 0);
let first_chunk = 2 * 64 * 1024;
peer.write_all(&content[..first_chunk])
.expect("send first chunk");
peer.flush().expect("flush first chunk");
let _ = wait_for_status(&statuses, "initial progress", |status| {
matches!(
status,
DownloadStatus::InProgress {
bytes_downloaded,
..
} if *bytes_downloaded >= first_chunk as u64
)
});
assert!(subject.client.pause_download(&control.username, filename));
let _ = wait_for_status(&statuses, "Paused", |status| {
matches!(status, DownloadStatus::Paused { .. })
});
// Give a read already in flight one timeout cycle to observe the pause.
std::thread::sleep(Duration::from_millis(1_200));
let stable_len = fs::metadata(&part_path).expect("paused partial").len();
let queued_while_paused = 8 * 1024;
peer.write_all(&content[first_chunk..first_chunk + queued_while_paused])
.expect("queue bytes while paused");
peer.flush().expect("flush bytes while paused");
std::thread::sleep(Duration::from_millis(300));
assert_eq!(
fs::metadata(&part_path).expect("paused partial").len(),
stable_len,
"a paused transfer must stop consuming its socket"
);
assert!(subject.client.resume_download(&control.username, filename));
peer.write_all(&content[first_chunk + queued_while_paused..])
.expect("send tail after resume");
peer.flush().expect("flush resumed tail");
expect_downloader_close(&mut peer);
let _ = wait_for_status(&statuses, "Completed", |status| {
matches!(status, DownloadStatus::Completed)
});
assert_eq!(
fs::read(destination.path().join(filename)).unwrap(),
content
);
}