Files
Frametop--frametop/hands/tests/test_handsmode.py
T
DeeJanuzandClaude Opus 5.5 213207309d handsmode: the three hand input switches, read and written apart
methods_of reads gaze clicks, the hand ray and direct touch from
frametop.conf as they work today; method_writes toggles one and pins
the others' keys so they don't move with POINTER_HANDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:47:34 -06:00

276 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""Tests for hands/handsmode.py: HANDS_MODE and HANDS_MATTE_MARGIN in a temporary frametop.conf, the
Hands page's three input switches (methods_of, method_writes), and which units each mode starts,
stops and makes SteamVR want, with a made-up systemd (nothing here runs systemctl, getcap or pgrep).
python3 hands/tests/test_handsmode.py
"""
import os
import shutil
import sys
import tempfile
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
import handsmode as hm # noqa: E402
CAMD, HANDS, VIEW = hm.TRACKING + (hm.VIEW,)
CUT_CAMD, CUT_HANDS = hm.CUTOUTS
BIN = "/home/u/frametop/hands/build/ft-camd"
class FakeHost:
"""systemd as the tests set it up: loaded units, active ones, SteamVR's wants."""
def __init__(self, loaded=(CAMD, HANDS), active=(), steamvr=True, desktop=True, caps=True, built=True,
wanted=()):
self.loaded, self.active, self.wanted_units = set(loaded), set(active), set(wanted)
self.steamvr, self.desktop, self.has_caps, self.built = steamvr, desktop, caps, built
self.calls = []
def units(self, names):
out = {}
for u in names:
on = u in self.active or (u == hm.STEAMVR and self.steamvr)
out[u] = {"load": "loaded" if u in self.loaded or u == hm.STEAMVR or (u in hm.CUTOUTS and on)
else "not-found", "active": "active" if on else "inactive"}
return out
def exec_path(self, unit):
return BIN if unit == CAMD and CAMD in self.loaded else ""
def wanted(self, unit):
return unit in self.wanted_units
def exists(self, path):
return self.built
def caps(self, path):
return self.has_caps
def desktop_up(self):
return self.desktop
def cameras(self):
return "ok"
def systemctl(self, *args):
self.calls.append(list(args))
def plan(mode, host, desktop=None, strict=False):
return hm.plan(mode, hm.status(host, {"HANDS_MODE": mode}, cameras=False), desktop, strict)
class ConfTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp(prefix="handsmode-test-")
self.conf = os.path.join(self.dir, "frametop.conf")
def tearDown(self):
shutil.rmtree(self.dir)
def test_mode_default_and_bad_values(self):
self.assertEqual(hm.mode_of({}), "off")
self.assertEqual(hm.mode_of({"HANDS_MODE": "sometimes"}), "off")
for m in hm.MODES:
self.assertEqual(hm.mode_of({"HANDS_MODE": m}), m)
def test_margin(self):
self.assertEqual(hm.margin_of({}), hm.MARGIN_DEFAULT)
self.assertEqual(hm.margin_of({"HANDS_MATTE_MARGIN": "0.75"}), 0.75)
self.assertEqual(hm.margin_of({"HANDS_MATTE_MARGIN": "big"}), hm.MARGIN_DEFAULT)
self.assertEqual(hm.margin_of({"HANDS_MATTE_MARGIN": "12"}), hm.MARGIN_DEFAULT) # ft-screens ignores it too
def test_write_keeps_comment_and_others(self):
with open(self.conf, "w") as f:
f.write("SCREENS=2\nHANDS_MODE=off # when hand tracking runs\n# HANDS_MODE=always\n")
hm.write_value("HANDS_MODE", "frametop", self.conf)
with open(self.conf) as f:
text = f.read()
self.assertIn("HANDS_MODE=frametop # when hand tracking runs\n", text)
self.assertIn("SCREENS=2\n", text)
self.assertIn("# HANDS_MODE=always\n", text) # a commented line isn't the setting
self.assertEqual(hm.read_conf(self.conf), {"SCREENS": "2", "HANDS_MODE": "frametop"})
def test_write_adds_missing_key_and_file(self):
hm.write_value("HANDS_MODE", "always", self.conf)
hm.write_value("HANDS_MATTE_MARGIN", "0.75", self.conf)
conf = hm.read_conf(self.conf)
self.assertEqual((hm.mode_of(conf), hm.margin_of(conf)), ("always", 0.75))
def test_quoted_values(self):
with open(self.conf, "w") as f:
f.write('HANDS_MODE="always"\n')
self.assertEqual(hm.mode_of(hm.read_conf(self.conf)), "always")
class PlanTest(unittest.TestCase):
def test_always_wants_and_starts(self):
h = FakeHost(loaded=(CAMD, HANDS, VIEW))
self.assertEqual(plan("always", h), [["add-wants", hm.STEAMVR, CAMD, HANDS, VIEW],
["start", "--no-block", CAMD, HANDS, VIEW]])
def test_always_without_steamvr_only_wants(self):
h = FakeHost(steamvr=False, wanted=(CAMD,))
self.assertEqual(plan("always", h, strict=True), [["add-wants", hm.STEAMVR, HANDS]])
def test_hand_view_not_installed_is_left_out(self):
h = FakeHost(loaded=(CAMD, HANDS))
self.assertFalse(any(VIEW in call for call in plan("always", h)))
def test_frametop_with_desktop(self):
h = FakeHost(wanted=(CAMD, HANDS))
self.assertEqual(plan("frametop", h), [["disable", CAMD, HANDS], ["start", "--no-block", CAMD, HANDS]])
def test_frametop_without_desktop(self):
h = FakeHost(desktop=False, active=(CAMD, HANDS))
self.assertEqual(plan("frametop", h), []) # an install or caps run leaves ft-handsctl's alone
self.assertEqual(plan("frametop", h, strict=True), [["stop", "--no-block", CAMD, HANDS]])
def test_desktop_hooks(self):
h = FakeHost(desktop=False) # the hook's word wins over pgrep (ft-screens still starting)
self.assertEqual(plan("frametop", h, desktop=True), [["start", "--no-block", CAMD, HANDS]])
h = FakeHost(desktop=True, active=(CAMD, HANDS)) # ft-screens still ending
self.assertEqual(plan("frametop", h, desktop=False), [["stop", "--no-block", CAMD, HANDS]])
def test_off_stops_everything_when_chosen(self):
h = FakeHost(loaded=(CAMD, HANDS, VIEW), active=(CAMD, HANDS, VIEW, CUT_CAMD), wanted=(CAMD, HANDS, VIEW))
self.assertEqual(plan("off", h, strict=True), [["disable", CAMD, HANDS, VIEW],
["stop", "--no-block", CAMD, HANDS, VIEW, CUT_CAMD]])
self.assertEqual(plan("off", h), [["disable", CAMD, HANDS, VIEW]])
def test_off_with_nothing_running(self):
self.assertEqual(plan("off", FakeHost(), strict=True), [])
def test_no_caps_starts_nothing(self):
h = FakeHost(caps=False)
self.assertEqual(plan("always", h), [["add-wants", hm.STEAMVR, CAMD, HANDS]])
self.assertEqual(plan("frametop", h), [])
def test_not_installed(self):
h = FakeHost(loaded=())
self.assertEqual(plan("always", h), [])
self.assertEqual(plan("frametop", h), [])
def test_apply_runs_the_plan(self):
h = FakeHost()
calls, _ = hm.apply(h, {"HANDS_MODE": "frametop"})
self.assertEqual(h.calls, calls)
self.assertEqual(calls, [["start", "--no-block", CAMD, HANDS]])
class StatusTest(unittest.TestCase):
def test_problems(self):
st = hm.status(FakeHost(loaded=()), {})
self.assertIn("hands/run.sh install", st["problem"])
st = hm.status(FakeHost(built=False), {})
self.assertIn("isn't built", st["problem"])
st = hm.status(FakeHost(caps=False), {"HANDS_MODE": "always"})
self.assertIn("/home/u/frametop/hands/run.sh caps", st["problem"])
self.assertEqual(st["summary"], "Hand tracking can't start with SteamVR")
self.assertEqual(hm.status(FakeHost(), {})["problem"], "")
def test_cameras(self):
h = FakeHost()
h.cameras = lambda: "degraded: upper cameras and IR light off (VCINT FPGA failed to load)"
self.assertIn("upper cameras", hm.status(h, {})["problem"])
h.steamvr = False # nothing to check without SteamVR
self.assertEqual(hm.status(h, {})["problem"], "")
def test_summary(self):
self.assertEqual(hm.status(FakeHost(), {})["summary"], "Hand tracking is off")
self.assertEqual(hm.status(FakeHost(active=(CAMD, HANDS)), {})["summary"],
"Hand tracking is running (started by hand: ft-handsctl)")
self.assertEqual(hm.status(FakeHost(active=(CAMD, HANDS)), {"HANDS_MODE": "frametop"})["summary"],
"Hand tracking is running")
self.assertEqual(hm.status(FakeHost(steamvr=False), {"HANDS_MODE": "frametop"})["summary"],
"Hand tracking starts with the Frametop desktop")
self.assertEqual(hm.status(FakeHost(active=(CUT_CAMD, CUT_HANDS)), {})["summary"],
"The hand cutouts are running (started by hand: ft-cutouts)")
class MethodsTest(unittest.TestCase):
"""The Hands page's three switches (gaze clicks, the ray, direct touch): read from old configs as
they always worked, and each toggled without moving the others."""
def apply(self, conf, method, on):
new = dict(conf, **hm.method_writes(conf, method, on))
return new, hm.methods_of(new)
def test_old_configs_read_as_before(self):
self.assertEqual(hm.methods_of({}), {"gaze_clicks": False, "ray": False, "touch": False})
on = {"gaze_clicks": True, "ray": True, "touch": True}
self.assertEqual(hm.methods_of({"POINTER_HANDS": "1"}), on)
self.assertEqual(hm.methods_of({"POINTER_HANDS": "1", "HANDS_NEAR": "1"}), on) # the example config's
self.assertEqual(hm.methods_of({"POINTER_HANDS": "1", "POINTER_HAND_RAY": "0"})["ray"], False)
self.assertEqual(hm.methods_of({"POINTER_HANDS": "1", "HANDS_NEAR": "0"})["touch"], False)
self.assertTrue(hm.methods_of({"HANDS_NEAR": "force"})["touch"])
self.assertFalse(hm.methods_of({"HANDS_NEAR": "1"})["touch"], "1 without POINTER_HANDS: off, as before")
self.assertTrue(hm.methods_of({"HANDS_NEAR": "on"})["touch"])
self.assertFalse(hm.methods_of({"POINTER_HANDS": "yes"})["ray"], "as the helper reads it: not a number, 0")
def test_each_switch_alone(self):
for method in hm.METHODS:
conf, got = self.apply({}, method, True)
self.assertEqual(got, {m: m == method for m in hm.METHODS}, f"{method} alone: {conf}")
conf, got = self.apply(conf, method, False)
self.assertEqual(got, {m: False for m in hm.METHODS}, f"{method} off again: {conf}")
def test_touch_alone_needs_no_pointer_hands(self):
conf, got = self.apply({}, "touch", True)
self.assertEqual(conf, {"HANDS_NEAR": "on"})
conf, got = self.apply({"POINTER_HANDS": "1"}, "gaze_clicks", False)
conf, got = self.apply(conf, "ray", False)
self.assertEqual(conf["POINTER_HANDS"], "0")
self.assertEqual(conf["HANDS_NEAR"], "on", "touch stays on without POINTER_HANDS")
self.assertTrue(got["touch"])
def test_turning_one_on_pins_the_others(self):
# all off, then the ray: POINTER_GAZE_PINCH's default (1) and HANDS_NEAR's (1) would turn the
# others on with POINTER_HANDS, so they're written off
conf, got = self.apply({}, "ray", True)
self.assertEqual(got, {"gaze_clicks": False, "ray": True, "touch": False})
self.assertEqual(conf, {"POINTER_HANDS": "1", "POINTER_GAZE_PINCH": "0", "HANDS_NEAR": "0"})
conf, got = self.apply(conf, "gaze_clicks", True)
self.assertEqual(got, {"gaze_clicks": True, "ray": True, "touch": False})
def test_minimal_writes(self):
conf = {"POINTER_HANDS": "1", "HANDS_NEAR": "1"}
self.assertEqual(hm.method_writes(conf, "ray", True), {}, "already on: nothing")
self.assertEqual(hm.method_writes(conf, "ray", False), {"POINTER_HAND_RAY": "0"})
self.assertEqual(hm.method_writes(conf, "touch", False), {"HANDS_NEAR": "0"})
self.assertEqual(hm.method_writes({"HANDS_NEAR": "force"}, "touch", True), {}, "force stays force")
def test_every_combination(self):
start = {"POINTER_HANDS": "1", "HANDS_NEAR": "1"}
for bits in range(8):
want = {m: bool(bits >> i & 1) for i, m in enumerate(hm.METHODS)}
conf = dict(start)
for m in hm.METHODS:
conf, _ = self.apply(conf, m, want[m])
self.assertEqual(hm.methods_of(conf), want, f"{want}: {conf}")
class HostTest(unittest.TestCase):
def test_units_parses_systemctl_show(self):
h = hm.Host()
out = ("Id=steamvr.service\nLoadState=loaded\nActiveState=active\n\n"
"Id=frametop-camd.service\nLoadState=not-found\nActiveState=inactive\n")
h.run = lambda cmd, timeout=20: hm.subprocess.CompletedProcess(cmd, 0, out, "")
got = h.units((hm.STEAMVR, CAMD, VIEW))
self.assertEqual(got[hm.STEAMVR], {"load": "loaded", "active": "active"})
self.assertEqual(got[CAMD]["load"], "not-found")
self.assertEqual(got[VIEW], {"load": "not-found", "active": "inactive"}) # no answer for it
def test_exec_path(self):
h = hm.Host()
out = "{ path=/x/hands/build/ft-camd ; argv[]=/x/hands/build/ft-camd --status 60 ; ignore_errors=no }\n"
h.run = lambda cmd, timeout=20: hm.subprocess.CompletedProcess(cmd, 0, out, "")
self.assertEqual(h.exec_path(CAMD), "/x/hands/build/ft-camd")
if __name__ == "__main__":
unittest.main()