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
+1 -2
View File
@@ -111,8 +111,7 @@ jobs:
run: cargo test -p soulseek-rs --test cli_e2e -- --nocapture run: cargo test -p soulseek-rs --test cli_e2e -- --nocapture
# The window over a real session: keys in, a screen out, a peer on the # The window over a real session: keys in, a screen out, a peer on the
# other end. The unit tests drive it against a stand-in session and the # other end. Nothing else opens the TUI against a server.
# CLI suite never opens it, so this is the only place the two meet.
- name: Run TUI e2e tests (required against soulfind) - name: Run TUI e2e tests (required against soulfind)
env: env:
SOULSEEK_E2E_REQUIRED: "1" SOULSEEK_E2E_REQUIRED: "1"
+12 -12
View File
@@ -621,19 +621,19 @@ cargo fmt
### End-to-end tests ### End-to-end tests
Three suites run against [soulfind](https://github.com/soulfind-dev/soulfind), Four suites run against [soulfind](https://github.com/soulfind-dev/soulfind),
a local Soulseek server: `soulseek-rs-lib/tests/e2e.rs` covers the protocol a local Soulseek server: `soulseek-rs-lib/tests/e2e.rs` covers the protocol
library, `soulseek-rs/tests/cli_e2e.rs` drives the binary the way a script library, `soulseek-rs/tests/cli_e2e.rs` drives the binary the way a script
would, and `soulseek-rs/tests/tui_e2e.rs` drives the window with key presses would, `soulseek-rs/tests/daemon_protocol_e2e.rs` meets the daemon's control
and reads the screen back. All are **server-optional**, running when a server protocol as a third party would, and `soulseek-rs/tests/tui_e2e.rs` drives the
is available and skipping otherwise, so `cargo test` stays green everywhere. window with key presses and reads the screen back. All are
**server-optional**, running when a server is available and skipping
otherwise, so `cargo test` stays green everywhere.
They locate a server in this order: Every suite spawns soulfind from `SOULFIND_BIN=/path/to/soulfind` (or a
`soulfind/bin/soulfind` checkout in a parent directory) on an ephemeral port
1. `SOULSEEK_TEST_SERVER=host:port`: connect to an already-running server, or with a throwaway database. The library and CLI suites take
2. `SOULFIND_BIN=/path/to/soulfind` (or a `soulfind/bin/soulfind` checkout in a `SOULSEEK_TEST_SERVER=host:port` first, to run against a server already up.
parent directory), which spawns soulfind on an ephemeral port with a
throwaway database.
```bash ```bash
# Build soulfind once (see its BUILDING.md), then: # Build soulfind once (see its BUILDING.md), then:
@@ -659,8 +659,8 @@ soulfind instead of skipping.
`cargo clippy --workspace --all-targets -- -D warnings`. `cargo clippy --workspace --all-targets -- -D warnings`.
- **Test**: `cargo test --verbose` on Linux, macOS, and Windows. - **Test**: `cargo test --verbose` on Linux, macOS, and Windows.
- **End-to-end**: builds soulfind from source (LDC + `dub build :server`), - **End-to-end**: builds soulfind from source (LDC + `dub build :server`),
points `SOULFIND_BIN` at it, and runs both the library and CLI e2e suites points `SOULFIND_BIN` at it, and runs all four e2e suites with
with `SOULSEEK_E2E_REQUIRED=1`, so a missing server fails instead of skipping. `SOULSEEK_E2E_REQUIRED=1`, so a missing server fails instead of skipping.
## Contributing ## Contributing
+8 -19
View File
@@ -213,14 +213,9 @@ impl MainTui {
self.show_search(self.state.searches.len() - 1); self.show_search(self.state.searches.len() - 1);
} }
/// Run the search at `index` again, in its place. /// Run the search at `index` again, in its row: a restored query comes
/// /// back with nothing under it, and this saves typing it out. A run still
/// This is what a restored list is for: last session's queries come back /// collecting is stopped first, or two threads would fill one set.
/// with nothing under them, and typing one out again to see what the
/// network has now is the work the list was meant to save. The entry
/// keeps its row rather than moving to the end — the user is looking at
/// it — and a run still collecting is stopped first, or two threads would
/// be pouring into one result set.
pub(super) fn rerun_search_at_index(&mut self, index: usize) { pub(super) fn rerun_search_at_index(&mut self, index: usize) {
let Some(previous) = self.state.searches.get(index) else { let Some(previous) = self.state.searches.get(index) else {
return; return;
@@ -255,18 +250,12 @@ impl MainTui {
let client = self.client.clone(); let client = self.client.clone();
let timeout = self.search_timeout; let timeout = self.search_timeout;
// Results are polled in update_search_results, not returned here.
thread::spawn(move || { thread::spawn(move || {
match client.search_with_cancel( if let Err(e) =
&query, client.search_with_cancel(&query, timeout, Some(cancel_flag))
timeout, {
Some(cancel_flag.clone()), soulseek_rs::warn!("Search failed: {e}");
) {
Ok(_results) => {
// Results will be polled in update_search_results
}
Err(e) => {
soulseek_rs::warn!("Search failed: {e}");
}
} }
}); });
+11 -73
View File
@@ -11,8 +11,8 @@
mod common; mod common;
use common::{CLI_ENV_VARS, free_port, soulfind_binary}; use common::{CLI_ENV_VARS, Soulfind, free_port, login, settle};
use soulseek_rs::{Client, ClientSettings, PeerAddress}; use soulseek_rs::Client;
use std::io::Write; use std::io::Write;
use std::net::{TcpStream, ToSocketAddrs}; use std::net::{TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -508,8 +508,9 @@ static SERVER_GATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct TestServer { struct TestServer {
host: String, host: String,
port: u16, port: u16,
child: Option<Child>, /// The soulfind spawned for this run; `None` against a server named by
db: Option<PathBuf>, /// `SOULSEEK_TEST_SERVER`.
_server: Option<Soulfind>,
_gate: std::sync::MutexGuard<'static, ()>, _gate: std::sync::MutexGuard<'static, ()>,
/// Daemons started for this server, one per (user, session flags). Dropped /// Daemons started for this server, one per (user, session flags). Dropped
/// with the server, which kills them. /// with the server, which kills them.
@@ -640,41 +641,17 @@ impl TestServer {
return Some(Self { return Some(Self {
host: host.to_string(), host: host.to_string(),
port, port,
child: None, _server: None,
db: None,
_gate: gate, _gate: gate,
daemons: std::sync::Mutex::new(std::collections::HashMap::new()), daemons: std::sync::Mutex::new(std::collections::HashMap::new()),
}); });
} }
let bin = soulfind_binary()?; let server = Soulfind::start()?;
let port = free_port()?;
let db = std::env::temp_dir().join(format!("soulfind-cli-{port}.db"));
let _ = std::fs::remove_file(&db);
let mut child = Command::new(&bin)
.arg("-p")
.arg(port.to_string())
.arg("-d")
.arg(&db)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
if wait_until_listening("127.0.0.1", port, Duration::from_secs(5))
.is_none()
{
let _ = child.kill();
let _ = child.wait();
return None;
}
Some(Self { Some(Self {
host: "127.0.0.1".to_string(), host: "127.0.0.1".to_string(),
port, port: server.port(),
child: Some(child), _server: Some(server),
db: Some(db),
_gate: gate, _gate: gate,
daemons: std::sync::Mutex::new(std::collections::HashMap::new()), daemons: std::sync::Mutex::new(std::collections::HashMap::new()),
}) })
@@ -777,31 +754,7 @@ impl TestServer {
/// An in-process client, used to stand up the other end of a test. /// An in-process client, used to stand up the other end of a test.
fn client(&self, user: &str, shares: Vec<String>) -> Client { fn client(&self, user: &str, shares: Vec<String>) -> Client {
let port = free_port().expect("peer port"); login(&self.host, self.port, user, shares)
let mut client = Client::with_settings(ClientSettings {
username: user.to_string(),
password: "pw".to_string(),
server_address: PeerAddress::new(self.host.clone(), self.port),
enable_listen: true,
listen_port: 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
}
}
impl Drop for TestServer {
fn drop(&mut self) {
if let Some(child) = self.child.as_mut() {
let _ = child.kill();
let _ = child.wait();
}
if let Some(db) = self.db.as_ref() {
let _ = std::fs::remove_file(db);
}
} }
} }
@@ -829,17 +782,7 @@ macro_rules! server_or_skip {
match TestServer::resolve() { match TestServer::resolve() {
Some(server) => server, Some(server) => server,
None => { None => {
let required = std::env::var("SOULSEEK_E2E_REQUIRED") common::no_server("cli e2e");
.is_ok_and(|v| v != "0" && !v.is_empty());
assert!(
!required,
"SOULSEEK_E2E_REQUIRED is set but no soulfind server could \
be started (set SOULFIND_BIN or SOULSEEK_TEST_SERVER)"
);
println!(
"cli e2e skipped: no soulfind server (set SOULFIND_BIN or \
SOULSEEK_TEST_SERVER to run)"
);
return; return;
} }
} }
@@ -936,11 +879,6 @@ fn split_session_flags(args: &[&str]) -> (Vec<String>, Vec<String>) {
(session, command) (session, command)
} }
/// Wait out the SetWaitPort registrations so peer lookups resolve.
fn settle() {
std::thread::sleep(Duration::from_secs(1));
}
/// Like [`cli`], but keeping a config file so a command that reads or writes /// Like [`cli`], but keeping a config file so a command that reads or writes
/// one (the wishlist) sees it. /// one (the wishlist) sees it.
/// ///
+117
View File
@@ -3,7 +3,11 @@
#![allow(dead_code)] #![allow(dead_code)]
use soulseek_rs::{Client, ClientSettings, PeerAddress};
use std::net::TcpStream;
use std::path::{Path, PathBuf}; 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 /// 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 — /// developer's shell (or a stray `.env`) cannot change what a test observes —
@@ -44,3 +48,116 @@ pub fn free_port() -> Option<u16> {
.ok() .ok()
.map(|addr| addr.port()) .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)");
}
+2 -72
View File
@@ -13,7 +13,7 @@
mod common; mod common;
use common::{CLI_ENV_VARS, free_port, soulfind_binary}; use common::{CLI_ENV_VARS, Soulfind, free_port};
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -215,17 +215,7 @@ macro_rules! daemon_or_skip {
match Daemon::start() { match Daemon::start() {
Some(daemon) => daemon, Some(daemon) => daemon,
None => { None => {
let required = std::env::var("SOULSEEK_E2E_REQUIRED") common::no_server("daemon protocol e2e");
.is_ok_and(|v| v != "0" && !v.is_empty());
assert!(
!required,
"SOULSEEK_E2E_REQUIRED is set but no daemon could be \
started (set SOULFIND_BIN)"
);
println!(
"daemon protocol e2e skipped: no soulfind server (set \
SOULFIND_BIN to run)"
);
return; return;
} }
} }
@@ -431,66 +421,6 @@ fn discover_serves_a_contract_whose_parameters_match_what_is_accepted() {
// Fixtures // Fixtures
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
struct Soulfind {
child: Child,
db: PathBuf,
port: u16,
}
impl Soulfind {
fn start() -> Option<Self> {
let port = free_port()?;
let db = std::env::temp_dir().join(format!("soulfind-proto-{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 bin = soulfind_binary()?;
let mut child = Command::new(bin)
.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 {
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
}
fn address(&self) -> String {
format!("127.0.0.1:{}", self.port)
}
fn stop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
fn restart(&mut self) -> Option<()> {
self.stop();
self.child = Self::spawn(self.port, &self.db)?;
Some(())
}
}
impl Drop for Soulfind {
fn drop(&mut self) {
self.stop();
let _ = std::fs::remove_file(&self.db);
}
}
fn wait_until(timeout: Duration, mut f: impl FnMut() -> bool) -> bool { fn wait_until(timeout: Duration, mut f: impl FnMut() -> bool) -> bool {
let deadline = Instant::now() + timeout; let deadline = Instant::now() + timeout;
while Instant::now() < deadline { while Instant::now() < deadline {
+38 -154
View File
@@ -14,17 +14,15 @@
mod common; mod common;
use common::{free_port, soulfind_binary}; use common::{Soulfind, settle};
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::TestBackend; use ratatui::backend::TestBackend;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use soulseek_rs::{Client, ClientSettings, PeerAddress}; use soulseek_rs::Client;
use soulseek_rs_tui::MainTui; use soulseek_rs_tui::MainTui;
use soulseek_rs_tui::models::{FocusedPane, SearchStatus}; use soulseek_rs_tui::models::SearchStatus;
use soulseek_rs_tui::persist::state::StateStore; use soulseek_rs_tui::persist::state::StateStore;
use std::net::TcpStream;
use std::path::Path; use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -32,98 +30,18 @@ use std::time::{Duration, Instant};
/// it out; long enough for a peer on loopback to answer many times over. /// it out; long enough for a peer on loopback to answer many times over.
const SEARCH_WINDOW: Duration = Duration::from_secs(3); const SEARCH_WINDOW: Duration = Duration::from_secs(3);
/// A soulfind spawned for one test, on a port nobody else holds.
struct Soulfind {
child: Child,
db: std::path::PathBuf,
port: u16,
}
impl Soulfind {
fn start() -> Option<Self> {
let bin = soulfind_binary()?;
let port = free_port()?;
let db = std::env::temp_dir().join(format!("soulfind-tui-{port}.db"));
let _ = std::fs::remove_file(&db);
let mut child = Command::new(bin)
.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 {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return Some(Self { child, db, port });
}
std::thread::sleep(Duration::from_millis(100));
}
let _ = child.kill();
let _ = child.wait();
None
}
/// A client logged in to this server as `user`, sharing `shares`.
///
/// The listener stays on: a peer delivers search results over a
/// connection it opens back to the searcher, so a searcher without one
/// hears nothing.
fn client(&self, 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(
"127.0.0.1".to_string(),
self.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
}
}
impl Drop for Soulfind {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_file(&self.db);
}
}
macro_rules! server_or_skip { macro_rules! server_or_skip {
() => { () => {
match Soulfind::start() { match Soulfind::start() {
Some(server) => server, Some(server) => server,
None => { None => {
let required = std::env::var("SOULSEEK_E2E_REQUIRED") common::no_server("tui e2e");
.is_ok_and(|v| v != "0" && !v.is_empty());
assert!(
!required,
"SOULSEEK_E2E_REQUIRED is set but no soulfind server could \
be started (set SOULFIND_BIN)"
);
println!(
"tui e2e skipped: no soulfind server (set SOULFIND_BIN to \
run)"
);
return; return;
} }
} }
}; };
} }
/// Wait out the SetWaitPort registrations so peer lookups resolve.
fn settle() {
std::thread::sleep(Duration::from_secs(1));
}
/// The window under test: the real `MainTui` over a real session, drawn onto /// The window under test: the real `MainTui` over a real session, drawn onto
/// a buffer instead of a terminal. /// a buffer instead of a terminal.
struct Window { struct Window {
@@ -162,23 +80,22 @@ impl Window {
self.terminal.backend().to_string() self.terminal.backend().to_string()
} }
/// Keep the window running until `done` says so, or `timeout` runs out. /// Keep the window running until `done` says so, and hand back the
/// Returns the last screen either way, so a failing test shows what the /// screen it stopped on. Past `timeout` the test fails showing the last
/// user would have been looking at. /// screen, what the user would have been looking at, under `what`.
fn run_until( fn run_until(
&mut self, &mut self,
timeout: Duration, timeout: Duration,
what: &str,
mut done: impl FnMut(&MainTui, &str) -> bool, mut done: impl FnMut(&MainTui, &str) -> bool,
) -> (bool, String) { ) -> String {
let deadline = Instant::now() + timeout; let deadline = Instant::now() + timeout;
loop { loop {
let screen = self.frame(); let screen = self.frame();
if done(&self.tui, &screen) { if done(&self.tui, &screen) {
return (true, screen); return screen;
}
if Instant::now() > deadline {
return (false, screen);
} }
assert!(Instant::now() <= deadline, "{what}:\n{screen}");
std::thread::sleep(Duration::from_millis(100)); std::thread::sleep(Duration::from_millis(100));
} }
} }
@@ -192,10 +109,6 @@ impl Window {
.map(|file| file.filename.clone()) .map(|file| file.filename.clone())
.collect() .collect()
} }
fn search_status(&self, index: usize) -> SearchStatus {
self.tui.state().searches[index].status.clone()
}
} }
/// Whether the file the sharer offers has reached the Results pane, on /// Whether the file the sharer offers has reached the Results pane, on
@@ -208,6 +121,11 @@ fn shows_the_shared_file(tui: &MainTui, screen: &str) -> bool {
&& screen.contains("tui_probe_rerun") && screen.contains("tui_probe_rerun")
} }
/// Whether the one search has run its window out.
fn finished(tui: &MainTui, _: &str) -> bool {
tui.state().searches[0].status == SearchStatus::Completed
}
#[test] #[test]
fn shift_s_runs_a_restored_query_again_and_the_answers_land_on_screen() { fn shift_s_runs_a_restored_query_again_and_the_answers_land_on_screen() {
let server = server_or_skip!(); let server = server_or_skip!();
@@ -215,11 +133,8 @@ fn shift_s_runs_a_restored_query_again_and_the_answers_land_on_screen() {
// The other end of the wire: a peer sharing one file with a name only // The other end of the wire: a peer sharing one file with a name only
// this test searches for. // this test searches for.
let share = tempfile::tempdir().expect("share dir"); let share = tempfile::tempdir().expect("share dir");
std::fs::write( std::fs::write(share.path().join("tui_probe_rerun_vexq.bin"), [7u8; 64])
share.path().join("tui_probe_rerun_vexq.bin"), .expect("share file");
(0..4096u32).map(|i| (i % 251) as u8).collect::<Vec<u8>>(),
)
.expect("share file");
let _sharer = server let _sharer = server
.client("tui_e2e_sharer", vec![share.path().display().to_string()]); .client("tui_e2e_sharer", vec![share.path().display().to_string()]);
settle(); settle();
@@ -240,46 +155,21 @@ fn shift_s_runs_a_restored_query_again_and_the_answers_land_on_screen() {
screen.contains("vexq"), screen.contains("vexq"),
"the old query is listed:\n{screen}" "the old query is listed:\n{screen}"
); );
assert_eq!(window.tui.state().searches.len(), 1);
assert_eq!(window.search_status(0), SearchStatus::Completed);
assert!(
window.result_names().is_empty(),
"and it has nothing under it"
);
assert_eq!(
window.tui.state().focused_pane,
FocusedPane::Searches,
"the window opens on the list, so the query is already highlighted"
);
// One key, no typing. // One key, no typing: the window opens on the list with the query
// highlighted.
window.press(KeyCode::Char('S')); window.press(KeyCode::Char('S'));
window.run_until(
assert_eq!(window.tui.state().searches.len(), 1, "no second row"); Duration::from_secs(15),
assert_eq!(window.search_status(0), SearchStatus::Active); "the sharer's file should reach the screen",
assert_eq!(window.tui.state().focused_pane, FocusedPane::Results); shows_the_shared_file,
let (found, screen) =
window.run_until(Duration::from_secs(15), shows_the_shared_file);
assert!(
found,
"the sharer's file should reach the screen:\n{screen}"
); );
assert!(
window
.result_names()
.iter()
.all(|name| name.contains("tui_probe_rerun")),
"only what the network answered: {:?}",
window.result_names()
);
// The search runs its window out and settles, keeping what it found. // The search runs its window out and settles, keeping what it found.
let (done, screen) = window let screen = window.run_until(
.run_until(SEARCH_WINDOW + Duration::from_secs(5), |tui, _| { SEARCH_WINDOW + Duration::from_secs(5),
tui.state().searches[0].status == SearchStatus::Completed "the search should finish",
}); finished,
assert!(done, "the search should finish:\n{screen}"); );
assert!(shows_the_shared_file(&window.tui, &screen)); assert!(shows_the_shared_file(&window.tui, &screen));
let first_run = window.result_names(); let first_run = window.result_names();
@@ -287,23 +177,17 @@ fn shift_s_runs_a_restored_query_again_and_the_answers_land_on_screen() {
// the same peer has to answer the same searcher a second time, and the // the same peer has to answer the same searcher a second time, and the
// window has to start the display over rather than stack the answers. // window has to start the display over rather than stack the answers.
window.press(KeyCode::Char('1')); window.press(KeyCode::Char('1'));
assert_eq!(window.tui.state().focused_pane, FocusedPane::Searches);
window.press(KeyCode::Char('S')); window.press(KeyCode::Char('S'));
assert_eq!(window.search_status(0), SearchStatus::Active); window.run_until(
assert!( Duration::from_secs(15),
window.result_names().is_empty(), "the second run should be answered too",
"the display starts over: {:?}", shows_the_shared_file,
window.result_names() );
window.run_until(
SEARCH_WINDOW + Duration::from_secs(5),
"the second run should finish",
finished,
); );
let (found, screen) =
window.run_until(Duration::from_secs(15), shows_the_shared_file);
assert!(found, "the second run should be answered too:\n{screen}");
let (done, _) = window
.run_until(SEARCH_WINDOW + Duration::from_secs(5), |tui, _| {
tui.state().searches[0].status == SearchStatus::Completed
});
assert!(done);
assert_eq!( assert_eq!(
window.result_names(), window.result_names(),
first_run, first_run,