From dec8e22fa1017bedace8b9be1ed1726663914c2f Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Thu, 16 May 2024 18:22:54 +0400 Subject: [PATCH] feat(tests): add `pyfakefs` to mock filesystem in tests --- .github/workflows/integration_delivery.yml | 1 - CHANGELOG.md | 4 ++ poetry.lock | 43 ++++++++----- pyproject.toml | 3 +- scripts/Dockerfile.test | 1 + tests/fixtures/app.py | 60 ++++++++++++++----- tests/fixtures/snapshot.py | 6 +- .../wireless_flow/store-000.jsonc | 2 +- .../wireless_flow/store-001.jsonc | 2 +- .../wireless_flow/store-002.jsonc | 2 +- .../wireless_flow/store-003.jsonc | 2 +- .../wireless_flow/store-004.jsonc | 2 +- .../wireless_flow/store-005.jsonc | 2 +- .../wireless_flow/store-006.jsonc | 2 +- .../wireless_flow/store-007.jsonc | 2 +- .../wireless_flow/store-008.jsonc | 2 +- .../all_services_register/store-000.jsonc | 2 +- tests/monkeypatch.py | 1 + ubo_app/load_services.py | 8 +++ ubo_app/store/__init__.py | 9 ++- 20 files changed, 108 insertions(+), 48 deletions(-) diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index 57eef632..bd337f51 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -135,7 +135,6 @@ jobs: - name: Run Tests run: | - mkdir -p $HOME/.kivy/mods POETRY_VIRTUALENVS_OPTIONS_SYSTEM_SITE_PACKAGES=true poetry run poe test --make-screenshots --cov-report=xml --cov-report=html -n auto --log-level=DEBUG - name: Collect Window Screenshots diff --git a/CHANGELOG.md b/CHANGELOG.md index fd809108..eddd5ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # ChangeLog +## Version 0.14.3 + +- feat(tests): add `pyfakefs` to mock filesystem in tests + ## Version 0.14.2 - fix(vscode): show a success notification when the login process is completed instead diff --git a/poetry.lock b/poetry.lock index b7c3f0fe..56839d68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,13 +50,13 @@ adafruit-circuitpython-typing = "*" [[package]] name = "adafruit-circuitpython-connectionmanager" -version = "2.0.0" +version = "3.1.0" description = "A urllib3.poolmanager/urllib3.connectionpool-like library for managing sockets and connections" optional = false python-versions = "*" files = [ - {file = "adafruit_circuitpython_connectionmanager-2.0.0-py3-none-any.whl", hash = "sha256:a7164831e02c02792c65123d2931dfeb6e998456aa0da48a0effaaddc8af7691"}, - {file = "adafruit_circuitpython_connectionmanager-2.0.0.tar.gz", hash = "sha256:32b0a2e38f703bf6e22d93e6929a301fe4e18fff6f68ad251521b8d250ef6436"}, + {file = "adafruit_circuitpython_connectionmanager-3.1.0-py3-none-any.whl", hash = "sha256:8dffd3e60d795e11c13f98795a45c42b568ea135953b2383c56a2fa6090512d3"}, + {file = "adafruit_circuitpython_connectionmanager-3.1.0.tar.gz", hash = "sha256:2a89f27e31be1b09dc221bffaa599baacd238a0bc369fe695c41be86c0d779a2"}, ] [package.dependencies] @@ -126,13 +126,13 @@ typing-extensions = ">=4.0,<5.0" [[package]] name = "adafruit-circuitpython-requests" -version = "3.2.8" +version = "3.2.9" description = "A requests-like library for web interfacing" optional = false python-versions = "*" files = [ - {file = "adafruit_circuitpython_requests-3.2.8-py3-none-any.whl", hash = "sha256:aca829e7a84bda37b8a70a867e28ae8c8aa27cbf596ea16f52805b0d2051be13"}, - {file = "adafruit_circuitpython_requests-3.2.8.tar.gz", hash = "sha256:50e46e4a1ee9a4f2e832308468191b855424ebc468a0bc8c911c68df33918863"}, + {file = "adafruit_circuitpython_requests-3.2.9-py3-none-any.whl", hash = "sha256:9b9350b47eb1f0de97c6bac6852274515eccc7cec56660e2290e5885c1c25369"}, + {file = "adafruit_circuitpython_requests-3.2.9.tar.gz", hash = "sha256:4cbcfff2fcf0888363033d9d18681fa61af65f6d88bdb5965507821b34d91cf2"}, ] [package.dependencies] @@ -1252,13 +1252,13 @@ ptyprocess = ">=0.5" [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -1411,6 +1411,17 @@ files = [ [package.extras] test = ["numpy"] +[[package]] +name = "pyfakefs" +version = "5.5.0" +description = "pyfakefs implements a fake file system that mocks the Python file system modules." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyfakefs-5.5.0-py3-none-any.whl", hash = "sha256:8dbf203ab7bef1529f11f7d41b9478b898e95bf9f3b71262163aac07a518cd76"}, + {file = "pyfakefs-5.5.0.tar.gz", hash = "sha256:7448aaa07142f892d0a4eb52a5ed3206a9f02c6599e686cd97d624c18979c154"}, +] + [[package]] name = "pyftdi" version = "0.55.4" @@ -1502,13 +1513,13 @@ files = [ [[package]] name = "pyright" -version = "1.1.362" +version = "1.1.363" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.362-py3-none-any.whl", hash = "sha256:969957cff45154d8a45a4ab1dae5bdc8223d8bd3c64654fa608ab3194dfff319"}, - {file = "pyright-1.1.362.tar.gz", hash = "sha256:6a477e448d4a07a6a0eab58b2a15a1bbed031eb3169fa809edee79cca168d83a"}, + {file = "pyright-1.1.363-py3-none-any.whl", hash = "sha256:d3b8d73c8d230e26cc3523862f3398032a0c39a00d7bb69dc0f595f8e888fd01"}, + {file = "pyright-1.1.363.tar.gz", hash = "sha256:00a8f0ae0e339473bb0488f8a2a2dcdf574e94a16cd7b4390d49d144714d8db2"}, ] [package.dependencies] @@ -1684,13 +1695,13 @@ typing-extensions = ">=4.10.0,<5.0.0" [[package]] name = "python-redux" -version = "0.15.2" +version = "0.15.4" description = "Redux implementation for Python" optional = false python-versions = "<4.0,>=3.11" files = [ - {file = "python_redux-0.15.2-py3-none-any.whl", hash = "sha256:bb6530d4d4da04a441f38542bc6889a3f710bc499959c885d0cbf75b9a3b1216"}, - {file = "python_redux-0.15.2.tar.gz", hash = "sha256:87ec46335130267bb23c28cdd031b204d8c31aef77bceac662fda40161ff7266"}, + {file = "python_redux-0.15.4-py3-none-any.whl", hash = "sha256:e7a2e806866da1a916fdbbee29791316896c59a3b0965ae17be2177886098d85"}, + {file = "python_redux-0.15.4.tar.gz", hash = "sha256:6f16f2cbbd2c2de80480336f61d0b00771589ab1f7fdfde642905127dfed36f5"}, ] [package.dependencies] @@ -2193,4 +2204,4 @@ dev = ["ubo-gui", "ubo-gui"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "df39e96f354f86fd3a532a3c2786e2f6b444efc16554d81051cb0460ee16eebe" +content-hash = "1e24aaf173dbd05aafd924fab7dd1bf35c2bb6bdb538f54a95c6a1aeb3bdf3ab" diff --git a/pyproject.toml b/pyproject.toml index b4fc444f..e8ff816b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ubo-app" -version = "0.14.2" +version = "0.14.3" description = "Ubo main app, running on device initialization. A platform for running other apps." authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -60,6 +60,7 @@ toml = "^0.10.2" pytest-mock = "^3.14.0" pyaudio = { version = "^0.2.14", markers = "platform_machine!='aarch64'" } ipython = "^8.23.0" +pyfakefs = "^5.5.0" [tool.poetry.extras] default = ["ubo-gui"] diff --git a/scripts/Dockerfile.test b/scripts/Dockerfile.test index 3a2cf381..61d402a2 100644 --- a/scripts/Dockerfile.test +++ b/scripts/Dockerfile.test @@ -1,5 +1,6 @@ FROM ubo-app-dev RUN mkdir -p /root/.kivy/mods +RUN touch /root/.kivy/icon ENTRYPOINT ["/bin/bash", "-c", "reset; poetry install --with dev --extras=dev --verbose && poetry run poe test $@"] diff --git a/tests/fixtures/app.py b/tests/fixtures/app.py index c72a36db..20861efa 100644 --- a/tests/fixtures/app.py +++ b/tests/fixtures/app.py @@ -7,8 +7,10 @@ import logging import sys import weakref +from pathlib import Path from typing import TYPE_CHECKING +import platformdirs import pytest from ubo_app.setup import setup @@ -17,6 +19,7 @@ from collections.abc import AsyncGenerator from _pytest.fixtures import SubRequest + from pyfakefs.fake_filesystem import FakeFilesystem from ubo_app.menu_app.menu import MenuApp @@ -26,9 +29,10 @@ class AppContext: """Context object for tests running a menu application.""" - def __init__(self: AppContext, request: SubRequest) -> None: + def __init__(self: AppContext, request: SubRequest, *, fs: FakeFilesystem) -> None: """Initialize the context.""" self.request = request + self.fs = fs def set_app(self: AppContext, app: MenuApp) -> None: """Set the application.""" @@ -81,35 +85,61 @@ async def clean_up(self: AppContext) -> None: Window.close() - for module in set(sys.modules) - modules_snapshot: - if module != 'objc' and 'numpy' not in module and 'cache' not in module: - del sys.modules[module] - gc.collect() - @pytest.fixture() -async def app_context( - request: SubRequest, -) -> AsyncGenerator[AppContext, None]: +async def app_context(request: SubRequest) -> AsyncGenerator[AppContext, None]: """Create the application.""" import os + from pyfakefs.fake_filesystem_unittest import Patcher + os.environ['KIVY_NO_FILELOG'] = '1' os.environ['KIVY_NO_CONSOLELOG'] = '1' os.environ['KIVY_METRICS_DENSITY'] = '1' - setup() - import headless_kivy_pi.config headless_kivy_pi.config.setup_headless_kivy( {'automatic_fps': True, 'flip_vertical': True}, ) - context = AppContext(request) + current_path = Path() + with Patcher( + additional_skip_names=[ + 'redux_pytest.fixtures', + 'tests.fixtures.snapshot', + 'pathlib', + ], + ) as patcher: + assert patcher.fs is not None + + patcher.fs.add_real_paths( + [ + (current_path / 'tests').absolute().as_posix(), + (current_path / 'ubo_app').absolute().as_posix(), + platformdirs.user_cache_dir('pypoetry'), + ], + ) + + setup() - yield context + context = AppContext(request, fs=patcher.fs) - await context.clean_up() + yield context - assert not hasattr(context, 'app'), 'App not cleaned up' + await context.clean_up() + + assert not hasattr(context, 'app'), 'App not cleaned up' + + del context + del patcher + + for module in set(sys.modules) - modules_snapshot: + if ( + module != 'objc' + and 'numpy' not in module + and 'kivy.cache' not in module + ): + del sys.modules[module] + + gc.collect() diff --git a/tests/fixtures/snapshot.py b/tests/fixtures/snapshot.py index ec6edf61..3f7c9628 100644 --- a/tests/fixtures/snapshot.py +++ b/tests/fixtures/snapshot.py @@ -4,13 +4,13 @@ import os from collections import defaultdict +from pathlib import Path from typing import TYPE_CHECKING, Any, cast import pytest if TYPE_CHECKING: from collections.abc import Generator - from pathlib import Path from _pytest.fixtures import SubRequest from _pytest.nodes import Node @@ -49,11 +49,11 @@ def __init__( self.make_screenshots = make_screenshots self.test_counter: dict[str | None, int] = defaultdict(int) file = test_node.path.with_suffix('').name - self.results_dir = ( + self.results_dir = Path( test_node.path.parent / 'results' / file - / test_node.nodeid.split('::')[-1][5:] + / test_node.nodeid.split('::')[-1][5:], ) if self.results_dir.exists(): for file in self.results_dir.glob( diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-000.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-000.jsonc index 01332311..9df24c8d 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-000.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-000.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-001.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-001.jsonc index 55935364..50b43483 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-001.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-001.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-002.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-002.jsonc index 047f16a7..f678afcc 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-002.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-002.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-003.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-003.jsonc index 2936ee7b..07b3e506 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-003.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-003.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-004.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-004.jsonc index c0d13e49..191b85b2 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-004.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-004.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-005.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-005.jsonc index 1023b797..4afbdc04 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-005.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-005.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-006.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-006.jsonc index 6df522c6..441b58af 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-006.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-006.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-007.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-007.jsonc index 7098184d..423a49d3 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-007.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-007.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/flows/results/test_wireless_flow/wireless_flow/store-008.jsonc b/tests/flows/results/test_wireless_flow/wireless_flow/store-008.jsonc index c99f7019..0be3079a 100644 --- a/tests/flows/results/test_wireless_flow/wireless_flow/store-008.jsonc +++ b/tests/flows/results/test_wireless_flow/wireless_flow/store-008.jsonc @@ -79,7 +79,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/integration/results/test_services/all_services_register/store-000.jsonc b/tests/integration/results/test_services/all_services_register/store-000.jsonc index 7d8725af..4d7dad71 100644 --- a/tests/integration/results/test_services/all_services_register/store-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-000.jsonc @@ -170,7 +170,7 @@ "sub_menu": { "items": [ { - "application": "ubo_app/services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", + "application": "services/030-wifi/pages/create_wireless_connection.py:CreateWirelessConnectionPage", "background_color": "#68B7FF", "color": [ 1, diff --git a/tests/monkeypatch.py b/tests/monkeypatch.py index e10dcf87..51e7d6d9 100644 --- a/tests/monkeypatch.py +++ b/tests/monkeypatch.py @@ -243,6 +243,7 @@ def _monkeypatch(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr('importlib.metadata.version', lambda _: '0.0.0') monkeypatch.setattr('ubo_app.constants.STORE_GRACE_TIME', 0.1) monkeypatch.setattr('ubo_app.utils.serializer.add_type_field', lambda _, y: y) + monkeypatch.setattr('pyzbar.pyzbar.decode', Fake()) def fake_read_from_persistent_store( key: str, diff --git a/ubo_app/load_services.py b/ubo_app/load_services.py index 5b63b456..d7ada89e 100644 --- a/ubo_app/load_services.py +++ b/ubo_app/load_services.py @@ -239,6 +239,14 @@ def initiate(self: UboServiceThread) -> None: def run(self: UboServiceThread) -> None: self.loop = asyncio.new_event_loop() self.loop.set_exception_handler(loop_exception_handler) + logger.debug( + 'Starting service thread', + extra={ + 'thread_native_id': self.native_id, + 'service_label': self.label, + 'service_id': self.service_id, + }, + ) asyncio.set_event_loop(self.loop) if DEBUG_MODE: self.loop.set_debug(enabled=True) diff --git a/ubo_app/store/__init__.py b/ubo_app/store/__init__.py index 12835a88..29c5e955 100644 --- a/ubo_app/store/__init__.py +++ b/ubo_app/store/__init__.py @@ -151,9 +151,14 @@ def serialize_value(cls: type[UboStore], obj: object | type) -> SnapshotAtom: if isinstance(obj, Autorun): obj = obj() if isinstance(obj, type) and issubclass(obj, PageWidget): + import ubo_app + + _ = ubo_app file_path = sys.modules[obj.__module__].__file__ - if file_path: - return f"""{Path(file_path).relative_to(Path().absolute()).as_posix()}:{ + ubo_app_path = sys.modules['ubo_app'].__file__ + if file_path and ubo_app_path: + root_path = Path(ubo_app_path).parent + return f"""{Path(file_path).relative_to(root_path).as_posix()}:{ obj.__name__}""" return f'{obj.__module__}:{obj.__name__}' if callable(obj):