Skip to content

Commit

Permalink
Send local games states notifications on start (do not rely on whethe…
Browse files Browse the repository at this point in the history
…r Galaxy call get_local_games) (#47)

* remove local_games_called boolean

* wait with watchers init until handshake_complete

* add tests

* adjust plugin.py

* add multiple games update status test

* make tests easier to read
  • Loading branch information
bartok765 authored Jan 29, 2021
1 parent e02b1a7 commit 1a1c1d8
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 26 deletions.
7 changes: 2 additions & 5 deletions src/local_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/local_games.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
21 changes: 7 additions & 14 deletions src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
161 changes: 157 additions & 4 deletions tests/test_local_games.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
)
2 changes: 0 additions & 2 deletions tests/test_owned_games.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import asyncio
import pytest
import json

from galaxy.api.types import Game, LicenseInfo
from galaxy.api.consts import LicenseType
Expand Down

0 comments on commit 1a1c1d8

Please sign in to comment.