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 new file mode 100644 index 0000000..5e54ff5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["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 -e .[dev] + - name: Lint with flake8 + run: | + pip install flake8 + flake8 examples tapsdk tests + - name: Run tests + run: pytest -v diff --git a/Readme.md b/Readme.md index 8160d54..d36fc30 100644 --- a/Readme.md +++ b/Readme.md @@ -241,6 +241,21 @@ The dynamic range of the sensors is determined with the ```set_input_mode``` met You can find some examples in the [examples folder](examples). +### Testing + +To run the tests, first install the development dependencies: + +```bash +pip install .[dev] +``` + +Then run the tests using pytest: + +```bash +pytest +``` + + ### Known Issues An up-to-date list of known issues is available [here](History.md). 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/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/setup.py b/setup.py index 98d9dd0..3ed784b 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", "flake8"] + }, # classifiers=[ # # Trove classifiers # # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 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/__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..1bedc78 --- /dev/null +++ b/tests/test_inputmodes.py @@ -0,0 +1,17 @@ +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..4a189c6 --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,55 @@ +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(): + # 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} + ]