From 2e7ff1c6ae9de1be7129913c879ba662d1be4253 Mon Sep 17 00:00:00 2001 From: Liron Ilouz <43831550+ilouzl@users.noreply.github.com> Date: Tue, 20 May 2025 10:57:40 +0300 Subject: [PATCH 1/5] Add cross-platform testing suite --- .github/workflows/test.yml | 28 +++++++++++++++++++ Readme.md | 4 +++ TESTING.md | 15 ++++++++++ requirements-dev.txt | 1 + requirements.txt | 2 +- tests/__init__.py | 0 tests/conftest.py | 28 +++++++++++++++++++ tests/test_cross_platform.py | 53 ++++++++++++++++++++++++++++++++++++ tests/test_inputmodes.py | 18 ++++++++++++ tests/test_parsers.py | 22 +++++++++++++++ 10 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 TESTING.md create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_cross_platform.py create mode 100644 tests/test_inputmodes.py create mode 100644 tests/test_parsers.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8b9e79d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: CI + +on: + pull_request: + push: + branches: [ main, master ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.8", "3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + pip install -e . + - name: Run tests + run: pytest -v diff --git a/Readme.md b/Readme.md index 8160d54..6bb1b82 100644 --- a/Readme.md +++ b/Readme.md @@ -240,6 +240,10 @@ The dynamic range of the sensors is determined with the ```set_input_mode``` met ### Examples You can find some examples in the [examples folder](examples). +### Testing + +Install test requirements and run `pytest`. + ### Known Issues An up-to-date list of known issues is available [here](History.md). diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..6231781 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,15 @@ +# Testing Suite + +This SDK interfaces with Bluetooth hardware, so running tests on machines without the accessory requires mocking the BLE backend. The tests in `tests/` rely on small stubs that replace the `bleak` library and patch the detected platform. This allows the package to be imported and exercised without real hardware. + +## Running Tests Locally + +```bash +pip install -r requirements.txt +pip install -r requirements-dev.txt +pytest -v +``` + +## Continuous Integration + +A GitHub Actions workflow (`.github/workflows/test.yml`) is provided. It runs the test suite on Windows, macOS and Linux against multiple Python versions. The workflow installs the package in editable mode and executes `pytest`. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest diff --git a/requirements.txt b/requirements.txt index 40d8264..845a550 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ bleak -setuptools \ No newline at end of file +setuptools diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ba16bdc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +import sys +import types + + +def pytest_configure(config): + bleak_stub = types.ModuleType("bleak") + + class Dummy: + def __init__(self, *args, **kwargs): + pass + + bleak_stub.BleakClient = Dummy + bleak_stub.BleakScanner = Dummy + bleak_stub._logger = types.SimpleNamespace( + debug=lambda *a, **k: None, + info=lambda *a, **k: None, + error=lambda *a, **k: None, + ) + sys.modules.setdefault("bleak", bleak_stub) + + core_mod = types.ModuleType("bleak.backends.corebluetooth.CentralManagerDelegate") + core_mod.CBUUID = type("CBUUID", (), {"UUIDWithString_": staticmethod(lambda x: x)}) + core_mod.CentralManagerDelegate = type( + "CentralManagerDelegate", + (), + {"alloc": classmethod(lambda cls: type("Obj", (), {"init": lambda self: None})())}, + ) + sys.modules.setdefault("bleak.backends.corebluetooth.CentralManagerDelegate", core_mod) diff --git a/tests/test_cross_platform.py b/tests/test_cross_platform.py new file mode 100644 index 0000000..b91ed7b --- /dev/null +++ b/tests/test_cross_platform.py @@ -0,0 +1,53 @@ +import importlib +import sys +import types +from unittest.mock import patch + + +def _make_bleak_stub(): + bleak_stub = types.ModuleType("bleak") + + class Dummy: + def __init__(self, *args, **kwargs): + pass + + bleak_stub.BleakClient = Dummy + bleak_stub.BleakScanner = Dummy + bleak_stub._logger = types.SimpleNamespace( + debug=lambda *a, **k: None, + info=lambda *a, **k: None, + error=lambda *a, **k: None, + ) + core_mod = types.ModuleType( + "bleak.backends.corebluetooth.CentralManagerDelegate" + ) + core_mod.CBUUID = type("CBUUID", (), {"UUIDWithString_": staticmethod(lambda x: x)}) + core_mod.CentralManagerDelegate = type( + "CentralManagerDelegate", + (), + {"alloc": classmethod(lambda cls: type("Obj", (), {"init": lambda self: None})())}, + ) + return bleak_stub, core_mod + + +def _load_tap(platform_name: str): + bleak_stub, core_stub = _make_bleak_stub() + with patch.dict( + sys.modules, + { + "bleak": bleak_stub, + "bleak.backends.corebluetooth.CentralManagerDelegate": core_stub, + }, + ): + with patch("platform.system", return_value=platform_name): + if "tapsdk.tap" in sys.modules: + module = importlib.reload(sys.modules["tapsdk.tap"]) + else: + module = importlib.import_module("tapsdk.tap") + return module + + +def test_tapclient_defined_for_all_platforms(): + for name in ["Linux", "Windows", "Darwin"]: + module = _load_tap(name) + assert hasattr(module, "TapClient") diff --git a/tests/test_inputmodes.py b/tests/test_inputmodes.py new file mode 100644 index 0000000..18c2ce1 --- /dev/null +++ b/tests/test_inputmodes.py @@ -0,0 +1,18 @@ +import pytest +from tapsdk.inputmodes import TapInputMode, input_type_command +from tapsdk.enumerations import InputType + + +def test_input_mode_basic(): + assert TapInputMode("text").get_command() == bytearray([0x3, 0xc, 0x0, 0x0]) + assert TapInputMode("controller").get_command() == bytearray([0x3, 0xc, 0x0, 0x1]) + assert TapInputMode("controller_text").get_command() == bytearray([0x3, 0xc, 0x0, 0x3]) + + +def test_input_mode_raw_with_sensitivity(): + mode = TapInputMode("raw", sensitivity=[1, 2, 3]) + assert mode.get_command() == bytearray([0x3, 0xc, 0x0, 0xa, 1, 2, 3]) + + +def test_input_type_command(): + assert input_type_command(InputType.MOUSE) == bytearray([0x3, 0xd, 0x0, InputType.MOUSE.value]) diff --git a/tests/test_parsers.py b/tests/test_parsers.py new file mode 100644 index 0000000..9be0999 --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,22 @@ +import tapsdk.parsers as parsers + + +def test_mouse_data_msg(): + data = bytearray([0, 1, 0, 2, 0, 0, 0, 0, 0, 1]) + assert parsers.mouse_data_msg(data) == (1, 2, True) + + +def test_tap_data_msg(): + data = bytearray([5]) + assert parsers.tap_data_msg(data) == [5] + + +def test_raw_data_msg(): + ts = 42 + payload = [1, 2, 3, 4, 5, 6] + msg = bytearray() + msg += ts.to_bytes(4, "little") + for v in payload: + msg += v.to_bytes(2, "little", signed=True) + msg += bytearray([0, 0, 0, 0]) + assert parsers.raw_data_msg(msg) == [{"type": "imu", "ts": ts, "payload": payload}] From 7d72362c8c0ff673ff4631680abf57bf42a4d2b8 Mon Sep 17 00:00:00 2001 From: Liron Date: Wed, 21 May 2025 13:56:41 +0300 Subject: [PATCH 2/5] some CI workflow refactoring --- .github/workflows/test.yml | 8 +++---- Readme.md | 13 +++++++++- TESTING.md | 15 ------------ requirements-dev.txt | 1 - setup.py | 10 ++++---- tests/test_parsers.py | 49 +++++++++++++++++++++++++++++++------- 6 files changed, 61 insertions(+), 35 deletions(-) delete mode 100644 TESTING.md delete mode 100644 requirements-dev.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b9e79d..08f7f76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: push: - branches: [ main, master ] + branches: [master] jobs: build: @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python @@ -21,8 +21,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - pip install -e . + pip install -e .[dev] - name: Run tests run: pytest -v diff --git a/Readme.md b/Readme.md index 6bb1b82..d36fc30 100644 --- a/Readme.md +++ b/Readme.md @@ -240,9 +240,20 @@ The dynamic range of the sensors is determined with the ```set_input_mode``` met ### Examples You can find some examples in the [examples folder](examples). + ### Testing -Install test requirements and run `pytest`. +To run the tests, first install the development dependencies: + +```bash +pip install .[dev] +``` + +Then run the tests using pytest: + +```bash +pytest +``` ### Known Issues diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 6231781..0000000 --- a/TESTING.md +++ /dev/null @@ -1,15 +0,0 @@ -# Testing Suite - -This SDK interfaces with Bluetooth hardware, so running tests on machines without the accessory requires mocking the BLE backend. The tests in `tests/` rely on small stubs that replace the `bleak` library and patch the detected platform. This allows the package to be imported and exercised without real hardware. - -## Running Tests Locally - -```bash -pip install -r requirements.txt -pip install -r requirements-dev.txt -pytest -v -``` - -## Continuous Integration - -A GitHub Actions workflow (`.github/workflows/test.yml`) is provided. It runs the test suite on Windows, macOS and Linux against multiple Python versions. The workflow installs the package in editable mode and executes `pytest`. diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index e079f8a..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -pytest diff --git a/setup.py b/setup.py index 98d9dd0..e28933e 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ REQUIRED = [ # linux reqs 'bleak==0.6.4;platform_system=="Linux"', - # macOS reqs + # macOS reqs 'bleak==0.12.1;platform_system=="Darwin"', # Windows reqs 'pythonnet;platform_system=="Windows"' @@ -82,13 +82,13 @@ def run(self): author_email=EMAIL, url=URL, packages=find_packages(exclude=("tests", "examples", "docs")), - # package_data={"tapsdk.backends.dotnet": ["*.dll"]}, install_requires=REQUIRED, - # test_suite="tests", - # tests_require=TEST_REQUIRED, include_package_data=True, license="MIT", - python_requires='>=3.7' + python_requires='>=3.9', + extras_require={ + "dev": ["pytest"] + }, # classifiers=[ # # Trove classifiers # # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 9be0999..4a189c6 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -12,11 +12,44 @@ def test_tap_data_msg(): def test_raw_data_msg(): - ts = 42 - payload = [1, 2, 3, 4, 5, 6] - msg = bytearray() - msg += ts.to_bytes(4, "little") - for v in payload: - msg += v.to_bytes(2, "little", signed=True) - msg += bytearray([0, 0, 0, 0]) - assert parsers.raw_data_msg(msg) == [{"type": "imu", "ts": ts, "payload": payload}] + # 1. packet with one imu message + # IMU message: type=0, timestamp=123, 6 samples (12 bytes) + ts = 123 + imu_ts = ts # type bit is 0, so ts stays 123 + imu_bytes = imu_ts.to_bytes(4, 'little', signed=False) + imu_payload = b'' + imu_samples = [100, -100, 200, -200, 300, -300] + for v in imu_samples: + imu_payload += v.to_bytes(2, 'little', signed=True) + imu_packet = bytearray(imu_bytes + imu_payload) + result = parsers.raw_data_msg(imu_packet) + assert result == [{ + 'type': 'imu', + 'ts': 123, + 'payload': imu_samples + }] + + # 2. packet with one accl message + # Accl message: type=1, timestamp=456, 15 samples (30 bytes) + accl_ts = (1 << 31) + 456 # set MSB for accl + accl_bytes = accl_ts.to_bytes(4, 'little', signed=False) + accl_samples = list(range(1, 16)) + accl_payload = b'' + for v in accl_samples: + accl_payload += v.to_bytes(2, 'little', signed=True) + accl_packet = bytearray(accl_bytes + accl_payload) + result = parsers.raw_data_msg(accl_packet) + assert result == [{ + 'type': 'accl', + 'ts': 456, + 'payload': accl_samples + }] + + # 3. packet with imu message and accl message + # imu first, then accl + combo_packet = bytearray(imu_bytes + imu_payload + accl_bytes + accl_payload) + result = parsers.raw_data_msg(combo_packet) + assert result == [ + {'type': 'imu', 'ts': 123, 'payload': imu_samples}, + {'type': 'accl', 'ts': 456, 'payload': accl_samples} + ] From 2e09a60c8b042124541106d0290749eeae1c4bf2 Mon Sep 17 00:00:00 2001 From: Liron Date: Wed, 21 May 2025 14:02:24 +0300 Subject: [PATCH 3/5] add lint to CI workflow --- .flake8 | 2 ++ .github/workflows/test.yml | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 08f7f76..562e48d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,5 +22,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -e .[dev] + - name: Lint with flake8 + run: | + flake8 *.py - name: Run tests run: pytest -v From ea95f25640b843d6e7b2902ea4f0bbafcf3a5420 Mon Sep 17 00:00:00 2001 From: Liron Date: Wed, 21 May 2025 14:04:02 +0300 Subject: [PATCH 4/5] leftover --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e28933e..3ed784b 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,7 @@ def run(self): license="MIT", python_requires='>=3.9', extras_require={ - "dev": ["pytest"] + "dev": ["pytest", "flake8"] }, # classifiers=[ # # Trove classifiers From 19e7d4e58f2dada310317dcff2b811af51f1d9a1 Mon Sep 17 00:00:00 2001 From: Liron Date: Wed, 21 May 2025 14:10:28 +0300 Subject: [PATCH 5/5] fix lint errors --- .github/workflows/test.yml | 3 ++- examples/basic.py | 2 +- tapsdk/tap.py | 1 - tests/test_inputmodes.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 562e48d..5e54ff5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,7 @@ jobs: pip install -e .[dev] - name: Lint with flake8 run: | - flake8 *.py + pip install flake8 + flake8 examples tapsdk tests - name: Run tests run: pytest -v diff --git a/examples/basic.py b/examples/basic.py index 9c6183d..4301472 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -53,7 +53,7 @@ async def run(loop): print("Force Mouse Mode for 5 seconds") await client.set_input_type(InputType.MOUSE) await asyncio.sleep(5) - + print("Force keyboard Mode for 5 seconds") await client.set_input_type(InputType.KEYBOARD) await asyncio.sleep(5) diff --git a/tapsdk/tap.py b/tapsdk/tap.py index 0c2297a..c1d681c 100644 --- a/tapsdk/tap.py +++ b/tapsdk/tap.py @@ -232,7 +232,6 @@ async def _refresh_input_mode(self): async def _write_input_mode(self, value): await self.client.write_gatt_char(tap_mode_characteristic, value) - async def run(self): stop_event = asyncio.Event() devices = [] diff --git a/tests/test_inputmodes.py b/tests/test_inputmodes.py index 18c2ce1..1bedc78 100644 --- a/tests/test_inputmodes.py +++ b/tests/test_inputmodes.py @@ -1,4 +1,3 @@ -import pytest from tapsdk.inputmodes import TapInputMode, input_type_command from tapsdk.enumerations import InputType