diff --git a/src/local_client_base.py b/src/local_client_base.py index 88b9945..7073986 100644 --- a/src/local_client_base.py +++ b/src/local_client_base.py @@ -52,9 +52,6 @@ def __init__(self, update_statuses): self.uninstaller = None self.installed_games_cache = self.get_installed_games() - loop = asyncio.get_event_loop() - loop.create_task(self._register_local_data_watcher()) - loop.create_task(self._register_classic_games_updater()) self.classic_games_parsing_task = None @abc.abstractproperty @@ -207,7 +204,7 @@ def _load_local_files(self): raise e return True - async def _register_local_data_watcher(self): + async def register_local_data_watcher(self): parse_local_data_event = asyncio.Event() FileWatcher(self.CONFIG_PATH, parse_local_data_event, interval=1) FileWatcher(self.PRODUCT_DB_PATH, parse_local_data_event, interval=2.5) @@ -228,7 +225,7 @@ async def _register_local_data_watcher(self): self.installed_games_cache = refreshed_games parse_local_data_event.clear() - async def _register_classic_games_updater(self): + async def register_classic_games_updater(self): tick_count = 0 while True: tick_count += 1 diff --git a/src/local_games.py b/src/local_games.py index 130c9fa..30fb4c4 100644 --- a/src/local_games.py +++ b/src/local_games.py @@ -33,7 +33,6 @@ def __init__(self, info: BlizzardGame, uninstall_tag: str, version: str, last_pl @property def has_galaxy_installed_state(self) -> bool: """Indicates when Play button should be available in Galaxy""" - return self.playable or self.installed def add_process(self, process: Process): diff --git a/src/plugin.py b/src/plugin.py index 458c314..487035d 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -39,11 +39,12 @@ def __init__(self, reader, writer, token): self.backend_client = BackendClient(self, self.authentication_client) self.watched_running_games = set() - self.local_games_called = False + + def handshake_complete(self): + self.create_task(self.local_client.register_local_data_watcher(), 'local data watcher') + self.create_task(self.local_client.register_classic_games_updater(), 'classic games updater') async def _notify_about_game_stop(self, game, starting_timeout): - if not self.local_games_called: - return id_to_watch = game.info.uid if id_to_watch in self.watched_running_games: @@ -62,8 +63,6 @@ async def _notify_about_game_stop(self, game, starting_timeout): self.watched_running_games.remove(id_to_watch) def _update_statuses(self, refreshed_games, previous_games): - if not self.local_games_called: - return for blizz_id, refr in refreshed_games.items(): prev = previous_games.get(blizz_id, None) @@ -73,14 +72,14 @@ def _update_statuses(self, refreshed_games, previous_games): state = LocalGameState.Installed else: log.debug('Detected not-fully installed game') - state = LocalGameState.None_ + continue elif refr.has_galaxy_installed_state and not prev.has_galaxy_installed_state: log.debug('Detected playable game') state = LocalGameState.Installed elif refr.last_played != prev.last_played: log.debug('Detected launched game') state = LocalGameState.Installed | LocalGameState.Running - asyncio.create_task(self._notify_about_game_stop(refr, 5)) + self.create_task(self._notify_about_game_stop(refr, 5), 'game stop waiter') else: continue @@ -174,9 +173,6 @@ async def uninstall_game(self, game_id): log.exception(f'Uninstalling game {game_id} failed: {e}') async def launch_game(self, game_id): - if not self.local_games_called: - await self.get_local_games() - try: game = self.local_client.get_installed_games().get(game_id, None) if game is None: @@ -349,9 +345,6 @@ async def get_local_games(self): log.exception(f"failed to get local games: {str(e)}") raise - finally: - self.local_games_called = True - async def get_game_time(self, game_id, context): total_time = None last_played_time = None @@ -373,7 +366,7 @@ async def get_game_time(self, game_id, context): async def _get_overwatch_time(self) -> Union[None, int]: log.debug("Fetching playtime for Overwatch...") player_data = await self.backend_client.get_ow_player_data() - if 'message' in player_data: # user not found... unfortunately no 404 status code is returned :/ + if 'message' in player_data: # user not found log.error('No Overwatch profile found.') return None if player_data['private'] == True: diff --git a/tests/test_local_games.py b/tests/test_local_games.py index f791bb4..24412f8 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -1,9 +1,12 @@ +from unittest.mock import Mock, patch, call +from typing import NamedTuple, Optional + import pytest from galaxy.api.types import LocalGame from galaxy.api.consts import LocalGameState -from src.local_games import InstalledGame +from local_games import InstalledGame from definitions import Blizzard @@ -17,12 +20,162 @@ async def test_local_games_states(plugin_mock, installed, playable, expected_state): plugin_mock.local_client.get_running_games.return_value = set() plugin_mock.local_client.get_installed_games.return_value = { - "test_game_id_1": InstalledGame(Blizzard['s1'], 's1', '1.0', '', '/path/', playable, installed), + "s1": InstalledGame(Blizzard['s1'], 's1', '1.0', '', '/path/', playable, installed), } - expected_local_games = [ - LocalGame("test_game_id_1", expected_state) + LocalGame("s1", expected_state) ] result = await plugin_mock.get_local_games() assert result == expected_local_games + + +class BlizzardGameState(NamedTuple): + playable: bool + installed: bool + + +@pytest.mark.parametrize("prev, refr, new_state", [ + pytest.param( + BlizzardGameState(True, True), + BlizzardGameState(True, True), + None, + id="no change; game playable" + ), + pytest.param( + None, + BlizzardGameState(False, False), + None, + id="game installation began or first iteration after plugin start" + ), + pytest.param( + None, + BlizzardGameState(True, False), + LocalGameState.Installed, + id="first iteration after plugin start: game is playable" + ), + pytest.param( + None, + BlizzardGameState(False, True), + LocalGameState.Installed, + id="first iteration after plugin start: game not playable but installed (eg. update pending)" + ), + pytest.param( + BlizzardGameState(False, False), + BlizzardGameState(True, True), + LocalGameState.Installed, + id="game installation finished" + ), + pytest.param( + BlizzardGameState(False, False), + BlizzardGameState(True, False), + LocalGameState.Installed, + id="game became playable during installation" + ), + pytest.param( + BlizzardGameState(True, False), + BlizzardGameState(True, True), + None, + id="playable game fully installed" + ), + pytest.param( + BlizzardGameState(True, True), + BlizzardGameState(False, True), + None, + id="game update appeared" + ), + pytest.param( + BlizzardGameState(False, True), + BlizzardGameState(True, True), + None, + id="game update finished" + ), + pytest.param( + BlizzardGameState(True, True), + None, + LocalGameState.None_, + id="game uninstalled" + ), +]) +def test_local_game_installation_state_notification( + plugin_mock, + prev: Optional[BlizzardGameState], + refr: Optional[BlizzardGameState], + new_state: Optional[LocalGameState] +): + + def installed_game(playable: bool, installed: bool): + with patch('local_games.pathfinder'): + last_played = '' # to not affect the logic for running game detection + return InstalledGame( + Mock(Blizzard), Mock(str), Mock(str), last_played, Mock(str), + playable=playable, installed=installed + ) + + plugin_mock.update_local_game_status = Mock() + GAME_ID = 's1' + previous_games = {GAME_ID: installed_game(prev.playable, prev.installed)} if prev else {} + refreshed_games = {GAME_ID: installed_game(refr.playable, refr.installed)} if refr else {} + + plugin_mock._update_statuses(refreshed_games, previous_games) + + if new_state is None: + plugin_mock.update_local_game_status.assert_not_called() + else: + local_game = LocalGame(GAME_ID, new_state) + plugin_mock.update_local_game_status.assert_called_once_with(local_game) + + +@pytest.mark.parametrize("prev_last_played, refr_last_played, new_state", [ + pytest.param('', '', None, id="game never played on this machine"), + pytest.param('', '123123123', LocalGameState.Running | LocalGameState.Installed, id="game started for the first time on this machine"), + pytest.param('11111111', '12222222', LocalGameState.Running | LocalGameState.Installed, id="game started"), +]) +def test_local_game_running_state_notification( + plugin_mock, + prev_last_played: str, + refr_last_played: str, + new_state: Optional[LocalGameState] +): + # patch side-effects + plugin_mock._notify_about_game_stop = Mock() + plugin_mock.create_task = Mock() + + plugin_mock.update_local_game_status = Mock() + GAME_ID = 's1' + previous_games = {GAME_ID: Mock(InstalledGame, has_galaxy_installed_state=True, last_played=prev_last_played)} + refreshed_games = {GAME_ID: Mock(InstalledGame, has_galaxy_installed_state=True, last_played=refr_last_played)} + + plugin_mock._update_statuses(refreshed_games, previous_games) + + if new_state is None: + plugin_mock.update_local_game_status.assert_not_called() + else: + local_game = LocalGame(GAME_ID, new_state) + plugin_mock.update_local_game_status.assert_called_once_with(local_game) + + +def test_local_game_notification_multiple_games(plugin_mock): + # patch side-effects + plugin_mock._notify_about_game_stop = Mock() + plugin_mock.create_task = Mock() + + plugin_mock.update_local_game_status = Mock() + previous_games = { + 'hs_beta': Mock(InstalledGame, has_galaxy_installed_state=True, last_played=''), + 's2': Mock(InstalledGame, has_galaxy_installed_state=True, last_played=''), + } + refreshed_games = { + 's1': Mock(InstalledGame, has_galaxy_installed_state=True, last_played=''), + 's2': Mock(InstalledGame, has_galaxy_installed_state=True, last_played='12313111'), + } + + plugin_mock._update_statuses(refreshed_games, previous_games) + + plugin_mock.update_local_game_status.assert_has_calls([ + call(LocalGame('hs_beta', LocalGameState.None_)), + call(LocalGame('s1', LocalGameState.Installed)), + call(LocalGame('s2', LocalGameState.Installed | LocalGameState.Running)), + ], + any_order=True + ) diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index 25c3a36..5608533 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -1,6 +1,4 @@ -import asyncio import pytest -import json from galaxy.api.types import Game, LicenseInfo from galaxy.api.consts import LicenseType