Files
DeeJanuz--frametop/hands/rec/tests/test_qml_backend.py
DeeJanuzandClaude Opus 5.5 2351cec310 hand recorder: final consent text (2026-10-03), residency check, installer
Consent 2026-10-03, after a non-lawyer review: who runs this and how to reach
them, the dataset is public (Hugging Face, possibly abroad), the Hugging Face
username shows next to the contributor id, purposes (no identification), safety,
the maintainer grant passes to whoever maintains Frametop next, withdrawal
before and after merge, rights such as the GDPR's, and what a new version means.
Residents of Illinois, Texas and Washington can't take part for now (biometric
privacy laws): a third checkbox, profile consent.region_ok, checked by
validate.py from this consent version on. The DRAFT banners are gone, so uploads
no longer need FT_HANDREC_ALLOW_UPLOAD.

hands/rec/install.sh installs the recorder on a Frame with Frametop: container
packages, hand tracking and panel builds, ft-camd's capabilities, menu entry.

test_qml_backend also checks each call's argument count against the slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:35:18 -06:00

71 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""main.qml against ft_handrec.Backend: every backend.name(...) the window calls is a slot, and
every backend.name it reads is a property or a slot. A method that lost its @Slot shows up in
QML only as "is not a function" when its button is pressed (2026-10-03: Export did nothing).
Needs PySide6 (the dev container); skipped without it.
python3 hands/rec/tests/test_qml_backend.py
"""
import os
import re
import sys
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
REC = os.path.dirname(HERE)
sys.path.insert(0, REC)
class QmlBackendTest(unittest.TestCase):
def setUp(self):
try:
import ft_handrec
except ImportError as e:
self.skipTest(f"no PySide6: {e}")
meta = self.meta = ft_handrec.Backend.staticMetaObject
self.slots = {bytes(meta.method(i).name()).decode() for i in range(meta.methodCount())}
self.props = {meta.property(i).name() for i in range(meta.propertyCount())}
with open(os.path.join(REC, "main.qml")) as f:
self.qml = f.read()
def test_calls_are_slots(self):
called = set(re.findall(r"\bbackend\.(\w+)\s*\(", self.qml))
self.assertTrue(called)
self.assertEqual(sorted(called - self.slots), [], "called from main.qml but not a slot")
def test_call_arity(self):
"""Each backend.name(a, b) call passes as many arguments as some slot of that name takes
(a decorator left at the old count fails only when the button is pressed)."""
arity = {}
meta = self.meta
for i in range(meta.methodCount()):
mm = meta.method(i)
arity.setdefault(bytes(mm.name()).decode(), set()).add(mm.parameterCount())
bad = []
for m in re.finditer(r"\bbackend\.(\w+)\s*\(", self.qml):
depth, args, k, seen = 1, 0, m.end(), False
while depth and k < len(self.qml):
c = self.qml[k]
if c in "([{":
depth += 1
elif c in ")]}":
depth -= 1
elif c == "," and depth == 1:
args += 1
elif not c.isspace() and depth >= 1:
seen = True
k += 1
n = args + 1 if seen else 0
if m.group(1) in arity and n not in arity[m.group(1)]:
bad.append("%s: %d arguments, slots take %s" % (m.group(1), n, sorted(arity[m.group(1)])))
self.assertEqual(bad, [])
def test_reads_exist(self):
read = set(re.findall(r"\bbackend\.(\w+)\b(?!\s*\()", self.qml))
self.assertTrue(read)
self.assertEqual(sorted(read - self.props - self.slots), [], "read in main.qml but not on Backend")
if __name__ == "__main__":
unittest.main()