From 617dbdfee70ab1690be34e15f3929d207e4ce253 Mon Sep 17 00:00:00 2001 From: Mieszko Banczerowski Date: Tue, 28 Jan 2020 10:35:54 +0100 Subject: [PATCH] GPI-1109 Implement get_game_size --- .gitignore | 1 + src/galaxy/api/consts.py | 1 + src/galaxy/api/plugin.py | 62 ++++++++++++- tests/conftest.py | 3 + tests/test_features.py | 3 +- tests/test_local_size.py | 188 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 tests/test_local_size.py diff --git a/.gitignore b/.gitignore index 449f335..15d5b74 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Pipfile .idea docs/source/_build .mypy_cache +.pytest_cache diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index 8881868..76a89b1 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -114,6 +114,7 @@ class Feature(Enum): ImportGameLibrarySettings = "ImportGameLibrarySettings" ImportOSCompatibility = "ImportOSCompatibility" ImportUserPresence = "ImportUserPresence" + ImportLocalSize = "ImportLocalSize" class LicenseType(Enum): diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index e940540..ed9cc88 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -166,6 +166,16 @@ class Plugin: self._user_presence_import_finished, self.user_presence_import_complete ) + self._local_size_importer = Importer( + self._external_task_manager, + "local size", + self.get_local_size, + self.prepare_local_size_context, + self._local_size_import_success, + self._local_size_import_failure, + self._local_size_import_finished, + self.local_size_import_complete + ) # internal self._register_method("shutdown", self._shutdown, internal=True) @@ -233,6 +243,9 @@ class Plugin: self._register_method("start_user_presence_import", self._start_user_presence_import) self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"]) + self._register_method("start_local_size_import", self._start_local_size_import) + self._detect_feature(Feature.ImportLocalSize, ["get_local_size"]) + async def __aenter__(self): return self @@ -613,6 +626,27 @@ class Plugin: def _user_presence_import_finished(self) -> None: self._connection.send_notification("user_presence_import_finished", None) + def _local_size_import_success(self, game_id: str, size: Optional[int]) -> None: + self._connection.send_notification( + "local_size_import_success", + { + "game_id": game_id, + "local_size": size + } + ) + + def _local_size_import_failure(self, game_id: str, error: ApplicationError) -> None: + self._connection.send_notification( + "local_size_import_failure", + { + "game_id": game_id, + "error": error.json() + } + ) + + def _local_size_import_finished(self) -> None: + self._connection.send_notification("local_size_import_finished", None) + def lost_authentication(self) -> None: """Notify the client that integration has lost authentication for the current user and is unable to perform actions which would require it. @@ -966,7 +1000,7 @@ class Plugin: await self._user_presence_importer.start(user_id_list) async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any: - """Override this method to prepare context for get_user_presence. + """Override this method to prepare context for :meth:`get_user_presence`. This allows for optimizations like batch requests to platform API. Default implementation returns None. @@ -988,6 +1022,32 @@ class Plugin: def user_presence_import_complete(self) -> None: """Override this method to handle operations after presence import is finished (like updating cache).""" + async def _start_local_size_import(self, game_ids: List[str]) -> None: + await self._local_size_importer.start(game_ids) + + async def prepare_local_size_context(self, game_ids: List[str]) -> Any: + """Override this method to prepare context for :meth:`get_local_size` + Default implementation returns None. + + :param game_ids: the ids of the games for which information about size is imported + :return: context + """ + return None + + async def get_local_size(self, game_id: str, context: Any) -> int: + """Override this method to return installed game size in bytes. + + .. note:: + It is preferable to use more efficient way of game size retrieval than iterating over all local files. + + :param context: the value returned from :meth:`prepare_local_size_context` + :return: game size in bytes + """ + raise NotImplementedError() + + def local_size_import_complete(self) -> None: + """Override this method to handle operations after local game size import is finished (like updating cache).""" + def create_and_run_plugin(plugin_class, argv): """Call this method as an entry point for the implemented integration. diff --git a/tests/conftest.py b/tests/conftest.py index fd01d5d..26ee0c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,6 +64,9 @@ async def plugin(reader, writer): "get_user_presence", "prepare_user_presence_context", "user_presence_import_complete", + "get_local_size", + "prepare_local_size_context", + "local_size_import_complete", ) with ExitStack() as stack: diff --git a/tests/test_features.py b/tests/test_features.py index cf2922e..6bb4201 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -17,7 +17,8 @@ def test_base_class(): Feature.LaunchPlatformClient, Feature.ImportGameLibrarySettings, Feature.ImportOSCompatibility, - Feature.ImportUserPresence + Feature.ImportUserPresence, + Feature.ImportLocalSize } diff --git a/tests/test_local_size.py b/tests/test_local_size.py new file mode 100644 index 0000000..42c9946 --- /dev/null +++ b/tests/test_local_size.py @@ -0,0 +1,188 @@ +from unittest.mock import call + +import pytest +from galaxy.api.errors import FailedParsingManifest +from galaxy.unittest.mock import async_return_value + +from tests import create_message, get_messages + + +@pytest.mark.asyncio +async def test_get_local_size_success(plugin, read, write): + context = {'abc': 'def'} + plugin.prepare_local_size_context.return_value = async_return_value(context) + request = { + "jsonrpc": "2.0", + "id": "11", + "method": "start_local_size_import", + "params": {"game_ids": ["777", "13", "42"]} + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_local_size.side_effect = [ + async_return_value(100000000000), + async_return_value(None), + async_return_value(3333333) + ] + await plugin.run() + plugin.get_local_size.assert_has_calls([ + call("777", context), + call("13", context), + call("42", context) + ]) + plugin.local_size_import_complete.assert_called_once_with() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "11", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_success", + "params": { + "game_id": "777", + "local_size": 100000000000 + } + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_success", + "params": { + "game_id": "13", + "local_size": None + } + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_success", + "params": { + "game_id": "42", + "local_size": 3333333 + } + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_finished", + "params": None + } + ] + +@pytest.mark.asyncio +@pytest.mark.parametrize("exception,code,message", [ + (FailedParsingManifest, 200, "Failed parsing manifest"), + (KeyError, 0, "Unknown error") +]) +async def test_get_local_size_error(exception, code, message, plugin, read, write): + game_id = "6" + request_id = "55" + plugin.prepare_local_size_context.return_value = async_return_value(None) + request = { + "jsonrpc": "2.0", + "id": request_id, + "method": "start_local_size_import", + "params": {"game_ids": [game_id]} + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_local_size.side_effect = exception + await plugin.run() + plugin.get_local_size.assert_called() + plugin.local_size_import_complete.assert_called_once_with() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": request_id, + "result": None + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_failure", + "params": { + "game_id": game_id, + "error": { + "code": code, + "message": message + } + } + }, + { + "jsonrpc": "2.0", + "method": "local_size_import_finished", + "params": None + } + ] + + +@pytest.mark.asyncio +async def test_prepare_get_local_size_context_error(plugin, read, write): + request_id = "31415" + error_details = "Unexpected syntax" + error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code + plugin.prepare_local_size_context.side_effect = FailedParsingManifest(error_details) + request = { + "jsonrpc": "2.0", + "id": request_id, + "method": "start_local_size_import", + "params": {"game_ids": ["6"]} + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + await plugin.run() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": error_code, + "message": error_message, + "data": error_details + } + } + ] + + +@pytest.mark.asyncio +async def test_import_already_in_progress_error(plugin, read, write): + plugin.prepare_local_size_context.return_value = async_return_value(None) + requests = [ + { + "jsonrpc": "2.0", + "id": "3", + "method": "start_local_size_import", + "params": { + "game_ids": ["42"] + } + }, + { + "jsonrpc": "2.0", + "id": "4", + "method": "start_local_size_import", + "params": { + "game_ids": ["13"] + } + } + ] + read.side_effect = [ + async_return_value(create_message(requests[0])), + async_return_value(create_message(requests[1])), + async_return_value(b"", 10) + ] + + await plugin.run() + + responses = get_messages(write) + assert { + "jsonrpc": "2.0", + "id": "3", + "result": None + } in responses + assert { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": 600, + "message": "Import already in progress" + } + } in responses +