unit tests for execution in desktop dev environment
This commit is contained in:
parent
e70ce5f431
commit
b6201d43d4
@ -76,6 +76,13 @@ found in `setup.cfg` loosening the rules in isolated cases, notably
|
|||||||
`user_keymaps` (which is *also* not subject to Black formatting for reasons
|
`user_keymaps` (which is *also* not subject to Black formatting for reasons
|
||||||
documented in `pyproject.toml`).
|
documented in `pyproject.toml`).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Unit tests within the `tests` folder mock various CicuitPython modules to allow
|
||||||
|
them to be executed in a desktop development environment.
|
||||||
|
|
||||||
|
Execute tests using the command `python -m unittest`.
|
||||||
|
|
||||||
## License, Copyright, and Legal
|
## License, Copyright, and Legal
|
||||||
|
|
||||||
All software in this repository is licensed under the [GNU Public License,
|
All software in this repository is licensed under the [GNU Public License,
|
||||||
|
@ -53,6 +53,7 @@ class AbstractHID:
|
|||||||
REPORT_BYTES = 8
|
REPORT_BYTES = 8
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self._prev_evt = bytearray(self.REPORT_BYTES)
|
||||||
self._evt = bytearray(self.REPORT_BYTES)
|
self._evt = bytearray(self.REPORT_BYTES)
|
||||||
self.report_device = memoryview(self._evt)[0:1]
|
self.report_device = memoryview(self._evt)[0:1]
|
||||||
self.report_device[0] = HIDReportTypes.KEYBOARD
|
self.report_device[0] = HIDReportTypes.KEYBOARD
|
||||||
@ -125,7 +126,10 @@ class AbstractHID:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
self.hid_send(self._evt)
|
changed = not self._evt.startswith(self._prev_evt)
|
||||||
|
if changed:
|
||||||
|
self._prev_evt[:] = self._evt
|
||||||
|
self.hid_send(self._evt)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -373,6 +373,11 @@ class KMKKeyboard:
|
|||||||
print('Failed to run post hid function in extension: ', err, ext)
|
print('Failed to run post hid function in extension: ', err, ext)
|
||||||
|
|
||||||
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
|
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
|
||||||
|
self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
|
||||||
|
while True:
|
||||||
|
self._main_loop()
|
||||||
|
|
||||||
|
def _init(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
|
||||||
self._go_args = kwargs
|
self._go_args = kwargs
|
||||||
self.hid_type = hid_type
|
self.hid_type = hid_type
|
||||||
self.secondary_hid_type = secondary_hid_type
|
self.secondary_hid_type = secondary_hid_type
|
||||||
@ -398,46 +403,44 @@ class KMKKeyboard:
|
|||||||
|
|
||||||
self._print_debug_cycle(init=True)
|
self._print_debug_cycle(init=True)
|
||||||
|
|
||||||
while True:
|
def _main_loop(self):
|
||||||
self.current_key = None
|
self.current_key = None
|
||||||
self.state_changed = False
|
self.state_changed = False
|
||||||
self.sandbox.active_layers = self.active_layers.copy()
|
self.sandbox.active_layers = self.active_layers.copy()
|
||||||
|
|
||||||
self.before_matrix_scan()
|
self.before_matrix_scan()
|
||||||
|
|
||||||
self.matrix_update = (
|
self.matrix_update = self.sandbox.matrix_update = self.matrix.scan_for_changes()
|
||||||
self.sandbox.matrix_update
|
self.sandbox.secondary_matrix_update = self.secondary_matrix_update
|
||||||
) = self.matrix.scan_for_changes()
|
|
||||||
self.sandbox.secondary_matrix_update = self.secondary_matrix_update
|
|
||||||
|
|
||||||
self.after_matrix_scan()
|
self.after_matrix_scan()
|
||||||
|
|
||||||
self._handle_matrix_report(self.secondary_matrix_update)
|
self._handle_matrix_report(self.secondary_matrix_update)
|
||||||
self.secondary_matrix_update = None
|
self.secondary_matrix_update = None
|
||||||
self._handle_matrix_report(self.matrix_update)
|
self._handle_matrix_report(self.matrix_update)
|
||||||
self.matrix_update = None
|
self.matrix_update = None
|
||||||
|
|
||||||
self.before_hid_send()
|
self.before_hid_send()
|
||||||
|
|
||||||
|
if self.hid_pending:
|
||||||
|
self._send_hid()
|
||||||
|
|
||||||
|
self._old_timeouts_len = len(self._timeouts)
|
||||||
|
self._process_timeouts()
|
||||||
|
self._new_timeouts_len = len(self._timeouts)
|
||||||
|
|
||||||
|
if self._old_timeouts_len != self._new_timeouts_len:
|
||||||
|
self.state_changed = True
|
||||||
if self.hid_pending:
|
if self.hid_pending:
|
||||||
self._send_hid()
|
self._send_hid()
|
||||||
|
|
||||||
self._old_timeouts_len = len(self._timeouts)
|
self.after_hid_send()
|
||||||
self._process_timeouts()
|
|
||||||
self._new_timeouts_len = len(self._timeouts)
|
|
||||||
|
|
||||||
if self._old_timeouts_len != self._new_timeouts_len:
|
if self._trigger_powersave_enable:
|
||||||
self.state_changed = True
|
self.powersave_enable()
|
||||||
if self.hid_pending:
|
|
||||||
self._send_hid()
|
|
||||||
|
|
||||||
self.after_hid_send()
|
if self._trigger_powersave_disable:
|
||||||
|
self.powersave_disable()
|
||||||
|
|
||||||
if self._trigger_powersave_enable:
|
if self.state_changed:
|
||||||
self.powersave_enable()
|
self._print_debug_cycle()
|
||||||
|
|
||||||
if self._trigger_powersave_disable:
|
|
||||||
self.powersave_disable()
|
|
||||||
|
|
||||||
if self.state_changed:
|
|
||||||
self._print_debug_cycle()
|
|
||||||
|
@ -44,8 +44,7 @@ class HoldTap(Module):
|
|||||||
self.ht_activate_on_interrupt(
|
self.ht_activate_on_interrupt(
|
||||||
key, keyboard, *state.args, **state.kwargs
|
key, keyboard, *state.args, **state.kwargs
|
||||||
)
|
)
|
||||||
if keyboard.hid_pending:
|
keyboard._send_hid()
|
||||||
keyboard._send_hid()
|
|
||||||
return current_key
|
return current_key
|
||||||
|
|
||||||
def before_hid_send(self, keyboard):
|
def before_hid_send(self, keyboard):
|
||||||
|
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from tests.mocks import init_circuit_python_modules_mocks
|
||||||
|
|
||||||
|
init_circuit_python_modules_mocks()
|
94
tests/keyboard_test.py
Normal file
94
tests/keyboard_test.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import random
|
||||||
|
import time
|
||||||
|
from functools import reduce
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from kmk.hid import HIDModes
|
||||||
|
from kmk.keys import ModifierKey
|
||||||
|
from kmk.kmk_keyboard import KMKKeyboard
|
||||||
|
from kmk.matrix import DiodeOrientation
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalInOut(Mock):
|
||||||
|
value = False
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardTest:
|
||||||
|
def __init__(
|
||||||
|
self, modules, keymap, keyboard_debug_enabled=False, debug_enabled=False
|
||||||
|
):
|
||||||
|
self.debug_enabled = debug_enabled
|
||||||
|
|
||||||
|
self.keyboard = KMKKeyboard()
|
||||||
|
self.keyboard.debug_enabled = keyboard_debug_enabled
|
||||||
|
|
||||||
|
self.keyboard.modules = modules
|
||||||
|
|
||||||
|
self.pins = tuple(DigitalInOut() for k in keymap[0])
|
||||||
|
|
||||||
|
self.keyboard.col_pins = (DigitalInOut(),)
|
||||||
|
self.keyboard.row_pins = self.pins
|
||||||
|
self.keyboard.diode_orientation = DiodeOrientation.COL2ROW
|
||||||
|
self.keyboard.keymap = keymap
|
||||||
|
|
||||||
|
self.keyboard._init(hid_type=HIDModes.NOOP)
|
||||||
|
|
||||||
|
@patch('kmk.hid.AbstractHID.hid_send')
|
||||||
|
def test(self, testname, key_events, assert_hid_reports, hid_send):
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(testname, key_events, assert_hid_reports)
|
||||||
|
|
||||||
|
hid_send_call_arg_list = []
|
||||||
|
hid_send.side_effect = lambda hid_report: hid_send_call_arg_list.append(
|
||||||
|
hid_report[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
for e in key_events:
|
||||||
|
if isinstance(e, int):
|
||||||
|
starttime_ms = time.time_ns() // 1_000_000
|
||||||
|
while time.time_ns() // 1_000_000 - starttime_ms < e:
|
||||||
|
self.do_main_loop()
|
||||||
|
else:
|
||||||
|
key_pos = e[0]
|
||||||
|
is_pressed = e[1]
|
||||||
|
self.pins[key_pos].value = is_pressed
|
||||||
|
self.do_main_loop()
|
||||||
|
|
||||||
|
if self.debug_enabled:
|
||||||
|
for hid_report in hid_send_call_arg_list:
|
||||||
|
print(hid_report)
|
||||||
|
|
||||||
|
for i, hid_report in enumerate(
|
||||||
|
hid_send_call_arg_list[-len(assert_hid_reports) :]
|
||||||
|
):
|
||||||
|
hid_report_keys = {code for code in hid_report[2:] if code != 0}
|
||||||
|
assert_keys = {
|
||||||
|
k.code for k in assert_hid_reports[i] if not isinstance(k, ModifierKey)
|
||||||
|
}
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(
|
||||||
|
'assert keys:',
|
||||||
|
hid_report_keys == assert_keys,
|
||||||
|
hid_report_keys,
|
||||||
|
assert_keys,
|
||||||
|
)
|
||||||
|
assert hid_report_keys == assert_keys
|
||||||
|
|
||||||
|
hid_report_modifiers = hid_report[0]
|
||||||
|
assert_modifiers = reduce(
|
||||||
|
lambda mod, all_mods: all_mods | mod,
|
||||||
|
{k.code for k in assert_hid_reports[i] if isinstance(k, ModifierKey)},
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(
|
||||||
|
'assert mods:',
|
||||||
|
hid_report_modifiers == assert_modifiers,
|
||||||
|
hid_report_modifiers,
|
||||||
|
assert_modifiers,
|
||||||
|
)
|
||||||
|
assert hid_report_modifiers == assert_modifiers
|
||||||
|
|
||||||
|
def do_main_loop(self):
|
||||||
|
for i in range(random.randint(5, 50)):
|
||||||
|
self.keyboard._main_loop()
|
20
tests/mocks.py
Normal file
20
tests/mocks.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
|
||||||
|
def init_circuit_python_modules_mocks():
|
||||||
|
sys.modules['usb_hid'] = Mock()
|
||||||
|
sys.modules['digitalio'] = Mock()
|
||||||
|
sys.modules['neopixel'] = Mock()
|
||||||
|
sys.modules['pulseio'] = Mock()
|
||||||
|
sys.modules['busio'] = Mock()
|
||||||
|
sys.modules['microcontroller'] = Mock()
|
||||||
|
sys.modules['board'] = Mock()
|
||||||
|
sys.modules['storage'] = Mock()
|
||||||
|
|
||||||
|
sys.modules['micropython'] = Mock()
|
||||||
|
sys.modules['micropython'].const = lambda x: x
|
||||||
|
|
||||||
|
sys.modules['supervisor'] = Mock()
|
||||||
|
sys.modules['supervisor'].ticks_ms = lambda: time.time_ns() // 1_000_000
|
106
tests/test_hold_tap.py
Normal file
106
tests/test_hold_tap.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from kmk.keys import KC
|
||||||
|
from kmk.modules.layers import Layers
|
||||||
|
from kmk.modules.modtap import ModTap
|
||||||
|
from tests.keyboard_test import KeyboardTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestHoldTap(unittest.TestCase):
|
||||||
|
def test_basic_kmk_keyboard(self):
|
||||||
|
keyboard = KeyboardTest(
|
||||||
|
[Layers(), ModTap()],
|
||||||
|
[
|
||||||
|
[KC.MT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D],
|
||||||
|
[KC.N1, KC.N2, KC.N3, KC.N4],
|
||||||
|
],
|
||||||
|
debug_enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test('MT tap behaviour', [(0, True), 100, (0, False)], [{KC.A}, {}])
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT hold behaviour', [(0, True), 350, (0, False)], [{KC.LCTL}, {}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO test multiple mods being held
|
||||||
|
|
||||||
|
# MT
|
||||||
|
keyboard.test(
|
||||||
|
'MT within tap time sequential -> tap behavior',
|
||||||
|
[(0, True), 100, (0, False), (3, True), (3, False)],
|
||||||
|
[{KC.A}, {}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT within tap time rolling -> tap behavior',
|
||||||
|
[(0, True), 100, (3, True), 250, (0, False), (3, False)],
|
||||||
|
[{KC.A}, {KC.A, KC.D}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT within tap time nested -> tap behavior',
|
||||||
|
[(0, True), 100, (3, True), (3, False), 250, (0, False)],
|
||||||
|
[{KC.A}, {KC.A, KC.D}, {KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT after tap time sequential -> hold behavior',
|
||||||
|
[(0, True), 350, (0, False), (3, True), (3, False)],
|
||||||
|
[{KC.LCTL}, {}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT after tap time rolling -> hold behavior',
|
||||||
|
[(0, True), 350, (3, True), (0, False), (3, False)],
|
||||||
|
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'MT after tap time nested -> hold behavior',
|
||||||
|
[(0, True), 350, (3, True), (3, False), (0, False)],
|
||||||
|
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# LT
|
||||||
|
keyboard.test(
|
||||||
|
'LT within tap time sequential -> tap behavior',
|
||||||
|
[(1, True), 100, (1, False), (3, True), (3, False)],
|
||||||
|
[{KC.B}, {}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'LT within tap time rolling -> tap behavior',
|
||||||
|
[(1, True), 100, (3, True), 250, (1, False), (3, False)],
|
||||||
|
[{KC.B}, {KC.B, KC.D}, {KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'LT within tap time nested -> tap behavior',
|
||||||
|
[(1, True), 100, (3, True), (3, False), 250, (1, False)],
|
||||||
|
[{KC.B}, {KC.B, KC.D}, {KC.B}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'LT after tap time sequential -> hold behavior',
|
||||||
|
[(1, True), 350, (1, False), (3, True), (3, False)],
|
||||||
|
[{KC.D}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'LT after tap time rolling -> hold behavior',
|
||||||
|
[(1, True), 350, (3, True), (1, False), (3, False)],
|
||||||
|
[{KC.N4}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'LT after tap time nested -> hold behavior',
|
||||||
|
[(1, True), 350, (3, True), (3, False), (1, False)],
|
||||||
|
[{KC.N4}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO test TT
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
15
tests/test_kmk_keyboard.py
Normal file
15
tests/test_kmk_keyboard.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from kmk.keys import KC
|
||||||
|
from tests.keyboard_test import KeyboardTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestKmkKeyboard(unittest.TestCase):
|
||||||
|
def test_basic_kmk_keyboard(self):
|
||||||
|
keyboard = KeyboardTest([], [[KC.N1, KC.N2, KC.N3, KC.N4]])
|
||||||
|
|
||||||
|
keyboard.test('Simple key press', [(0, True), (0, False)], [{KC.N1}, {}])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user