Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send local games states notifications on start (do not rely on whether Galaxy call get_local_games) #47

Merged
merged 6 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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