Dotfiles_ArchAP/tests/test_quickshell_rgb_keyboard.py
2026-11-09 19:21:49 +01:00

159 lines
9.5 KiB
Python

"""Exercise production RGB QML with an executable fixture; never open real HID."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / 'quickshell/.config/quickshell/labfy-sway'
class RgbKeyboardTests(unittest.TestCase):
def test_production_process_flow_and_opt_out(self):
qs = shutil.which('qs')
if not qs:
self.skipTest('QuickShell unavailable')
with tempfile.TemporaryDirectory(prefix='labfy-rgb-qml-') as folder:
p = Path(folder)
(p/'runtime').mkdir(mode=0o700)
(p/'config').mkdir()
(p/'rgbkeyboard').mkdir()
shutil.copy(BASE/'rgbkeyboard/qmldir', p/'rgbkeyboard/qmldir')
source = (BASE/'rgbkeyboard/RgbKeyboardService.qml').read_text()
old = 'readonly property string executable: (Quickshell.env("HOME") || "") + "/.local/bin/labfy-rgb"'
self.assertIn(old, source)
source = source.replace(old, 'readonly property string executable: ' + json.dumps(str(p/'fake-cli')))
(p/'rgbkeyboard/RgbKeyboardService.qml').write_text(source)
# Only the executable path is replaced. All Process/state/validation
# logic is production code. Preferences and endpoint state are isolated.
(p/'fake-cli').write_text('''#!/usr/bin/python3
import json, os, sys
from pathlib import Path
p=Path(os.environ['RGB_FIXTURE'])
a=sys.argv[1:]
with (p/'calls').open('a') as f: f.write(json.dumps(a)+'\\n')
s=dict(schema=1,available=True,device='/dev/hidraw77',commanded_mode='unknown',last_color=None,last_percent=None)
if (p/'state').exists(): s=json.loads((p/'state').read_text())
if a[0]=='set' and a[1]=='#010101': print('fixture access denied',file=sys.stderr); sys.exit(4)
if a[0]=='set' and a[1]=='#000001': Path(sys.argv[0]).unlink()
if a[0]=='status': print(json.dumps(s))
elif a[0]=='set': s.update(commanded_mode='software',last_color=a[1],last_percent=int(a[2]) if len(a)>2 else 100); (p/'state').write_text(json.dumps(s))
elif a[0]=='brightness': s['last_percent']=int(a[1]); (p/'state').write_text(json.dumps(s))
elif a[0]=='off': s['last_percent']=0; (p/'state').write_text(json.dumps(s))
elif a[0]=='auto': s.update(commanded_mode='firmware',last_color=None,last_percent=None); (p/'state').write_text(json.dumps(s))
else: sys.exit(2)
''')
(p/'fake-cli').chmod(0o700)
(p/'shell.qml').write_text('''import QtQuick
import Quickshell
import "rgbkeyboard"
ShellRoot {
id: root
property int step: 0
property int presetIndex: 0
property int ticks: 0
function check(ok, message) { if (!ok) { console.error("RGB_FAIL " + message); Qt.exit(1); } }
Timer {
interval: 80; running: true; repeat: true
onTriggered: {
root.ticks++;
if (root.ticks > 100) { console.error("RGB_TIMEOUT"); Qt.exit(1); return; }
if (RgbKeyboardService.busy || RgbKeyboardService.refreshPending) return;
if (root.step === 0) {
root.check(!RgbKeyboardService.restoreEnabled, "default opt out");
root.check(RgbKeyboardService.palette.length === 8, "eight presets");
const golden = ["#8000FF","#0040FF","#00FFFF","#00FF40","#FF0080","#FF0000","#FF6000","#FFFF00"];
for (let i = 0; i < golden.length; ++i) root.check(RgbKeyboardService.palette[i].color === golden[i], "preset wire color");
root.check(!RgbKeyboardService.applyColor("#bad", 60), "reject bad color");
root.check(!RgbKeyboardService.intensity(101), "reject intensity");
RgbKeyboardService.refresh(); root.step++;
} else if (root.step === 1 && RgbKeyboardService.available) {
root.check(RgbKeyboardService.preset("#8000ff"), "apply");
root.check(!RgbKeyboardService.firmware(), "serialize writes"); root.step++;
} else if (root.step === 2 && RgbKeyboardService.lastColor === RgbKeyboardService.palette[root.presetIndex].color) {
root.check(RgbKeyboardService.lastPercent === 100, "preset CLI default percent");
root.presetIndex++;
if (root.presetIndex < RgbKeyboardService.palette.length) {
root.check(RgbKeyboardService.preset(RgbKeyboardService.palette[root.presetIndex].color), "next preset");
} else { RgbKeyboardService.intensity(30); root.step++; }
} else if (root.step === 3 && RgbKeyboardService.lastPercent === 30) {
RgbKeyboardService.off(); root.step++;
} else if (root.step === 4 && RgbKeyboardService.lastPercent === 0) {
RgbKeyboardService.firmware(); root.step++;
} else if (root.step === 5 && RgbKeyboardService.commandedMode === "firmware") {
root.check(RgbKeyboardService.lastColor === "", "no physical firmware color claim");
root.check(!RgbKeyboardService.restoreEnabled, "opt in not activated");
RgbKeyboardService.applyColor("#010101", 100); root.step++;
} else if (root.step === 6 && RgbKeyboardService.message.indexOf("code 4") >= 0) {
RgbKeyboardService.applyColor("#000001", 100); root.step++;
} else if (root.step === 7 && !RgbKeyboardService.available) {
root.check(!RgbKeyboardService.busy, "failed executable start releases ownership");
console.log("RGB_PASS"); Qt.exit(0);
}
}
}
}
''')
env = dict(os.environ, RGB_FIXTURE=str(p), XDG_CONFIG_HOME=str(p/'config'),
XDG_RUNTIME_DIR=str(p/'runtime'), QT_QPA_PLATFORM='offscreen')
result = subprocess.run([qs, '-p', str(p/'shell.qml')], env=env,
capture_output=True, text=True, timeout=15)
log = result.stdout + result.stderr
self.assertEqual(result.returncode, 0, log)
self.assertIn('RGB_PASS', log)
self.assertNotIn('Failed to initialize QSettings', log)
commands = [json.loads(line) for line in (p/'calls').read_text().splitlines()]
writes = [args for args in commands if args[0] != 'status']
self.assertEqual(writes, [['set',c] for c in ['#8000FF','#0040FF','#00FFFF','#00FF40','#FF0080','#FF0000','#FF6000','#FFFF00']] + [['brightness','30'],['off'],['auto'],['set','#010101','100'],['set','#000001','100']])
self.assertTrue(all(args == ['status','--json'] for args in commands if args[0]=='status'))
def test_startup_opt_in_firmware_priority_and_invalid_preferences(self):
qs = shutil.which('qs')
if not qs:
self.skipTest('QuickShell unavailable')
cases = [(False, 'software', 'unknown', '#B4BEFE', False),
(True, 'software', 'unknown', '#B4BEFE', True),
(True, 'firmware', 'unknown', '#B4BEFE', False),
(True, 'software', 'firmware', '#B4BEFE', False),
(True, 'software', 'unknown', '#INVALID', False)]
for enabled, saved_mode, native_mode, color, expect_set in cases:
with self.subTest(enabled=enabled, saved_mode=saved_mode, native_mode=native_mode, color=color), tempfile.TemporaryDirectory(prefix='labfy-rgb-startup-') as folder:
p = Path(folder)
(p/'runtime').mkdir(mode=0o700)
(p/'config/labfy-rgb').mkdir(parents=True)
(p/'rgbkeyboard').mkdir()
shutil.copy(BASE/'rgbkeyboard/qmldir', p/'rgbkeyboard/qmldir')
source = (BASE/'rgbkeyboard/RgbKeyboardService.qml').read_text()
old = 'readonly property string executable: (Quickshell.env("HOME") || "") + "/.local/bin/labfy-rgb"'
self.assertIn(old, source)
(p/'rgbkeyboard/RgbKeyboardService.qml').write_text(source.replace(old, 'readonly property string executable: '+json.dumps(str(p/'fake-cli'))))
(p/'config/labfy-rgb/quickshell.ini').write_text('[General]\nrestoreEnabled='+str(enabled).lower()+'\nsavedMode='+saved_mode+'\nsavedColor='+color+'\nsavedPercent=60\n')
(p/'fake-cli').write_text('''#!/usr/bin/python3
import os,sys,json
from pathlib import Path
p=Path(os.environ['RGB_FIXTURE'])
a=sys.argv[1:]
with (p/'calls').open('a') as f: f.write(json.dumps(a)+'\\n')
if a[0]=='status': print(json.dumps(dict(schema=1,available=True,device='/dev/hidraw77',commanded_mode=os.environ['NATIVE_MODE'],last_color=None,last_percent=None)))
''')
(p/'fake-cli').chmod(0o700)
(p/'shell.qml').write_text('''import QtQuick
import Quickshell
import "rgbkeyboard"
ShellRoot {
Component.onCompleted: console.log("OPT_IN", RgbKeyboardService.restoreEnabled)
Timer { running: true; interval: 2400; onTriggered: Qt.exit(0) }
}
''')
env = dict(os.environ, RGB_FIXTURE=str(p), NATIVE_MODE=native_mode,
XDG_CONFIG_HOME=str(p/'config'), XDG_RUNTIME_DIR=str(p/'runtime'), QT_QPA_PLATFORM='offscreen')
result = subprocess.run([qs,'-p',str(p/'shell.qml')],env=env,capture_output=True,text=True,timeout=8)
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
commands = [json.loads(line) for line in (p/'calls').read_text().splitlines()]
writes = [args for args in commands if args[0] != 'status']
self.assertEqual(writes, [['set','#B4BEFE','60']] if expect_set else [])
if __name__ == '__main__':
unittest.main()