Skip to content

Commit

Permalink
chore(test): set up testing framework with initial examples
Browse files Browse the repository at this point in the history
chore(test): set up snapshot test helper to compare screenshots of different stages of tests with previous successful tests using hashes
  • Loading branch information
sassanh committed Mar 15, 2024
1 parent 2d1a0b8 commit ee8798b
Show file tree
Hide file tree
Showing 21 changed files with 1,061 additions and 200 deletions.
58 changes: 58 additions & 0 deletions .github/workflows/integration_delivery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,59 @@ jobs:
- name: Lint
run: poetry run poe lint

test:
name: Test
needs:
- dependencies
runs-on: ubuntu-latest
steps:
- name: System Dependencies
run: sudo apt-get install -y libegl1 libgl1 libmtdev1 libzbar0

- uses: actions/checkout@v4
name: Checkout

- uses: actions/setup-python@v5
name: Setup Python
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64

- name: Load Cached Poetry
id: cached-poetry
uses: actions/cache/restore@v4
with:
path: |
~/.cache
~/.local
key: poetry-${{ hashFiles('poetry.lock') }}

- name: Test
run: poetry run poe test --cov-report=xml --cov-report=html

- name: Collect Mismatching Screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: mismatching-screenshots
path: tests/**/*.mismatch.png

- name: Collect HTML Coverage Report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov

- name: Upload Coverage to Codecov
if: always()
uses: codecov/[email protected]
with:
file: ./coverage.xml
flags: integration
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

build:
name: Build
needs:
Expand Down Expand Up @@ -155,6 +208,7 @@ jobs:
needs:
- type-check
- lint
- test
- build
runs-on: ubuntu-latest
environment:
Expand Down Expand Up @@ -183,6 +237,9 @@ jobs:
images:
name: Create Images
needs:
- type-check
- lint
- test
- build
runs-on: ubuntu-latest
container:
Expand Down Expand Up @@ -271,6 +328,7 @@ jobs:
needs:
- type-check
- lint
- test
- build
- pypi-publish
- images
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
tests/**/results/*png

# Translations
*.mo
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Version 0.11.1

- chore(test): set up testing framework with initial examples
- chore(test): set up snapshot test helper to compare screenshots of different stages
of tests with previous successful tests using hashes

## Version 0.11.0

- feat: add ollama and open-webui docker images
Expand Down
270 changes: 232 additions & 38 deletions poetry.lock

Large diffs are not rendered by default.

29 changes: 23 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "ubo-app"
version = "0.11.0"
version = "0.11.1"
description = "Ubo main app, running on device initialization. A platform for running other apps."
authors = ["Sassan Haradji <[email protected]>"]
license = "Apache-2.0"
Expand All @@ -15,21 +15,22 @@ priority = "primary"


[tool.poetry.dependencies]

python = "^3.11"
psutil = "^5.9.8"
ubo-gui = [
{ version = "^0.9.7", markers = "extra=='default'", extras = [
{ version = "^0.9.9", markers = "extra=='default'", extras = [
'default',
] },
{ version = "^0.9.7", markers = "extra=='dev'", extras = [
{ version = "^0.9.9", markers = "extra=='dev'", extras = [
'dev',
] },
]
python-redux = "^0.10.7"
python-redux = "^0.11.0"
pyzbar = "^0.1.9"
sdbus-networkmanager = { version = "^2.0.0", markers = "platform_machine=='aarch64'" }
rpi_ws281x = { version = "^5.0.0", markers = "platform_machine=='aarch64'" }
python-debouncer = "^0.1.3"
python-debouncer = "^0.1.4"
adafruit-circuitpython-neopixel = "^6.3.11"
pulsectl = "^23.5.2"
aiohttp = "^3.9.1"
Expand All @@ -43,9 +44,15 @@ python-dotenv = "^1.0.1"
optional = true

[tool.poetry.group.dev.dependencies]
poethepoet = "^0.24.4"
pyright = "^1.1.349"
ruff = "^0.3.2"
toml = "^0.10.2"
pytest = "^8.0.0"
pytest-cov = "^4.1.0"
pytest-xdist = "^3.5.0"
tenacity = "^8.2.3"
pytest-asyncio = "^0.23.5.post1"

[tool.poetry.extras]
default = ['ubo-gui']
Expand All @@ -61,7 +68,8 @@ build-backend = "poetry.core.masonry.api"
[tool.poe.tasks]
lint = "ruff check . --unsafe-fixes"
typecheck = "pyright -p pyproject.toml ."
sanity = ["typecheck", "lint"]
test = "pytest --cov=ubo_app --cov-report=term-missing"
sanity = ["typecheck", "lint", "test"]

[tool.poe.tasks.deploy_to_device]
args = [
Expand All @@ -85,6 +93,9 @@ docstring-quotes = "double"
inline-quotes = "single"
multiline-quotes = "double"

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]

[tool.ruff.format]
quote-style = 'single'

Expand All @@ -93,3 +104,9 @@ profile = "black"

[tool.pyright]
exclude = ['typings']

[tool.pytest.ini_options]
asyncio_mode = 'auto'
filterwarnings = "ignore:'imghdr' is deprecated:DeprecationWarning"
log_cli = 1
log_cli_level = 'ERROR'
173 changes: 173 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Pytest configuration file for the tests."""
from __future__ import annotations

import asyncio
import atexit
import datetime
import gc
import sys
import threading
import weakref
from typing import TYPE_CHECKING, AsyncGenerator, Callable, Generator

import pytest
from redux import FinishAction
from snapshot import snapshot
from tenacity import retry, stop_after_delay, wait_exponential

from ubo_app.utils.garbage_collection import examine

if TYPE_CHECKING:
from logging import Logger

from ubo_app.menu import MenuApp

__all__ = ('app_context', 'snapshot')


@pytest.fixture(autouse=True, name='monkeypatch_atexit')
def _(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(atexit, 'register', lambda _: None)


modules_snapshot = set(sys.modules)


@pytest.fixture(autouse=True)
def _(monkeypatch: pytest.MonkeyPatch) -> None:
"""Mock external resources."""
monkeypatch.setattr('psutil.cpu_percent', lambda **_: 50)
monkeypatch.setattr(
'psutil.virtual_memory',
lambda *_: type('', (object,), {'percent': 50}),
)

class DateTime(datetime.datetime):
@classmethod
def now(cls: type[DateTime], tz: datetime.tzinfo | None = None) -> DateTime:
_ = tz
return DateTime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)

monkeypatch.setattr(datetime, 'datetime', DateTime)


@pytest.fixture()
def logger() -> Logger:
import logging

import ubo_app.logging
from ubo_app.constants import LOG_LEVEL

level = (
getattr(
ubo_app.logging,
LOG_LEVEL,
getattr(logging, LOG_LEVEL, logging.DEBUG),
)
if LOG_LEVEL
else logging.DEBUG
)

logger = ubo_app.logging.get_logger('test')
logger.setLevel(level)

return logger


class AppContext:
"""Context object for tests running a menu application."""

def set_app(self: AppContext, app: MenuApp) -> None:
"""Set the application."""
self.app = app
loop = asyncio.get_event_loop()
self.task = loop.create_task(self.app.async_run(async_lib='asyncio'))


@pytest.fixture()
async def app_context(logger: Logger) -> AsyncGenerator[AppContext, None]:
"""Create the application."""
import os

os.environ['KIVY_NO_FILELOG'] = '1'
os.environ['KIVY_NO_CONSOLELOG'] = '1'

import headless_kivy_pi.config

headless_kivy_pi.config.setup_headless_kivy({'automatic_fps': True})

context = AppContext()

yield context

assert context.task is not None, 'App not set for test'

await context.task

app_ref = weakref.ref(context.app)
context.app.root.clear_widgets()

del context.app
del context.task

gc.collect()
app = app_ref()

if app is not None:
logger.debug(
'Memory leak: failed to release app for test.',
extra={
'referrers': gc.get_referrers(app),
'referents': gc.get_referents(app),
'refcount': sys.getrefcount(app),
'ref': app,
},
)
gc.collect()
for cell in gc.get_referrers(app):
if type(cell).__name__ == 'cell':
logger.debug('CELL EXAMINATION', extra={'cell': cell})
examine(cell, depth_limit=2)
assert app is None, 'Memory leak: failed to release app for test'

from kivy.core.window import Window

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()
def needs_finish() -> Generator:
yield None

from ubo_app.store import dispatch

dispatch(FinishAction())


class WaitFor(threading.Thread):
def __call__(
self: WaitFor,
satisfaction: Callable[[], None],
*,
timeout: float = 1,
) -> None:
self.retry = retry(
stop=stop_after_delay(timeout),
wait=wait_exponential(multiplier=0.5),
)(satisfaction)
self.start()

def run(self: WaitFor) -> None:
self.retry()


@pytest.fixture()
def wait_for() -> Generator[WaitFor, None, None]:
context = WaitFor()
yield context
context.join()
1 change: 1 addition & 0 deletions tests/results/test_all_services_register-000.hash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a4173e6761d21df059d9865602afbe8482c959c68a8a9426e3c3270374d77471
Loading

0 comments on commit ee8798b

Please sign in to comment.