159 lines
9.5 KiB
Python
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()
|