diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 1af8442..c77002f 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -89,16 +89,9 @@ class Plugin: ) self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"]) - self._register_method( - "import_unlocked_achievements", - self.get_unlocked_achievements, - result_name="unlocked_achievements" - ) + self._register_method("start_achievements_import", self._start_achievements_import) self._detect_feature(Feature.ImportAchievements, ["get_unlocked_achievements"]) - self._register_method("start_achievements_import", self.start_achievements_import) - self._detect_feature(Feature.ImportAchievements, ["import_games_achievements"]) - self._register_method("import_local_games", self.get_local_games, result_name="local_games") self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"]) @@ -117,11 +110,8 @@ class Plugin: self._register_method("import_friends", self.get_friends, result_name="friend_info_list") self._detect_feature(Feature.ImportFriends, ["get_friends"]) - self._register_method("import_game_times", self.get_game_times, result_name="game_times") - self._detect_feature(Feature.ImportGameTime, ["get_game_times"]) - - self._register_method("start_game_times_import", self.start_game_times_import) - self._detect_feature(Feature.ImportGameTime, ["import_game_times"]) + self._register_method("start_game_times_import", self._start_game_times_import) + self._detect_feature(Feature.ImportGameTime, ["get_game_time"]) @property def features(self) -> List[Feature]: @@ -316,26 +306,14 @@ class Plugin: } self._notification_client.notify("achievement_unlocked", params) - def game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None: - """Notify the client that import of achievements for a given game has succeeded. - This method is called by import_games_achievements. - - :param game_id: id of the game for which the achievements were imported - :param achievements: list of imported achievements - """ + def _game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None: 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: str, error: ApplicationError) -> None: - """Notify the client that import of achievements for a given game has failed. - This method is called by import_games_achievements. - - :param game_id: id of the game for which the achievements import failed - :param error: error which prevented the achievements import - """ + def _game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None: params = { "game_id": game_id, "error": { @@ -345,9 +323,7 @@ class Plugin: } self._notification_client.notify("game_achievements_import_failure", params) - def achievements_import_finished(self) -> None: - """Notify the client that importing achievements has finished. - This method is called by import_games_achievements_task""" + def _achievements_import_finished(self) -> None: self._notification_client.notify("achievements_import_finished", None) def update_local_game_status(self, local_game: LocalGame) -> None: @@ -400,22 +376,11 @@ class Plugin: params = {"game_time": game_time} self._notification_client.notify("game_time_updated", params) - def game_time_import_success(self, game_time: GameTime) -> None: - """Notify the client that import of a given game_time has succeeded. - This method is called by import_game_times. - - :param game_time: game_time which was imported - """ + def _game_time_import_success(self, game_time: GameTime) -> None: params = {"game_time": game_time} self._notification_client.notify("game_time_import_success", params) - def game_time_import_failure(self, game_id: str, error: ApplicationError) -> None: - """Notify the client that import of a game time for a given game has failed. - This method is called by import_game_times. - - :param game_id: id of the game for which the game time could not be imported - :param error: error which prevented the game time import - """ + def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> None: params = { "game_id": game_id, "error": { @@ -425,10 +390,7 @@ class Plugin: } self._notification_client.notify("game_time_import_failure", params) - def game_times_import_finished(self) -> None: - """Notify the client that importing game times has finished. - This method is called by :meth:`~.import_game_times_task`. - """ + def _game_times_import_finished(self) -> None: self._notification_client.notify("game_times_import_finished", None) def lost_authentication(self) -> None: @@ -557,51 +519,51 @@ class Plugin: """ raise NotImplementedError() - async def get_unlocked_achievements(self, game_id: str) -> List[Achievement]: - """ - .. deprecated:: 0.33 - Use :meth:`~.import_games_achievements`. - """ - raise NotImplementedError() - - async def start_achievements_import(self, game_ids: List[str]) -> None: - """Starts the task of importing achievements. - This method is called by the GOG Galaxy Client. - - :param game_ids: ids of the games for which the achievements are imported - """ + async def _start_achievements_import(self, game_ids: List[str]) -> None: if self._achievements_import_in_progress: raise ImportInProgress() - async def import_games_achievements_task(game_ids): + context = await self.prepare_achievements_context(game_ids) + + async def import_game_achievements(game_id, context_): try: - await self.import_games_achievements(game_ids) + achievements = await self.get_unlocked_achievements(game_id, context_) + self._game_achievements_import_success(game_id, achievements) + except ApplicationError as error: + self._game_achievements_import_failure(game_id, error) + except Exception: + logging.exception("Unexpected exception raised in import_game_achievements") + self._game_achievements_import_failure(game_id, UnknownError()) + + async def import_games_achievements(game_ids_, context_): + try: + imports = [import_game_achievements(game_id, context_) for game_id in game_ids_] + await asyncio.gather(*imports) finally: - self.achievements_import_finished() + self._achievements_import_finished() self._achievements_import_in_progress = False - asyncio.create_task(import_games_achievements_task(game_ids)) + self.create_task(import_games_achievements(game_ids, context), "Games unlocked achievements import") self._achievements_import_in_progress = True - async def import_games_achievements(self, game_ids: List[str]) -> None: + async def prepare_achievements_context(self, game_ids: List[str]) -> None: + """Override this method to prepare context for get_unlocked_achievements. + + This allows for optimizations like batch requests to platform API. + Default implementation returns None. """ - Override this method to return the unlocked achievements - of the user that is currently logged in to the plugin. - Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list. - This method is called by the GOG Galaxy Client. + return None - :param game_ids: ids of the games for which to import unlocked achievements + async def get_unlocked_achievements(self, game_id: str, context: Any) -> List[Achievement]: + """Override this method to return list of unlocked achievements + for the game identified by the provided game_id. + This method is called by import task initialized by GOG Galaxy Client. + + :param game_id: + :param context: Value return from :meth:`prepare_achievements_context` + :return: """ - - 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) - - imports = [import_game_achievements(game_id) for game_id in game_ids] - await asyncio.gather(*imports) + raise NotImplementedError() async def get_local_games(self) -> List[LocalGame]: """Override this method to return the list of @@ -704,54 +666,50 @@ class Plugin: """ raise NotImplementedError() - async def get_game_times(self) -> List[GameTime]: - """ - .. deprecated:: 0.33 - Use :meth:`~.import_game_times`. - """ - raise NotImplementedError() - - async def start_game_times_import(self, game_ids: List[str]) -> None: - """Starts the task of importing game times - This method is called by the GOG Galaxy Client. - - :param game_ids: ids of the games for which the game time is imported - """ + async def _start_game_times_import(self, game_ids: List[str]) -> None: if self._game_times_import_in_progress: raise ImportInProgress() - async def import_game_times_task(game_ids): + context = await self.prepare_game_times_context(game_ids) + + async def import_game_time(game_id, context_): try: - await self.import_game_times(game_ids) + game_time = await self.get_game_time(game_id, context_) + self._game_time_import_success(game_time) + except ApplicationError as error: + self._game_time_import_failure(game_id, error) + except Exception: + logging.exception("Unexpected exception raised in import_game_time") + self._game_time_import_failure(game_id, UnknownError()) + + async def import_game_times(game_ids_, context_): + try: + imports = [import_game_time(game_id, context_) for game_id in game_ids_] + await asyncio.gather(*imports) finally: - self.game_times_import_finished() + self._game_times_import_finished() self._game_times_import_in_progress = False - asyncio.create_task(import_game_times_task(game_ids)) + self.create_task(import_game_times(game_ids, context), "Game times import") self._game_times_import_in_progress = True - async def import_game_times(self, game_ids: List[str]) -> None: + async def prepare_game_times_context(self, game_ids: List[str]) -> None: + """Override this method to prepare context for get_game_time. + This allows for optimizations like batch requests to platform API. + Default implementation returns None. """ - Override this method to return game times for - games owned by the currently authenticated user. - Call game_time_import_success/game_time_import_failure for each game_id on the list. - This method is called by GOG Galaxy Client. + return None - :param game_ids: ids of the games for which the game time is imported + async def get_game_time(self, game_id: str, context: Any) -> GameTime: + """Override this method to return the game time for the game + identified by the provided game_id. + This method is called by import task initialized by GOG Galaxy Client. + + :param game_id: + :param context: Value return from :meth:`prepare_game_times_context` + :return: """ - 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) + raise NotImplementedError() def create_and_run_plugin(plugin_class, argv): diff --git a/src/galaxy/unittest/mock.py b/src/galaxy/unittest/mock.py index 264c3fa..a3051cc 100644 --- a/src/galaxy/unittest/mock.py +++ b/src/galaxy/unittest/mock.py @@ -1,12 +1,24 @@ -from asyncio import coroutine +import asyncio from unittest.mock import MagicMock + class AsyncMock(MagicMock): async def __call__(self, *args, **kwargs): return super(AsyncMock, self).__call__(*args, **kwargs) + def coroutine_mock(): coro = MagicMock(name="CoroutineResult") - corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro)) + corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro)) corofunc.coro = coro - return corofunc \ No newline at end of file + return corofunc + + +async def skip_loop(iterations=1): + for _ in range(iterations): + await asyncio.sleep(0) + + +async def async_return_value(return_value, loop_iterations_delay=0): + await skip_loop(loop_iterations_delay) + return return_value diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..2174b66 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,18 @@ +import json + + +def create_message(request): + return json.dumps(request).encode() + b"\n" + + +def get_messages(write_mock): + messages = [] + print("call_args_list", write_mock.call_args_list) + for call_args in write_mock.call_args_list: + print("call_args", call_args) + data = call_args[0][0] + print("data", data) + for line in data.splitlines(): + message = json.loads(line) + messages.append(message) + return messages diff --git a/tests/conftest.py b/tests/conftest.py index 9cd4b9a..b819f8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform -from galaxy.unittest.mock import AsyncMock, coroutine_mock +from galaxy.unittest.mock import AsyncMock, coroutine_mock, skip_loop @pytest.fixture() def reader(): @@ -15,11 +15,12 @@ def reader(): yield stream @pytest.fixture() -def writer(): +async def writer(): stream = MagicMock(name="stream_writer") stream.write = MagicMock() - stream.drain = AsyncMock() + stream.drain = AsyncMock(return_value=None) yield stream + await skip_loop(1) # drain @pytest.fixture() def read(reader): @@ -36,13 +37,15 @@ def plugin(reader, writer): "handshake_complete", "authenticate", "get_owned_games", + "prepare_achievements_context", "get_unlocked_achievements", "get_local_games", "launch_game", "install_game", "uninstall_game", "get_friends", - "get_game_times", + "get_game_time", + "prepare_game_times_context", "shutdown_platform_client" ) diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 9a6ec30..311554d 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -1,94 +1,210 @@ -import asyncio import json -from unittest.mock import call +from unittest.mock import MagicMock import pytest from pytest import raises from galaxy.api.types import Achievement -from galaxy.api.errors import UnknownError, ImportInProgress, BackendError +from galaxy.api.errors import BackendError +from galaxy.unittest.mock import async_return_value + +from tests import create_message, get_messages + def test_initialization_no_unlock_time(): with raises(Exception): Achievement(achievement_id="lvl30", achievement_name="Got level 30") + def test_initialization_no_id_nor_name(): with raises(AssertionError): Achievement(unlock_time=1234567890) -def test_success(plugin, read, write): + +# TODO replace AsyncMocks with MagicMocks in conftest and use async_return_value +@pytest.fixture() +def reader(): + stream = MagicMock(name="stream_reader") + stream.read = MagicMock() + yield stream + + +@pytest.mark.asyncio +async def test_get_unlocked_achievements_success(plugin, read, write): + plugin.prepare_achievements_context.coro.return_value = 5 request = { "jsonrpc": "2.0", "id": "3", - "method": "import_unlocked_achievements", + "method": "start_achievements_import", "params": { - "game_id": "14" + "game_ids": ["14"] } } - read.side_effect = [json.dumps(request).encode() + b"\n", b""] + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] plugin.get_unlocked_achievements.coro.return_value = [ Achievement(achievement_id="lvl10", unlock_time=1548421241), Achievement(achievement_name="Got level 20", unlock_time=1548422395), Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633) ] - asyncio.run(plugin.run()) - plugin.get_unlocked_achievements.assert_called_with(game_id="14") - response = json.loads(write.call_args[0][0]) + await plugin.run() + plugin.prepare_achievements_context.assert_called_with(["14"]) + plugin.get_unlocked_achievements.assert_called_with("14", 5) - assert response == { - "jsonrpc": "2.0", - "id": "3", - "result": { - "unlocked_achievements": [ - { - "achievement_id": "lvl10", - "unlock_time": 1548421241 - }, - { - "achievement_name": "Got level 20", - "unlock_time": 1548422395 - }, - { - "achievement_id": "lvl30", - "achievement_name": "Got level 30", - "unlock_time": 1548495633 - } - ] + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_achievements_import_success", + "params": { + "game_id": "14", + "unlocked_achievements": [ + { + "achievement_id": "lvl10", + "unlock_time": 1548421241 + }, + { + "achievement_name": "Got level 20", + "unlock_time": 1548422395 + }, + { + "achievement_id": "lvl30", + "achievement_name": "Got level 30", + "unlock_time": 1548495633 + } + ] + } + }, + { + "jsonrpc": "2.0", + "method": "achievements_import_finished", + "params": None } - } + ] -def test_failure(plugin, read, write): + +@pytest.mark.asyncio +@pytest.mark.parametrize("exception,code,message", [ + (BackendError, 4, "Backend error"), + (KeyError, 0, "Unknown error") +]) +async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", - "method": "import_unlocked_achievements", + "method": "start_achievements_import", "params": { - "game_id": "14" + "game_ids": ["14"] } } - read.side_effect = [json.dumps(request).encode() + b"\n", b""] - plugin.get_unlocked_achievements.coro.side_effect = UnknownError() - asyncio.run(plugin.run()) + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_unlocked_achievements.coro.side_effect = exception + await plugin.run() plugin.get_unlocked_achievements.assert_called() - response = json.loads(write.call_args[0][0]) - assert response == { + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_achievements_import_failure", + "params": { + "game_id": "14", + "error": { + "code": code, + "message": message + } + } + }, + { + "jsonrpc": "2.0", + "method": "achievements_import_finished", + "params": None + } + ] + +@pytest.mark.asyncio +async def test_prepare_get_unlocked_achievements_context_error(plugin, read, write): + plugin.prepare_achievements_context.coro.side_effect = BackendError() + request = { "jsonrpc": "2.0", "id": "3", - "error": { - "code": 0, - "message": "Unknown error" + "method": "start_achievements_import", + "params": { + "game_ids": ["14"] } } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")] + await plugin.run() -def test_unlock_achievement(plugin, write): + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": 4, + "message": "Backend error" + } + } + ] + + +@pytest.mark.asyncio +async def test_import_in_progress(plugin, read, write): + requests = [ + { + "jsonrpc": "2.0", + "id": "3", + "method": "start_achievements_import", + "params": { + "game_ids": ["14"] + } + }, + { + "jsonrpc": "2.0", + "id": "4", + "method": "start_achievements_import", + "params": { + "game_ids": ["15"] + } + } + ] + read.side_effect = [ + async_return_value(create_message(requests[0])), + async_return_value(create_message(requests[1])), + async_return_value(b"") + ] + + await plugin.run() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + ] + + +@pytest.mark.asyncio +async def test_unlock_achievement(plugin, write): achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395) - - async def couritine(): - plugin.unlock_achievement("14", achievement) - - asyncio.run(couritine()) + plugin.unlock_achievement("14", achievement) response = json.loads(write.call_args[0][0]) assert response == { @@ -102,92 +218,3 @@ 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_features.py b/tests/test_features.py index 1b518db..47e561d 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -39,11 +39,11 @@ def test_multi_features(): async def get_owned_games(self): pass - async def import_games_achievements(self, game_ids) -> None: + async def get_unlocked_achievements(self, game_id, context): pass - async def start_game_times_import(self, game_ids) -> None: + async def get_game_time(self, game_id, context): pass plugin = PluginImpl(Platform.Generic, "0.1", None, None, None) - assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames} + assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime} diff --git a/tests/test_game_times.py b/tests/test_game_times.py index a9f11f7..f17c614 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -1,71 +1,205 @@ import asyncio import json -from unittest.mock import call +from unittest.mock import MagicMock, call import pytest from galaxy.api.types import GameTime -from galaxy.api.errors import UnknownError, ImportInProgress, BackendError +from galaxy.api.errors import BackendError +from galaxy.unittest.mock import async_return_value -def test_success(plugin, read, write): +from tests import create_message, get_messages + +# TODO replace AsyncMocks with MagicMocks in conftest and use async_return_value +@pytest.fixture() +def reader(): + stream = MagicMock(name="stream_reader") + stream.read = MagicMock() + yield stream + + +@pytest.mark.asyncio +async def test_get_game_time_success(plugin, read, write): + plugin.prepare_game_times_context.coro.return_value = "abc" request = { "jsonrpc": "2.0", "id": "3", - "method": "import_game_times" + "method": "start_game_times_import", + "params": { + "game_ids": ["3", "5", "7"] + } } - - read.side_effect = [json.dumps(request).encode() + b"\n", b""] - plugin.get_game_times.coro.return_value = [ + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_game_time.coro.side_effect = [ GameTime("3", 60, 1549550504), GameTime("5", 10, None), GameTime("7", None, 1549550502), ] - asyncio.run(plugin.run()) - plugin.get_game_times.assert_called_with() - response = json.loads(write.call_args[0][0]) + await plugin.run() + plugin.get_game_time.assert_has_calls([ + call("3", "abc"), + call("5", "abc"), + call("7", "abc"), + ]) - assert response == { - "jsonrpc": "2.0", - "id": "3", - "result": { - "game_times": [ - { + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_time_import_success", + "params": { + "game_time": { "game_id": "3", - "time_played": 60, - "last_played_time": 1549550504 - }, - { - "game_id": "5", - "time_played": 10, - }, - { - "game_id": "7", - "last_played_time": 1549550502 + "last_played_time": 1549550504, + "time_played": 60 } - ] + } + }, + { + "jsonrpc": "2.0", + "method": "game_time_import_success", + "params": { + "game_time": { + "game_id": "5", + "time_played": 10 + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_time_import_success", + "params": { + "game_time": { + "game_id": "7", + "last_played_time": 1549550502 + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_times_import_finished", + "params": None } - } + ] -def test_failure(plugin, read, write): + +@pytest.mark.asyncio +@pytest.mark.parametrize("exception,code,message", [ + (BackendError, 4, "Backend error"), + (KeyError, 0, "Unknown error") +]) +async def test_get_game_time_error(exception, code, message, plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", - "method": "import_game_times" - } - - read.side_effect = [json.dumps(request).encode() + b"\n", b""] - plugin.get_game_times.coro.side_effect = UnknownError() - asyncio.run(plugin.run()) - plugin.get_game_times.assert_called_with() - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "id": "3", - "error": { - "code": 0, - "message": "Unknown error", + "method": "start_game_times_import", + "params": { + "game_ids": ["6"] } } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_game_time.coro.side_effect = exception + await plugin.run() + plugin.get_game_time.assert_called() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_time_import_failure", + "params": { + "game_id": "6", + "error": { + "code": code, + "message": message + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_times_import_finished", + "params": None + } + ] + + +@pytest.mark.asyncio +async def test_prepare_get_game_time_context_error(plugin, read, write): + plugin.prepare_game_times_context.coro.side_effect = BackendError() + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_times_import", + "params": { + "game_ids": ["6"] + } + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")] + await plugin.run() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": 4, + "message": "Backend error" + } + } + ] + + +@pytest.mark.asyncio +async def test_import_in_progress(plugin, read, write): + requests = [ + { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_times_import", + "params": { + "game_ids": ["6"] + } + }, + { + "jsonrpc": "2.0", + "id": "4", + "method": "start_game_times_import", + "params": { + "game_ids": ["7"] + } + } + ] + read.side_effect = [ + async_return_value(create_message(requests[0])), + async_return_value(create_message(requests[1])), + async_return_value(b"") + ] + + await plugin.run() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + ] + def test_update_game(plugin, write): game_time = GameTime("3", 60, 1549550504) @@ -87,93 +221,3 @@ 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()