diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 04f86a3..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [main] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.0 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2226bb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: ['main', 'dev'] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install poetry + poetry install + - name: Setup container display + # https://arbitrary-but-fixed.net/2022/01/21/headless-gui-github-actions.html + run: Xvfb :1 -screen 0 1600x1200x24 & + - name: Test with pytest + run: poetry run pytest -v + env: + DISPLAY: :1 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml deleted file mode 100644 index 6ebffb8..0000000 --- a/.github/workflows/tox.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Test - -on: - pull_request: - push: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry tox tox-gh-actions - - name: Test with tox - run: tox \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8d69795..fe66cdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ python = ">=3.8, <4.0" pyserial = "^3.5" python-can = "^4.1.0" dearpygui = "^1.9.0" +dearpygui-ext = "^0.9.5" [tool.poetry.group.dev.dependencies] pytest = "^7.2.2" diff --git a/src/can_explorer/app.py b/src/can_explorer/app.py index e0d1f01..2de05d2 100644 --- a/src/can_explorer/app.py +++ b/src/can_explorer/app.py @@ -37,10 +37,7 @@ def set_state(self, state: bool) -> None: """ self._state = State(state) - if self._state: - self.start() - else: - self.stop() + self.start() if self._state else self.stop() def repopulate(self) -> None: """ diff --git a/src/can_explorer/can_bus.py b/src/can_explorer/can_bus.py index f753448..72feff8 100644 --- a/src/can_explorer/can_bus.py +++ b/src/can_explorer/can_bus.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import defaultdict, deque -from typing import Final +from typing import Final, Optional from can.bus import BusABC from can.interfaces import VALID_INTERFACES @@ -41,9 +41,9 @@ def __getitem__(self, index) -> tuple: # type: ignore [override] class Recorder(defaultdict): _active = False - _bus: BusABC _notifier: Notifier _listener: _Listener + _bus: Optional[BusABC] = None def __init__(self): super().__init__(PayloadBuffer) @@ -57,7 +57,7 @@ def start(self) -> None: return self._listener = _Listener(self) - self._notifier = Notifier(self.bus, [self._listener]) + self._notifier = Notifier(self._bus, [self._listener]) self._active = True def stop(self) -> None: @@ -67,4 +67,4 @@ def stop(self) -> None: self._notifier.stop() # type: ignore [union-attr] def set_bus(self, bus: BusABC) -> None: - self.bus = bus + self._bus = bus diff --git a/src/can_explorer/layout.py b/src/can_explorer/layout.py index fbe14be..70a228c 100644 --- a/src/can_explorer/layout.py +++ b/src/can_explorer/layout.py @@ -3,6 +3,7 @@ from typing import Callable, Final, Iterable, Union import dearpygui.dearpygui as dpg +from dearpygui_ext.themes import create_theme_imgui_light from can_explorer.can_bus import PayloadBuffer from can_explorer.resources import Percentage @@ -20,8 +21,14 @@ class Default: class Font: - DEFAULT: str - LABEL: str + DEFAULT: int + LABEL: int + + +class Theme: + DEFAULT: int + LIGHT: int + MIDNIGHT: int @unique @@ -91,6 +98,11 @@ def _init_fonts(): def _init_themes(): + global Theme + + Theme.DEFAULT = 0 + Theme.LIGHT = create_theme_imgui_light() + default_background = (50, 50, 50, 255) with dpg.theme() as disabled_theme: with dpg.theme_component(dpg.mvButton, enabled_state=False): @@ -195,6 +207,14 @@ def _settings_tab() -> None: dpg.add_spacer(height=5) with dpg.collapsing_header(label="GUI"): + + def light_theme_calllback(sender, app_data, user_data): + dpg.bind_theme(Theme.LIGHT if dpg.get_value(sender) else Theme.DEFAULT) + + with dpg.group(horizontal=True): + dpg.add_text("Light Theme") + dpg.add_checkbox(callback=light_theme_calllback) + dpg.add_button( label="Launch Font Manager", width=-1, callback=dpg.show_font_manager ) diff --git a/tests/conftest.py b/tests/conftest.py index 1350304..77dc98b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,52 @@ +from unittest.mock import Mock, patch + import pytest +from can_explorer import app, can_bus, plotting + + +@pytest.fixture +def mock_buffer(): + with patch("can_explorer.can_bus.PayloadBuffer", autospec=True) as mock: + yield mock + + +@pytest.fixture +def mock_listener(): + with patch("can_explorer.can_bus._Listener", autospec=True) as mock: + yield mock @pytest.fixture -def mock_recorder(): - ... +def mock_notifier(): + with patch("can_explorer.can_bus.Notifier", autospec=True) as mock: + yield mock @pytest.fixture -def mock_plot_manager(): - ... +def fake_recorder(mock_listener, mock_notifier): + recorder = can_bus.Recorder() + + with patch("can_explorer.can_bus.Recorder") as mock: + mock.return_value = recorder + + yield recorder + + +@pytest.fixture +def fake_manager(): + with patch("can_explorer.plotting.Row"): + manager = plotting.PlotManager() + + with patch("can_explorer.plotting.PlotManager") as mock: + mock.return_value = manager + + yield manager @pytest.fixture -def mock_app(): - ... +def fake_app(fake_manager, fake_recorder): + main_app = app.MainApp() + main_app.plot_manager = fake_manager + main_app.can_recorder = fake_recorder + main_app.set_bus(Mock()) + yield main_app diff --git a/tests/test_app.py b/tests/test_app.py index 2657ab6..4dea082 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,42 @@ -# for 3.8, 3.9, etc. -def test_app_launches_without_errors(): - ... +from random import sample +from threading import Thread +from time import sleep +import dearpygui.dearpygui as dpg +from can_explorer import app -def test_app_populates_data_in_ascending_order(): - ... +DELAY = 0.1 + + +def test_app_launch_basic(): + Thread(target=app.main).start() + sleep(DELAY) + assert dpg.is_dearpygui_running() + dpg.stop_dearpygui() + + +def test_set_app_state_starts_worker(fake_app): + fake_app.set_state(True) + assert fake_app._worker.is_alive() + + +def test_set_app_state_stops_worker(fake_app): + fake_app.set_state(True) + assert fake_app._worker.is_alive() + fake_app.set_state(False) + assert not fake_app._worker.is_alive() + + +def test_app_populates_data_in_ascending_order(fake_app, fake_manager, fake_recorder): + data = sample(range(250), 25) + + fake_app.start() + + for i in data: + fake_recorder[i] = [0] + + sleep(DELAY) + sorted_data = list(sorted(data)) + sorted_keys = list(fake_manager.row.keys()) + + assert sorted_data == sorted_keys