test(e2e): fence on the server where a test slept for another client

Tests that need one client's request to have landed before another
client acts slept for it: a join before a room message, ticker or room
search, a watcher's global-room join and leave, a guest accepting
invitations, a room created before an outsider asks for it, share
counts and interests before they are queried, a stored message before
its recipient logs in, and an acknowledgement before the session ends.
Several of those requests get no reply to wait on.

soulfind takes each connection's messages in order, so a fence asks it
about a user over the connection that has to be settled and waits for
the answer, which only arrives once everything sent before it has been
handled. Where the event itself is visible - a member list, alice's own
ticker - the tests wait on that instead.
This commit is contained in:
Michel de Graaf
2026-09-10 15:18:22 +02:00
parent 2a343dd4b9
commit a6d4c8b3df
+52 -19
View File
@@ -2955,9 +2955,9 @@ fn a_joined_room_reports_its_members_statistics() {
assert!(sharer.login().expect("sharer login"));
sharer.join_room(room).expect("sharer joins room");
// Let the server register the sharer's membership and share counts, so
// the observer's own join reply describes a room that already has them.
std::thread::sleep(Duration::from_millis(750));
// The sharer's membership and share counts have to land first, so the
// observer's own join reply describes a room that already has them.
fence(&sharer, "e2e_stats_observer");
let mut observer =
Client::with_settings(server.settings("e2e_stats_observer", "pw"));
@@ -3038,9 +3038,9 @@ fn watching_a_user_returns_their_status_and_share_counts() {
watcher.connect().expect("watcher connect");
assert!(watcher.login().expect("watcher login"));
// Let the subject's share counts reach the server before watching, so
// the reply describes a user it already knows the statistics for.
std::thread::sleep(Duration::from_millis(750));
// The subject's share counts have to reach the server before watching,
// so the reply describes a user it already knows the statistics for.
fence(&subject, "e2e_watch_watcher");
watcher
.watch_user("e2e_watch_subject")
@@ -3814,6 +3814,18 @@ fn await_room_event<T>(
None
}
/// Wait until the server has handled everything `client` has sent, including
/// requests it never answers: soulfind takes a connection's messages in order,
/// so the answer to a question about `other` asked now comes after them.
fn fence(client: &Client, other: &str) {
client.request_user_info(other).expect("ask about a user");
let deadline = Instant::now() + Duration::from_secs(5);
while client.user_info(other).is_none() {
assert!(Instant::now() < deadline, "the server should answer");
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
fn a_ticker_set_in_a_room_reaches_the_other_members() {
use soulseek_rs::types::RoomEvent;
@@ -3830,7 +3842,12 @@ fn a_ticker_set_in_a_room_reaches_the_other_members() {
alice.join_room(room).expect("alice joins");
bob.join_room(room).expect("bob joins");
std::thread::sleep(Duration::from_millis(500));
// A ticker reaches only who is in the room when it is set.
let deadline = Instant::now() + Duration::from_secs(5);
while bob.room_members(room).is_empty() {
assert!(Instant::now() < deadline, "bob should get into the room");
std::thread::sleep(Duration::from_millis(50));
}
let _ = bob.take_room_events();
alice
@@ -3874,11 +3891,15 @@ fn a_client_joining_later_receives_the_whole_ticker_board() {
alice.connect().expect("alice connect");
assert!(alice.login().expect("alice login"));
alice.join_room(room).expect("alice joins");
std::thread::sleep(Duration::from_millis(300));
alice
.set_room_ticker(room, "standing message")
.expect("set ticker");
std::thread::sleep(Duration::from_millis(500));
// The room, alice included, is told once the board holds it.
await_room_event(&alice, Duration::from_secs(5), |event| {
matches!(event, RoomEvent::TickerAdded { room: r, .. } if r == room)
.then_some(())
})
.expect("alice should see her ticker go up");
// Carol joins afterwards and must be handed the board that already exists.
let mut carol =
@@ -3919,7 +3940,7 @@ fn the_global_room_feed_carries_a_room_we_never_joined() {
watcher.join_global_room().expect("join global room");
talker.join_room(room).expect("talker joins a room");
std::thread::sleep(Duration::from_millis(500));
fence(&watcher, "e2e_global_talk");
let _ = watcher.take_room_events();
let body = "spoken where nobody is watching";
@@ -3943,7 +3964,7 @@ fn the_global_room_feed_carries_a_room_we_never_joined() {
// And leaving the feed stops it: the next message must not arrive.
watcher.leave_global_room().expect("leave global room");
std::thread::sleep(Duration::from_millis(500));
fence(&watcher, "e2e_global_talk");
let _ = watcher.take_room_events();
talker
.say_in_room(room, "after leaving")
@@ -4009,7 +4030,16 @@ fn a_room_search_is_answered_by_the_rooms_members() {
);
sharer.join_room(room).expect("sharer joins");
searcher.join_room(room).expect("searcher joins");
std::thread::sleep(Duration::from_millis(750));
// A room search goes to whoever is in the room when it is sent.
let deadline = Instant::now() + Duration::from_secs(5);
while !searcher
.room_members(room)
.iter()
.any(|member| member == "e2e_rs_sharer")
{
assert!(Instant::now() < deadline, "both should get into the room");
std::thread::sleep(Duration::from_millis(50));
}
let query = "relic";
searcher.search_room(room, query).expect("room search");
@@ -4046,7 +4076,7 @@ fn shared_interests_make_two_users_similar() {
alice
.add_dislike("e2e_interest_muzak")
.expect("alice dislikes something");
std::thread::sleep(Duration::from_millis(500));
fence(&bob, "e2e_like_alice");
// Who else likes this item (code 112).
let deadline = Instant::now() + Duration::from_secs(5);
@@ -4140,7 +4170,7 @@ fn recommendations_are_returned_for_our_interests() {
alice.add_interest(shared).expect("alice likes shared");
bob.add_interest(shared).expect("bob likes shared");
bob.add_interest(other).expect("bob likes other");
std::thread::sleep(Duration::from_millis(500));
fence(&bob, "e2e_rec_alice");
let deadline = Instant::now() + Duration::from_secs(8);
let mut recommended = Vec::new();
@@ -4635,7 +4665,8 @@ fn a_private_room_is_owned_granted_and_revoked() {
owner
.join_private_room(room)
.expect("owner creates the room");
std::thread::sleep(Duration::from_millis(750));
// An invite reaches only a user already accepting invitations.
fence(&guest, "e2e_priv_owner");
// The guest is invited, and hears about it (code 139).
owner
@@ -4754,7 +4785,7 @@ fn a_private_room_someone_else_owns_cannot_be_taken() {
owner
.join_private_room(room)
.expect("owner creates the room");
std::thread::sleep(Duration::from_millis(750));
fence(&owner, "e2e_taken_outsider");
// An outsider asking for the same name must be refused (code 1003), not
// handed the room.
@@ -4807,7 +4838,7 @@ fn an_acknowledged_offline_message_is_not_delivered_twice() {
sender
.send_private_message("e2e_ack_recipient", body)
.expect("send to an offline user");
std::thread::sleep(Duration::from_millis(500));
fence(&sender, "e2e_ack_recipient");
let received_once = |label: &str| -> bool {
let mut client =
@@ -4825,7 +4856,9 @@ fn an_acknowledged_offline_message_is_not_delivered_twice() {
std::thread::sleep(Duration::from_millis(100));
}
}
std::thread::sleep(Duration::from_millis(500));
// The acknowledgement goes out ahead of the message it answers, so
// once the server has handled what followed, it has the ack too.
fence(&client, "e2e_ack_sender");
got
};
@@ -5379,7 +5412,7 @@ fn a_member_can_give_up_a_private_room_and_an_owner_can_disband_it() {
owner
.join_private_room(room)
.expect("owner creates the room");
std::thread::sleep(Duration::from_millis(750));
fence(&guest, "e2e_leave_owner");
owner
.add_room_member(room, "e2e_leave_guest")
.expect("owner invites the guest");