refactor(tests): share one soulfind fixture across the e2e suites

The TUI suite had grown its own copy of the soulfind spawner the protocol
suite already carried, and its own copy of the CLI suite's in-process
client and settle helpers. All four suites now spawn, log in, settle and
skip through `tests/common`: the CLI suite's own server goes too, and
the fixture checks its child is still alive before calling the port
answered, so a lost port cannot pass another server off as ours.
This commit is contained in:
Michel de Graaf
2026-09-08 13:12:12 +02:00
parent 76f4854169
commit 3ef78c6de1
7 changed files with 189 additions and 332 deletions
+117
View File
@@ -3,7 +3,11 @@
#![allow(dead_code)]
use soulseek_rs::{Client, ClientSettings, PeerAddress};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
/// Every environment variable the binary reads, cleared for each run so a
/// developer's shell (or a stray `.env`) cannot change what a test observes —
@@ -44,3 +48,116 @@ pub fn free_port() -> Option<u16> {
.ok()
.map(|addr| addr.port())
}
/// A soulfind spawned for one test, on a port nobody else holds, with a
/// throwaway database that goes with it.
pub struct Soulfind {
child: Child,
db: PathBuf,
port: u16,
}
impl Soulfind {
pub fn start() -> Option<Self> {
let port = free_port()?;
let db = std::env::temp_dir().join(format!("soulfind-{port}.db"));
let _ = std::fs::remove_file(&db);
let child = Self::spawn(port, &db)?;
Some(Self { child, db, port })
}
fn spawn(port: u16, db: &Path) -> Option<Child> {
let mut child = Command::new(soulfind_binary()?)
.args(["-d", db.to_str()?, "-p", &port.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
// Whoever answers on the port has to be this child: one that
// lost the port and died would otherwise pass as up.
if child.try_wait().ok().flatten().is_some() {
return None;
}
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return Some(child);
}
std::thread::sleep(Duration::from_millis(100));
}
let _ = child.kill();
let _ = child.wait();
None
}
pub const fn port(&self) -> u16 {
self.port
}
pub fn address(&self) -> String {
format!("127.0.0.1:{}", self.port)
}
pub fn stop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
/// Bring it back on the same port and database.
pub fn restart(&mut self) -> Option<()> {
self.stop();
self.child = Self::spawn(self.port, &self.db)?;
Some(())
}
/// A client logged in to this server as `user`, sharing `shares`.
pub fn client(&self, user: &str, shares: Vec<String>) -> Client {
login("127.0.0.1", self.port, user, shares)
}
}
impl Drop for Soulfind {
fn drop(&mut self) {
self.stop();
let _ = std::fs::remove_file(&self.db);
}
}
/// An in-process client logged in to the server at `host:port` as `user`,
/// sharing `shares`: the other end of a test.
///
/// The listener stays on: a peer delivers search results and transfers over
/// a connection it opens back to us, so a client without one hears nothing.
pub fn login(host: &str, port: u16, user: &str, shares: Vec<String>) -> Client {
let mut client = Client::with_settings(ClientSettings {
username: user.to_string(),
password: "pw".to_string(),
server_address: PeerAddress::new(host.to_string(), port),
enable_listen: true,
listen_port: free_port().expect("peer port"),
shared_directories: shares,
version: soulseek_rs::ClientVersion::default(),
});
client.connect().expect("peer connect");
assert!(client.login().expect("peer login"), "peer should log in");
client
}
/// Wait out the SetWaitPort registrations so peer lookups resolve.
pub fn settle() {
std::thread::sleep(Duration::from_secs(1));
}
/// What a suite does without a server: fail when the run said one is
/// required, otherwise say it skipped. The caller returns afterwards.
pub fn no_server(suite: &str) {
let required = std::env::var("SOULSEEK_E2E_REQUIRED")
.is_ok_and(|v| v != "0" && !v.is_empty());
assert!(
!required,
"SOULSEEK_E2E_REQUIRED is set but no soulfind server could be \
started for the {suite} suite (set SOULFIND_BIN)"
);
println!("{suite} skipped: no soulfind server (set SOULFIND_BIN to run)");
}