From 94130740c4c5f687f8fa18ee3fd33b2e73f724c7 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Sun, 23 Sep 2018 02:38:28 -0700 Subject: [PATCH] Write a basic keymap sanity checker utility --- .circleci/config.yml | 29 +++++++- Makefile | 14 +++- bin/keymap_sanity_check.py | 70 +++++++++++++++++++ micropython.sh => bin/micropython.sh | 0 kmk/entrypoints/handwire/Pipfile | 11 --- setup.cfg | 3 +- tests/test_data/__init__.py | 0 tests/test_data/keymaps/__init__.py | 0 tests/test_data/keymaps/duplicate_row_pins.py | 29 ++++++++ .../duplicated_pins_between_row_col.py | 29 ++++++++ tests/test_data/keymaps/ghosted_layer_mo.py | 29 ++++++++ tests/test_data/keymaps/known_good.py | 29 ++++++++ upy-unix-stubs/machine/__init__.py | 18 +++++ upy-unix-stubs/pyb/USB_HID.py | 0 upy-unix-stubs/pyb/__init__.py | 0 upy-unix-stubs/pyb/delay.py | 0 user_keymaps/__init__.py | 0 user_keymaps/kdb424/__init__.py | 0 user_keymaps/klardotsh/__init__.py | 0 19 files changed, 246 insertions(+), 15 deletions(-) create mode 100755 bin/keymap_sanity_check.py rename micropython.sh => bin/micropython.sh (100%) delete mode 100644 kmk/entrypoints/handwire/Pipfile create mode 100644 tests/test_data/__init__.py create mode 100644 tests/test_data/keymaps/__init__.py create mode 100644 tests/test_data/keymaps/duplicate_row_pins.py create mode 100644 tests/test_data/keymaps/duplicated_pins_between_row_col.py create mode 100644 tests/test_data/keymaps/ghosted_layer_mo.py create mode 100644 tests/test_data/keymaps/known_good.py create mode 100644 upy-unix-stubs/machine/__init__.py create mode 100644 upy-unix-stubs/pyb/USB_HID.py create mode 100644 upy-unix-stubs/pyb/__init__.py create mode 100644 upy-unix-stubs/pyb/delay.py create mode 100644 user_keymaps/__init__.py create mode 100644 user_keymaps/kdb424/__init__.py create mode 100644 user_keymaps/klardotsh/__init__.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 546765c..2763f14 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,6 +22,23 @@ jobs: paths: - .venv + test: + docker: + - image: 'python:3.7' + + environment: + KMK_TEST: 1 + PIPENV_VENV_IN_PROJECT: 1 + + steps: + - checkout + - restore_cache: + keys: + - v1-kmk-venv-{{ checksum "Pipfile.lock" }} + + - run: pip install pipenv==2018.7.1 + - run: make test + build_pyboard: docker: - image: 'python:3.7' @@ -70,6 +87,14 @@ workflows: only: /.*/ tags: only: /.*/ + - test: + filters: + branches: + only: /.*/ + tags: + only: /.*/ + requires: + - lint - build_pyboard: filters: branches: @@ -77,7 +102,7 @@ workflows: tags: only: /.*/ requires: - - lint + - test - build_teensy_31: filters: branches: @@ -85,4 +110,4 @@ workflows: tags: only: /.*/ requires: - - lint + - test diff --git a/Makefile b/Makefile index 16def45..7f3e1dd 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,18 @@ lint: devdeps fix-isort: devdeps @find kmk/ user_keymaps/ -name "*.py" | xargs pipenv run isort +test: micropython-build-unix + @echo "===> Testing keymap_sanity_check.py script" + @echo " --> Known good layout should pass..." + @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/known_good.py + @echo " --> Layer with ghosted MO should fail..." + @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/ghosted_layer_mo.py 2>/dev/null && exit 1 || exit 0 + @echo " --> Sharing a pin between rows/cols should fail..." + @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/duplicated_pins_between_row_col.py 2>/dev/null && exit 1 || exit 0 + @echo " --> Sharing a pin between two rows should fail..." + @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/duplicate_row_pins.py 2>/dev/null && exit 1 || exit 0 + @echo "===> The sanity checker is sane, unlike klardotsh" + .submodules: .gitmodules @echo "===> Pulling dependencies, this may take several minutes" @git submodule update --init --recursive @@ -41,7 +53,7 @@ circuitpy-deps: .circuitpy-deps micropython-deps: .micropython-deps -vendor/micropython/ports/unix/micropython: vendor/micropython/ports/unix/modules/.kmk_frozen +vendor/micropython/ports/unix/micropython: micropython-deps vendor/micropython/ports/unix/modules/.kmk_frozen @make -j4 -C vendor/micropython/ports/unix micropython-build-unix: vendor/micropython/ports/unix/micropython diff --git a/bin/keymap_sanity_check.py b/bin/keymap_sanity_check.py new file mode 100755 index 0000000..4ec5139 --- /dev/null +++ b/bin/keymap_sanity_check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env micropython + +import sys + +import uos + +from kmk.common.keycodes import Keycodes + +if len(sys.argv) < 2: + print('Must provide a keymap to test as first argument', file=sys.stderr) + sys.exit(200) + +user_keymap_file = sys.argv[1] + +if user_keymap_file.endswith('.py'): + user_keymap_file = user_keymap_file[:-3] + +# Before we can import the user's keymap, we need to wrangle sys.path to +# add our stub modules. Before we can do THAT, we have to figure out where +# we actually are, and that's not the most trivial thing in MicroPython! +# +# The hack here is to see if we can find ourselves in whatever uPy thinks +# the current directory is. If we can, we need to head up a level. Obviously, +# if the layout of the KMK repo ever changes, this script will need updated +# or all hell will break loose. + +# First, hack around https://github.com/micropython/micropython/issues/2322, +# where frozen modules aren't available if MicroPython is running a script +# rather than via REPL +sys.path.insert(0, '') + +if any(fname == 'keymap_sanity_check.py' for fname, _, _ in uos.ilistdir()): + sys.path.extend(('../', '../upy-unix-stubs/')) +else: + sys.path.extend(('./', './upy-unix-stubs')) + +user_keymap = __import__(user_keymap_file) + +if hasattr(user_keymap, 'cols') or hasattr(user_keymap, 'rows'): + assert hasattr(user_keymap, 'cols'), 'Handwired keyboards must have both rows and cols defined' + assert hasattr(user_keymap, 'rows'), 'Handwired keyboards must have both rows and cols defined' + + # Ensure that no pins are duplicated in a handwire config + # This is the same check done in the MatrixScanners, relying + # on the __repr__ of the objects to be unique (because generally, + # Pin objects themselves are not hashable) + assert len(user_keymap.cols) == len({p for p in user_keymap.cols}), \ + 'Cannot use a single pin for multiple columns' + assert len(user_keymap.rows) == len({p for p in user_keymap.rows}), \ + 'Cannot use a single pin for multiple rows' + + unique_pins = {repr(c) for c in user_keymap.cols} | {repr(r) for r in user_keymap.rows} + assert len(unique_pins) == len(user_keymap.cols) + len(user_keymap.rows), \ + 'Cannot use a pin as both a column and row' + +assert hasattr(user_keymap, 'keymap'), 'Must define a keymap array' +assert len(user_keymap.keymap), 'Keymap must contain at least one layer' + +for lidx, layer in enumerate(user_keymap.keymap): + assert len(layer), 'Layer {} must contain at least one row'.format(lidx) + assert all(len(row) for row in layer), 'Layer {} must not contain empty rows'.format(lidx) + assert all(len(row) == len(layer[0]) for row in user_keymap.keymap), \ + 'All rows in layer {} must be of the same length'.format(lidx) + + for ridx, row in enumerate(layer): + for cidx, key in enumerate(row): + if key.code == Keycodes.Layers._KC_MO: + assert user_keymap.keymap[key.layer][ridx][cidx] == Keycodes.KMK.KC_TRNS, \ + ('The physical key used for MO layer switching must be KC_TRNS on the ' + 'target layer or you will get stuck on that layer.') diff --git a/micropython.sh b/bin/micropython.sh similarity index 100% rename from micropython.sh rename to bin/micropython.sh diff --git a/kmk/entrypoints/handwire/Pipfile b/kmk/entrypoints/handwire/Pipfile deleted file mode 100644 index b9ba84f..0000000 --- a/kmk/entrypoints/handwire/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.7" diff --git a/setup.cfg b/setup.cfg index 9699245..48a6b8c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ max_line_length = 99 ignore = X100 per-file-ignores = user_keymaps/**/*.py: F401,E501 + tests/test_data/keymaps/**/*.py: F401,E501 [isort] -known_third_party = analogio,bitbangio,bleio,board,busio,digitalio,framebuf,gamepad,gc,microcontroller,micropython,pulseio,pyb,pydux,uio,ubluepy,machine,pyb +known_third_party = analogio,bitbangio,bleio,board,busio,digitalio,framebuf,gamepad,gc,microcontroller,micropython,pulseio,pyb,pydux,uio,ubluepy,machine,pyb,uos diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/keymaps/__init__.py b/tests/test_data/keymaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/keymaps/duplicate_row_pins.py b/tests/test_data/keymaps/duplicate_row_pins.py new file mode 100644 index 0000000..aea8f23 --- /dev/null +++ b/tests/test_data/keymaps/duplicate_row_pins.py @@ -0,0 +1,29 @@ +import machine + +from kmk.common.consts import DiodeOrientation +from kmk.common.keycodes import KC +from kmk.entrypoints.handwire.pyboard import main + +p = machine.Pin.board +cols = (p.X10, p.X10, p.X12) +rows = (p.X1, p.X2, p.X3) + +diode_orientation = DiodeOrientation.COLUMNS + +keymap = [ + [ + [KC.MO(1), KC.H, KC.RESET], + [KC.MO(2), KC.I, KC.ENTER], + [KC.LCTRL, KC.SPACE, KC.LSHIFT], + ], + [ + [KC.TRNS, KC.B, KC.C], + [KC.NO, KC.D, KC.E], + [KC.F, KC.G, KC.H], + ], + [ + [KC.X, KC.Y, KC.Z], + [KC.TRNS, KC.N, KC.O], + [KC.R, KC.P, KC.Q], + ], +] diff --git a/tests/test_data/keymaps/duplicated_pins_between_row_col.py b/tests/test_data/keymaps/duplicated_pins_between_row_col.py new file mode 100644 index 0000000..25624e6 --- /dev/null +++ b/tests/test_data/keymaps/duplicated_pins_between_row_col.py @@ -0,0 +1,29 @@ +import machine + +from kmk.common.consts import DiodeOrientation +from kmk.common.keycodes import KC +from kmk.entrypoints.handwire.pyboard import main + +p = machine.Pin.board +cols = (p.X10, p.X11, p.X12) +rows = (p.X1, p.X11, p.X3) + +diode_orientation = DiodeOrientation.COLUMNS + +keymap = [ + [ + [KC.MO(1), KC.H, KC.RESET], + [KC.MO(2), KC.I, KC.ENTER], + [KC.LCTRL, KC.SPACE, KC.LSHIFT], + ], + [ + [KC.TRNS, KC.B, KC.C], + [KC.NO, KC.D, KC.E], + [KC.F, KC.G, KC.H], + ], + [ + [KC.X, KC.Y, KC.Z], + [KC.TRNS, KC.N, KC.O], + [KC.R, KC.P, KC.Q], + ], +] diff --git a/tests/test_data/keymaps/ghosted_layer_mo.py b/tests/test_data/keymaps/ghosted_layer_mo.py new file mode 100644 index 0000000..658f838 --- /dev/null +++ b/tests/test_data/keymaps/ghosted_layer_mo.py @@ -0,0 +1,29 @@ +import machine + +from kmk.common.consts import DiodeOrientation +from kmk.common.keycodes import KC +from kmk.entrypoints.handwire.pyboard import main + +p = machine.Pin.board +cols = (p.X10, p.X11, p.X12) +rows = (p.X1, p.X2, p.X3) + +diode_orientation = DiodeOrientation.COLUMNS + +keymap = [ + [ + [KC.MO(1), KC.H, KC.RESET], + [KC.MO(2), KC.I, KC.ENTER], + [KC.LCTRL, KC.SPACE, KC.LSHIFT], + ], + [ + [KC.A, KC.B, KC.C], + [KC.NO, KC.D, KC.E], + [KC.F, KC.G, KC.H], + ], + [ + [KC.X, KC.Y, KC.Z], + [KC.TRNS, KC.N, KC.O], + [KC.R, KC.P, KC.Q], + ], +] diff --git a/tests/test_data/keymaps/known_good.py b/tests/test_data/keymaps/known_good.py new file mode 100644 index 0000000..f95d0db --- /dev/null +++ b/tests/test_data/keymaps/known_good.py @@ -0,0 +1,29 @@ +import machine + +from kmk.common.consts import DiodeOrientation +from kmk.common.keycodes import KC +from kmk.entrypoints.handwire.pyboard import main + +p = machine.Pin.board +cols = (p.X10, p.X11, p.X12) +rows = (p.X1, p.X2, p.X3) + +diode_orientation = DiodeOrientation.COLUMNS + +keymap = [ + [ + [KC.MO(1), KC.H, KC.RESET], + [KC.MO(2), KC.I, KC.ENTER], + [KC.LCTRL, KC.SPACE, KC.LSHIFT], + ], + [ + [KC.TRNS, KC.B, KC.C], + [KC.NO, KC.D, KC.E], + [KC.F, KC.G, KC.H], + ], + [ + [KC.X, KC.Y, KC.Z], + [KC.TRNS, KC.N, KC.O], + [KC.R, KC.P, KC.Q], + ], +] diff --git a/upy-unix-stubs/machine/__init__.py b/upy-unix-stubs/machine/__init__.py new file mode 100644 index 0000000..ecf74c1 --- /dev/null +++ b/upy-unix-stubs/machine/__init__.py @@ -0,0 +1,18 @@ +class Anything: + ''' + A stub class which will repr as a provided name + ''' + def __init__(self, name): + self.name = name + + def __repr__(self): + return 'Anything<{}>'.format(self.name) + + +class Passthrough: + def __getattr__(self, attr): + return Anything(attr) + + +class Pin: + board = Passthrough() diff --git a/upy-unix-stubs/pyb/USB_HID.py b/upy-unix-stubs/pyb/USB_HID.py new file mode 100644 index 0000000..e69de29 diff --git a/upy-unix-stubs/pyb/__init__.py b/upy-unix-stubs/pyb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/upy-unix-stubs/pyb/delay.py b/upy-unix-stubs/pyb/delay.py new file mode 100644 index 0000000..e69de29 diff --git a/user_keymaps/__init__.py b/user_keymaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user_keymaps/kdb424/__init__.py b/user_keymaps/kdb424/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user_keymaps/klardotsh/__init__.py b/user_keymaps/klardotsh/__init__.py new file mode 100644 index 0000000..e69de29