diff --git a/requirements.txt b/requirements.txt index 49e755d..528cc4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -e . pytest==4.2.0 +pytest-asyncio==0.10.0 +pytest-mock==1.10.3 pytest-flakes==4.0.0 # because of pip bug https://github.com/pypa/pip/issues/4780 aiohttp==3.5.4 diff --git a/src/galaxy/api/errors.py b/src/galaxy/api/errors.py index 3e1921d..189db00 100644 --- a/src/galaxy/api/errors.py +++ b/src/galaxy/api/errors.py @@ -77,3 +77,7 @@ class IncoherentLastMessage(ApplicationError): class MessageNotFound(ApplicationError): def __init__(self, data=None): super().__init__(500, "Message not found", data) + +class ImportInProgress(ApplicationError): + def __init__(self, data=None): + super().__init__(600, "Import already in progress", data) \ No newline at end of file diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index c46b26f..07290d7 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -12,6 +12,9 @@ class JsonRpcError(Exception): self.data = data super().__init__() + def __eq__(self, other): + return self.code == other.code and self.message == other.message and self.data == other.data + class ParseError(JsonRpcError): def __init__(self): super().__init__(-32700, "Parse error") diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index b10105f..1059f35 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -9,6 +9,7 @@ import sys from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.consts import Feature +from galaxy.api.errors import UnknownError, ImportInProgress class JSONEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=method-hidden @@ -41,6 +42,9 @@ class Plugin(): self._shutdown() self._server.register_eof(eof_handler) + self._achievements_import_in_progress = False + self._game_times_import_in_progress = False + # internal self._register_method("shutdown", self._shutdown, internal=True) self._register_method("get_capabilities", self._get_capabilities, internal=True) @@ -230,6 +234,26 @@ class Plugin(): } self._notification_client.notify("achievement_unlocked", params) + def game_achievements_import_success(self, game_id, achievements): + params = { + "game_id": game_id, + "unlocked_achievements": achievements + } + self._notification_client.notify("game_achievements_import_success", params) + + def game_achievements_import_failure(self, game_id, error): + params = { + "game_id": game_id, + "error": { + "code": error.code, + "message": error.message + } + } + self._notification_client.notify("game_achievements_import_failure", params) + + def achievements_import_finished(self): + self._notification_client.notify("achievements_import_finished", None) + def update_local_game_status(self, local_game): params = {"local_game" : local_game} self._notification_client.notify("local_game_status_changed", params) @@ -254,6 +278,23 @@ class Plugin(): params = {"game_time" : game_time} self._notification_client.notify("game_time_updated", params) + def game_time_import_success(self, game_time): + params = {"game_time" : game_time} + self._notification_client.notify("game_time_import_success", params) + + def game_time_import_failure(self, game_id, error): + params = { + "game_id": game_id, + "error": { + "code": error.code, + "message": error.message + } + } + self._notification_client.notify("game_time_import_failure", params) + + def game_times_import_finished(self): + self._notification_client.notify("game_times_import_finished", None) + def lost_authentication(self): self._notification_client.notify("authentication_lost", None) @@ -287,6 +328,28 @@ class Plugin(): async def get_unlocked_achievements(self, game_id): raise NotImplementedError() + async def start_achievements_import(self, game_ids): + if self._achievements_import_in_progress: + raise ImportInProgress() + + async def import_games_achievements(game_ids): + async def import_game_achievements(game_id): + try: + achievements = await self.get_unlocked_achievements(game_id) + self.game_achievements_import_success(game_id, achievements) + except Exception as error: + self.game_achievements_import_failure(game_id, error) + + try: + imports = [import_game_achievements(game_id) for game_id in game_ids] + await asyncio.gather(*imports) + finally: + self.achievements_import_finished() + self._achievements_import_in_progress = False + + asyncio.create_task(import_games_achievements(game_ids)) + self._achievements_import_in_progress = True + async def get_local_games(self): raise NotImplementedError() @@ -323,6 +386,32 @@ class Plugin(): async def get_game_times(self): raise NotImplementedError() + async def start_game_times_import(self, game_ids): + if self._game_times_import_in_progress: + raise ImportInProgress() + + async def import_game_times(game_ids): + try: + game_times = await self.get_game_times() + game_ids_set = set(game_ids) + for game_time in game_times: + if game_time.game_id not in game_ids_set: + continue + self.game_time_import_success(game_time) + game_ids_set.discard(game_time.game_id) + for game_id in game_ids_set: + self.game_time_import_failure(game_id, UnknownError()) + + except Exception as error: + for game_id in game_ids: + self.game_time_import_failure(game_id, error) + finally: + self.game_times_import_finished() + self._game_times_import_in_progress = False + + asyncio.create_task(import_game_times(game_ids)) + self._game_times_import_in_progress = True + def create_and_run_plugin(plugin_class, argv): if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 3926662..84421bd 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -1,9 +1,12 @@ import asyncio import json +from unittest.mock import call + +import pytest from pytest import raises from galaxy.api.types import Achievement -from galaxy.api.errors import UnknownError +from galaxy.api.errors import UnknownError, ImportInProgress, BackendError def test_initialization_no_unlock_time(): with raises(Exception): @@ -99,3 +102,92 @@ def test_unlock_achievement(plugin, write): } } } + +@pytest.mark.asyncio +async def test_game_achievements_import_success(plugin, write): + achievements = [ + Achievement(achievement_id="lvl10", unlock_time=1548421241), + Achievement(achievement_name="Got level 20", unlock_time=1548422395) + ] + plugin.game_achievements_import_success("134", achievements) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_achievements_import_success", + "params": { + "game_id": "134", + "unlocked_achievements": [ + { + "achievement_id": "lvl10", + "unlock_time": 1548421241 + }, + { + "achievement_name": "Got level 20", + "unlock_time": 1548422395 + } + ] + } + } + +@pytest.mark.asyncio +async def test_game_achievements_import_failure(plugin, write): + plugin.game_achievements_import_failure("134", ImportInProgress()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_achievements_import_failure", + "params": { + "game_id": "134", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + } + +@pytest.mark.asyncio +async def test_achievements_import_finished(plugin, write): + plugin.achievements_import_finished() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "achievements_import_finished", + "params": None + } + +@pytest.mark.asyncio +async def test_start_achievements_import(plugin, write, mocker): + game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success") + game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure") + achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished") + + game_ids = ["1", "5", "9"] + error = BackendError() + achievements = [ + Achievement(achievement_id="lvl10", unlock_time=1548421241), + Achievement(achievement_name="Got level 20", unlock_time=1548422395) + ] + plugin.get_unlocked_achievements.coro.side_effect = [ + achievements, + [], + error + ] + await plugin.start_achievements_import(game_ids) + + with pytest.raises(ImportInProgress): + await plugin.start_achievements_import(["4", "8"]) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")]) + game_achievements_import_success.assert_has_calls([ + call("1", achievements), + call("5", []) + ]) + game_achievements_import_failure.assert_called_once_with("9", error) + achievements_import_finished.assert_called_once_with() diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 38f3853..9ad7220 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -1,8 +1,10 @@ import asyncio import json +from unittest.mock import call +import pytest from galaxy.api.types import GameTime -from galaxy.api.errors import UnknownError +from galaxy.api.errors import UnknownError, ImportInProgress, BackendError def test_success(plugin, readline, write): request = { @@ -81,3 +83,93 @@ def test_update_game(plugin, write): } } } + +@pytest.mark.asyncio +async def test_game_time_import_success(plugin, write): + plugin.game_time_import_success(GameTime("3", 60, 1549550504)) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_import_success", + "params": { + "game_time": { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + } + } + } + +@pytest.mark.asyncio +async def test_game_time_import_failure(plugin, write): + plugin.game_time_import_failure("134", ImportInProgress()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_import_failure", + "params": { + "game_id": "134", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + } + +@pytest.mark.asyncio +async def test_game_times_import_finished(plugin, write): + plugin.game_times_import_finished() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_times_import_finished", + "params": None + } + +@pytest.mark.asyncio +async def test_start_game_times_import(plugin, write, mocker): + game_time_import_success = mocker.patch.object(plugin, "game_time_import_success") + game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure") + game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished") + + game_ids = ["1", "5"] + game_time = GameTime("1", 10, 1549550502) + plugin.get_game_times.coro.return_value = [ + game_time + ] + await plugin.start_game_times_import(game_ids) + + with pytest.raises(ImportInProgress): + await plugin.start_game_times_import(["4", "8"]) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_game_times.coro.assert_called_once_with() + game_time_import_success.assert_called_once_with(game_time) + game_time_import_failure.assert_called_once_with("5", UnknownError()) + game_times_import_finished.assert_called_once_with() + +@pytest.mark.asyncio +async def test_start_game_times_import_failure(plugin, write, mocker): + game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure") + game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished") + + game_ids = ["1", "5"] + error = BackendError() + plugin.get_game_times.coro.side_effect = error + + await plugin.start_game_times_import(game_ids) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_game_times.coro.assert_called_once_with() + + assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)] + game_times_import_finished.assert_called_once_with()