From 8178395016ac77000efeba7f8a0c5324cefffa2c Mon Sep 17 00:00:00 2001 From: Askaholic Date: Fri, 19 Jun 2020 18:22:58 -0800 Subject: [PATCH] Wait for all players to accept match before starting game --- server/config.py | 5 +- server/ladder_service/ladder_service.py | 57 ++++- server/lobbyconnection.py | 6 +- server/matchmaker/matchmaker_queue.py | 2 +- server/matchmaker/search.py | 3 + tests/integration_tests/test_game.py | 58 +++-- tests/integration_tests/test_matchmaker.py | 34 ++- .../test_matchmaker_violations.py | 218 +++++++++++++++++- .../integration_tests/test_teammatchmaker.py | 25 +- tests/unit_tests/test_ladder_service.py | 7 + 10 files changed, 383 insertions(+), 32 deletions(-) diff --git a/server/config.py b/server/config.py index ffa9ff93d..d3df2694b 100644 --- a/server/config.py +++ b/server/config.py @@ -146,7 +146,7 @@ def __init__(self): # The method for choosing map pool rating # Can be "mean", "min", or "max" self.MAP_POOL_RATING_SELECTION = "mean" - # The maximum amount of time in seconds to wait between pops + # The maximum amount of time (in seconds) to wait between pops. self.QUEUE_POP_TIME_MAX = 90 # The number of possible matches we would like to have when the queue # pops. The queue pop time will be adjusted based on the current rate of @@ -154,6 +154,9 @@ def __init__(self): self.QUEUE_POP_DESIRED_MATCHES = 2.5 # How many previous queue sizes to consider self.QUEUE_POP_TIME_MOVING_AVG_SIZE = 5 + # Amount of time (in seconds) that players have to accept a match + # before it will time out. + self.MATCH_OFFER_TIME = 20 self._defaults = { key: value for key, value in vars(self).items() if key.isupper() diff --git a/server/ladder_service/ladder_service.py b/server/ladder_service/ladder_service.py index 3f8767bdf..8506ff3f2 100644 --- a/server/ladder_service/ladder_service.py +++ b/server/ladder_service/ladder_service.py @@ -6,8 +6,10 @@ import random import re import statistics +import weakref from collections import defaultdict -from typing import Awaitable, Callable, Optional +from datetime import timedelta +from typing import Awaitable, Callable, Iterable, Optional import aiocron import humanize @@ -43,11 +45,14 @@ from server.matchmaker import ( MapPool, MatchmakerQueue, + MatchOffer, + OfferTimeoutError, OnMatchedCallback, Search ) from server.metrics import MatchLaunch from server.players import Player, PlayerState +from server.timing import datetime_now from server.types import GameLaunchOptions, Map, NeroxisGeneratedMap @@ -71,6 +76,7 @@ def __init__( self.violation_service = violation_service self._searches: dict[Player, dict[str, Search]] = defaultdict(dict) + self._match_offers = weakref.WeakValueDictionary() self._allow_new_searches = True async def initialize(self) -> None: @@ -462,13 +468,60 @@ def on_match_found( self._clear_search(player, queue.name) - asyncio.create_task(self.start_game(s1.players, s2.players, queue)) + asyncio.create_task(self.confirm_match(s1, s2, queue)) except Exception: self._logger.exception( "Error processing match between searches %s, and %s", s1, s2 ) + async def confirm_match( + self, + s1: Search, + s2: Search, + queue: MatchmakerQueue + ): + try: + all_players = s1.players + s2.players + + offer = self.create_match_offer(all_players) + offer.write_broadcast_update() + await offer.wait_ready() + except OfferTimeoutError: + unready_players = list(offer.get_unready_players()) + assert unready_players, "Unready players should be non-empty if offer timed out" + + self._logger.info( + "Match failed to start. Some players did not ready up in time: %s", + [player.login for player in unready_players] + ) + # TODO: Refactor duplication + msg = {"command": "match_cancelled"} + for player in all_players: + if player.state == PlayerState.STARTING_AUTOMATCH: + player.state = PlayerState.IDLE + player.write_message(msg) + + # TODO: Unmatch and return to queue + + self.violation_service.register_violations(unready_players) + + await self.start_game(s1.players, s2.players, queue) + + def create_match_offer(self, players: Iterable[Player]): + offer = MatchOffer( + players, + datetime_now() + timedelta(seconds=config.MATCH_OFFER_TIME) + ) + for player in players: + self._match_offers[player] = offer + return offer + + def ready_player(self, player: Player): + offer = self._match_offers.get(player) + if offer is not None: + offer.ready_player(player) + def start_game( self, team1: list[Player], diff --git a/server/lobbyconnection.py b/server/lobbyconnection.py index 9c227b315..7a96d3dbe 100644 --- a/server/lobbyconnection.py +++ b/server/lobbyconnection.py @@ -951,11 +951,7 @@ async def command_game_host(self, message): await self.launch_game(game, is_host=True) async def command_match_ready(self, message): - """ - Replace with full implementation when implemented in client, see: - https://github.com/FAForever/downlords-faf-client/issues/1783 - """ - pass + self.ladder_service.ready_player(self.player) async def launch_game( self, diff --git a/server/matchmaker/matchmaker_queue.py b/server/matchmaker/matchmaker_queue.py index 089dc6412..a69613dfa 100644 --- a/server/matchmaker/matchmaker_queue.py +++ b/server/matchmaker/matchmaker_queue.py @@ -175,7 +175,7 @@ async def find_matches(self) -> None: proposed_matches, unmatched_searches = await loop.run_in_executor( None, self.matchmaker.find, - searches, + (search for search in searches if not search.done()), self.team_size, self.rating_peak, ) diff --git a/server/matchmaker/search.py b/server/matchmaker/search.py index eb0229943..e1adbe669 100644 --- a/server/matchmaker/search.py +++ b/server/matchmaker/search.py @@ -226,6 +226,9 @@ def match(self, other: "Search"): ) self._match.set_result(other) + def unmatch(self): + self._match = asyncio.Future() + async def await_match(self): """ Wait for this search to complete diff --git a/tests/integration_tests/test_game.py b/tests/integration_tests/test_game.py index 9df858378..cbfe1cd2d 100644 --- a/tests/integration_tests/test_game.py +++ b/tests/integration_tests/test_game.py @@ -155,6 +155,35 @@ async def start_search(proto, queue_name="ladder1v1"): ) +async def accept_match(proto, timeout=5): + await read_until_command(proto, "match_info", timeout=timeout) + await proto.send_message({"command": "match_ready"}) + return await read_until_command( + proto, + "match_info", + ready=True, + timeout=10 + ) + + +async def read_until_match(proto, timeout=30): + # If the players did not match, this will fail due to a timeout error + msg = await read_until_command(proto, "match_found", timeout=timeout) + # Accept match + match_info = await read_until_command(proto, "match_info", timeout=timeout) + await proto.send_message({"command": "match_ready"}) + # Wait for all players to be ready. This could be the same as the message + # notifying us that our match ready was received so we can't use + # `accept_match` here. + await read_until_command( + proto, + "match_info", + players_ready=match_info["players_total"], + timeout=10 + ) + return msg + + async def queue_player_for_matchmaking( user, lobby_server, @@ -182,10 +211,6 @@ async def queue_players_for_matchmaking( queue_name ) - # If the players did not match, this will fail due to a timeout error - await read_until_command(proto1, "match_found", timeout=30) - await read_until_command(proto2, "match_found") - return player1_id, proto1, player2_id, proto2 @@ -203,18 +228,12 @@ async def queue_temp_players_for_matchmaking( tmp_user(queue_name) for _ in range(num_players) ]) - protos = await asyncio.gather(*[ + responses = await asyncio.gather(*[ queue_player_for_matchmaking(user, lobby_server, queue_name) for user in users ]) - # If the players did not match, this will fail due to a timeout error - await asyncio.gather(*[ - read_until_command(proto, "match_found", timeout=30) - for _, proto in protos - ]) - - return protos + return (proto for _, proto in responses) async def get_player_ratings(proto, *names, rating_type="global"): @@ -674,9 +693,11 @@ async def test_ladder_game_draw_bug(lobby_server, database): instead of a draw. """ player1_id, proto1, player2_id, proto2 = await queue_players_for_matchmaking(lobby_server) + protos = (proto1, proto2) + await asyncio.gather(*[read_until_match(proto) for proto in protos]) msg1, msg2 = await asyncio.gather(*[ - client_response(proto) for proto in (proto1, proto2) + client_response(proto) for proto in protos ]) game_id = msg1["uid"] army1 = msg1["map_position"] @@ -690,7 +711,7 @@ async def test_ladder_game_draw_bug(lobby_server, database): (player_id, "Faction", msg["faction"]), (player_id, "Color", msg["map_position"]), ) - for proto in (proto1, proto2): + for proto in protos: await proto.send_message({ "target": "game", "command": "GameState", @@ -707,14 +728,14 @@ async def test_ladder_game_draw_bug(lobby_server, database): [army1, "score 1"], [army2, "defeat -10"] ): - for proto in (proto1, proto2): + for proto in protos: await proto.send_message({ "target": "game", "command": "GameResult", "args": result }) - for proto in (proto1, proto2): + for proto in protos: await proto.send_message({ "target": "game", "command": "GameEnded", @@ -757,7 +778,7 @@ async def test_ladder_game_draw_bug(lobby_server, database): assert row.score == 0 -@fast_forward(15) +@fast_forward(70) async def test_ladder_game_not_joinable(lobby_server): """ We should not be able to join AUTO_LOBBY games using the `game_join` command. @@ -765,7 +786,8 @@ async def test_ladder_game_not_joinable(lobby_server): _, _, test_proto = await connect_and_sign_in( ("test", "test_password"), lobby_server ) - _, proto1, _, _ = await queue_players_for_matchmaking(lobby_server) + _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) await read_until_command(test_proto, "game_info") msg = await read_until_command(proto1, "game_launch") diff --git a/tests/integration_tests/test_matchmaker.py b/tests/integration_tests/test_matchmaker.py index 835584146..0f74d42d2 100644 --- a/tests/integration_tests/test_matchmaker.py +++ b/tests/integration_tests/test_matchmaker.py @@ -23,6 +23,7 @@ queue_players_for_matchmaking, queue_temp_players_for_matchmaking, read_until_launched, + read_until_match, send_player_options ) @@ -31,6 +32,7 @@ async def test_game_launch_message(lobby_server): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) msg1 = await read_until_command(proto1, "game_launch") await open_fa(proto1) msg2 = await read_until_command(proto2, "game_launch") @@ -67,6 +69,7 @@ async def test_game_launch_message_map_generator(lobby_server): queue_name="neroxis1v1" ) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) msg1 = await read_until_command(proto1, "game_launch") await open_fa(proto1) msg2 = await read_until_command(proto2, "game_launch") @@ -87,6 +90,7 @@ async def test_game_launch_message_game_options(lobby_server, tmp_user): queue_name="gameoptions" ) + await asyncio.gather(*[read_until_match(proto) for proto in protos]) msgs = await asyncio.gather(*[ client_response(proto) for _, proto in protos ]) @@ -103,6 +107,7 @@ async def test_game_launch_message_game_options(lobby_server, tmp_user): async def test_game_matchmaking_start(lobby_server, database): host_id, host, guest_id, guest = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (host, guest)]) # The player that queued last will be the host msg = await read_until_command(host, "game_launch") game_id = msg["uid"] @@ -166,8 +171,9 @@ async def test_game_matchmaking_start(lobby_server, database): @fast_forward(15) async def test_game_matchmaking_start_while_matched(lobby_server): - _, proto1, _, _ = await queue_players_for_matchmaking(lobby_server) + _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) # Trying to queue again after match was found should generate an error await proto1.send_message({ "command": "game_matchmaking", @@ -182,6 +188,7 @@ async def test_game_matchmaking_start_while_matched(lobby_server): async def test_game_matchmaking_timeout(lobby_server, game_service): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) msg1 = await idle_response(proto1, timeout=120) await read_until_command(proto2, "match_cancelled", timeout=120) await read_until_command(proto1, "match_cancelled", timeout=120) @@ -229,6 +236,7 @@ async def test_game_matchmaking_timeout(lobby_server, game_service): async def test_game_matchmaking_timeout_guest(lobby_server, game_service): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) msg1, msg2 = await asyncio.gather( client_response(proto1), client_response(proto2) @@ -305,10 +313,12 @@ async def test_game_matchmaking_cancel(lobby_server): await read_until_command(proto, "search_info", timeout=5) -@fast_forward(50) +@fast_forward(120) async def test_game_matchmaking_disconnect(lobby_server): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + # One player disconnects before the game has launched + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) await proto1.close() msg = await read_until_command(proto2, "match_cancelled", timeout=120) @@ -316,12 +326,26 @@ async def test_game_matchmaking_disconnect(lobby_server): assert msg == {"command": "match_cancelled", "game_id": 41956} +@fast_forward(120) +async def test_game_matchmaking_disconnect_no_accept(lobby_server): + _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) + + # One player disconnects before the game has launched + await read_until_command(proto1, "match_found", timeout=30) + await proto1.close() + + msg = await read_until_command(proto2, "match_cancelled", timeout=120) + + assert msg == {"command": "match_cancelled"} + + @pytest.mark.flaky @fast_forward(130) async def test_game_matchmaking_close_fa_and_requeue(lobby_server): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) - _, _ = await asyncio.gather( + await asyncio.gather(*[read_until_match(proto) for proto in (proto1, proto2)]) + await asyncio.gather( client_response(proto1), client_response(proto2) ) @@ -365,6 +389,10 @@ async def test_anti_map_repetition(lobby_server): for _ in range(20): ret = await queue_players_for_matchmaking(lobby_server) player1_id, proto1, player2_id, proto2 = ret + await asyncio.gather( + read_until_match(proto1), + read_until_match(proto2) + ) msg1, msg2 = await asyncio.gather( client_response(proto1), client_response(proto2) diff --git a/tests/integration_tests/test_matchmaker_violations.py b/tests/integration_tests/test_matchmaker_violations.py index 542df8452..24ac64aa1 100644 --- a/tests/integration_tests/test_matchmaker_violations.py +++ b/tests/integration_tests/test_matchmaker_violations.py @@ -4,7 +4,13 @@ from tests.utils import fast_forward from .conftest import connect_and_sign_in, read_until_command -from .test_game import open_fa, queue_players_for_matchmaking, start_search +from .test_game import ( + accept_match, + open_fa, + queue_players_for_matchmaking, + read_until_match, + start_search +) from .test_parties import accept_party_invite, invite_to_party @@ -18,6 +24,10 @@ async def test_violation_for_guest_timeout(mocker, lobby_server): # The player that queued last will be the host async def launch_game_and_timeout_guest(): + await asyncio.gather(*[ + read_until_match(proto) + for proto in (host, guest) + ]) await read_until_command(host, "game_launch") await open_fa(host) await read_until_command(host, "game_info") @@ -33,11 +43,25 @@ async def launch_game_and_timeout_guest(): await launch_game_and_timeout_guest() + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 1, + "time": "2022-02-05T00:00:00+00:00" + } + # Second time searching there is no ban await start_search(host) await start_search(guest) await launch_game_and_timeout_guest() + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 2, + "time": "2022-02-05T00:00:00+00:00" + } + # Third time searching there is a short ban await guest.send_message({ "command": "game_matchmaking", @@ -110,6 +134,180 @@ async def launch_game_and_timeout_guest(): } +@fast_forward(360) +async def test_violation_for_not_accepting_match(mocker, lobby_server): + mock_now = mocker.patch( + "server.ladder_service.violation_service.datetime_now", + return_value=datetime(2022, 2, 5, tzinfo=timezone.utc) + ) + host_id, host, guest_id, guest = await queue_players_for_matchmaking(lobby_server) + + await read_until_command(host, "match_cancelled", timeout=120) + await read_until_command(guest, "match_cancelled", timeout=10) + + # Both players should have received a violation + msg = await read_until_command(host, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 1, + "time": "2022-02-05T00:00:00+00:00" + } + + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 1, + "time": "2022-02-05T00:00:00+00:00" + } + + # Second time searching there is no ban + await start_search(host) + await start_search(guest) + await read_until_command(host, "match_cancelled", timeout=120) + await read_until_command(guest, "match_cancelled", timeout=10) + + # Both players should have received another violation + msg = await read_until_command(host, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 2, + "time": "2022-02-05T00:00:00+00:00" + } + + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 2, + "time": "2022-02-05T00:00:00+00:00" + } + + # Third time searching there is a short ban + for proto in (host, guest): + await proto.send_message({ + "command": "game_matchmaking", + "state": "start", + "queue_name": "ladder1v1" + }) + + # Both players should have been banned + msg = await read_until_command(host, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": host_id, + "expires_at": "2022-02-05T00:10:00+00:00" + }] + } + msg = await read_until_command(guest, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": guest_id, + "expires_at": "2022-02-05T00:10:00+00:00" + }] + } + + mock_now.return_value = datetime(2022, 2, 5, 0, 10, tzinfo=timezone.utc) + await asyncio.sleep(1) + + # Third successful search + await start_search(host) + await start_search(guest) + await read_until_command(host, "match_cancelled", timeout=120) + await read_until_command(guest, "match_cancelled", timeout=10) + + # Both players should have received another violation + msg = await read_until_command(host, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 3, + "time": "2022-02-05T00:10:00+00:00" + } + + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 3, + "time": "2022-02-05T00:10:00+00:00" + } + + # Fourth time searching there is a long ban + for proto in (host, guest): + await proto.send_message({ + "command": "game_matchmaking", + "state": "start", + "queue_name": "ladder1v1" + }) + + # Both players should have been banned + msg = await read_until_command(host, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": host_id, + "expires_at": "2022-02-05T00:40:00+00:00" + }] + } + msg = await read_until_command(guest, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": guest_id, + "expires_at": "2022-02-05T00:40:00+00:00" + }] + } + + mock_now.return_value = datetime(2022, 2, 5, 0, 40, tzinfo=timezone.utc) + await asyncio.sleep(1) + + # Fourth successful search + await start_search(host) + await start_search(guest) + await read_until_command(host, "match_cancelled", timeout=120) + await read_until_command(guest, "match_cancelled", timeout=10) + + # Both players should have received another violation + msg = await read_until_command(host, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 4, + "time": "2022-02-05T00:40:00+00:00" + } + + msg = await read_until_command(guest, "search_violation", timeout=10) + assert msg == { + "command": "search_violation", + "count": 4, + "time": "2022-02-05T00:40:00+00:00" + } + + # Fifth time searching there is a long ban + for proto in (host, guest): + await proto.send_message({ + "command": "game_matchmaking", + "state": "start", + "queue_name": "ladder1v1" + }) + + # Both players should have been banned + msg = await read_until_command(host, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": host_id, + "expires_at": "2022-02-05T01:10:00+00:00" + }] + } + msg = await read_until_command(guest, "search_timeout") + assert msg == { + "command": "search_timeout", + "timeouts": [{ + "player": guest_id, + "expires_at": "2022-02-05T01:10:00+00:00" + }] + } + + @fast_forward(360) async def test_violation_persisted_across_logins(mocker, lobby_server): mocker.patch( @@ -117,13 +315,22 @@ async def test_violation_persisted_across_logins(mocker, lobby_server): return_value=datetime(2022, 2, 5, tzinfo=timezone.utc) ) host_id, host, _, guest = await queue_players_for_matchmaking(lobby_server) + protos = (host, guest) + await asyncio.gather(*[ + accept_match(proto, timeout=30) + for proto in protos + ]) await read_until_command(host, "match_cancelled", timeout=120) await read_until_command(guest, "match_cancelled", timeout=10) # Second time searching there is no ban await start_search(host) await start_search(guest) + await asyncio.gather(*[ + accept_match(proto, timeout=30) + for proto in protos + ]) await read_until_command(host, "match_cancelled", timeout=120) await read_until_command(guest, "match_cancelled", timeout=10) @@ -174,13 +381,22 @@ async def test_violation_persisted_across_parties(mocker, lobby_server): return_value=datetime(2022, 2, 5, tzinfo=timezone.utc) ) host_id, host, guest_id, guest = await queue_players_for_matchmaking(lobby_server) + protos = (host, guest) + await asyncio.gather(*[ + accept_match(proto, timeout=30) + for proto in protos + ]) await read_until_command(host, "match_cancelled", timeout=120) await read_until_command(guest, "match_cancelled", timeout=10) # Second time searching there is no ban await start_search(host) await start_search(guest) + await asyncio.gather(*[ + accept_match(proto, timeout=30) + for proto in protos + ]) await read_until_command(host, "match_cancelled", timeout=120) await read_until_command(guest, "match_cancelled", timeout=10) diff --git a/tests/integration_tests/test_teammatchmaker.py b/tests/integration_tests/test_teammatchmaker.py index 50c1e7368..c99601c2e 100644 --- a/tests/integration_tests/test_teammatchmaker.py +++ b/tests/integration_tests/test_teammatchmaker.py @@ -13,6 +13,7 @@ from .conftest import connect_and_sign_in, read_until, read_until_command from .test_game import ( + accept_match, client_response, get_player_ratings, idle_response, @@ -62,11 +63,19 @@ async def queue_players_for_matchmaking(lobby_server): read_until_command(proto, "match_found", timeout=30) for proto in protos ]) + await asyncio.gather(*[ + proto.send_message({ + "command": "match_ready", + }) + for proto in protos + ]) + return protos, ids async def matchmaking_client_response(proto): await read_until_command(proto, "match_found", timeout=30) + await accept_match(proto) return await client_response(proto) @@ -148,6 +157,7 @@ async def test_game_matchmaking_multiqueue(lobby_server): }) for proto in protos ]) + await read_until_command(protos[0], "match_found", timeout=30) msg = await read_until_command( protos[0], "search_info", @@ -155,7 +165,19 @@ async def test_game_matchmaking_multiqueue(lobby_server): ) assert msg["state"] == "stop" - msgs = await asyncio.gather(*[client_response(proto) for proto in protos]) + # Since `match_found` comes before `search_info` we can't use + # `matchmaking_client_response` for the first player. + async def _accept_and_respond(proto): + await accept_match(proto) + return await client_response(proto) + + msgs = await asyncio.gather( + _accept_and_respond(protos[0]), + *[ + matchmaking_client_response(proto) + for proto in protos[1:] + ] + ) uid = set(msg["uid"] for msg in msgs) assert len(uid) == 1 @@ -353,6 +375,7 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server): ) assert msg["state"] == "stop" + await asyncio.gather(*[accept_match(proto) for proto in protos]) await client_response(protos[0]) await idle_response(protos[1]) diff --git a/tests/unit_tests/test_ladder_service.py b/tests/unit_tests/test_ladder_service.py index c2393a818..fe3aacee5 100644 --- a/tests/unit_tests/test_ladder_service.py +++ b/tests/unit_tests/test_ladder_service.py @@ -828,6 +828,13 @@ async def test_start_game_called_on_match( search1 = ladder_service._searches[p1]["ladder1v1"] await search1.await_match() + # Wait for offer to be created + await asyncio.sleep(1) + + ladder_service.ready_player(p1) + ladder_service.ready_player(p2) + + await asyncio.sleep(1) ladder_service.write_rating_progress.assert_called() ladder_service.start_game.assert_called_once()