121 lines
5.9 KiB
Python
121 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Synthetic dashboard on an isolated headless Sway; never touches the live shell."""
|
|
import json
|
|
import resource
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / 'tests/session_v2/fixtures'))
|
|
from headless import Lab
|
|
SOURCE = ROOT / 'quickshell/.config/quickshell/labfy-sway'
|
|
QS = str(Path.home() / '.local/opt/quickshell-qt612-popup/bin/quickshell')
|
|
|
|
|
|
def call(lab, fixture, name, *args):
|
|
result = subprocess.run([QS, 'ipc', '-p', str(fixture), 'call', 'fixture', name, *map(str,args)],
|
|
env=lab.env, text=True, capture_output=True, timeout=5)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr[-1500:])
|
|
if not result.stdout.strip():
|
|
return None
|
|
try:
|
|
return json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
return result.stdout.strip()
|
|
|
|
|
|
with Lab() as lab:
|
|
lab.env['DBUS_SESSION_BUS_ADDRESS'] = os.environ['DBUS_SESSION_BUS_ADDRESS']
|
|
config = Path(lab.env['XDG_CONFIG_HOME']) / 'quickshell/labfy-sway'
|
|
config.mkdir(parents=True)
|
|
(config / 'theme').symlink_to(SOURCE / 'theme', target_is_directory=True)
|
|
monitor = config / 'systemmonitor'
|
|
monitor.mkdir()
|
|
for name in ('Sparkline.qml', 'MonitorPanel.qml'):
|
|
content = (SOURCE / 'systemmonitor' / name).read_text()
|
|
if name == 'MonitorPanel.qml':
|
|
content = content.replace(' grabFocus: true', ' grabFocus: false')
|
|
content = content.replace(' signal dismissed()', ''' signal dismissed()
|
|
function metrics(): string {
|
|
return JSON.stringify({width:width,height:height,screenWidth:barWindow.screen.width,
|
|
screenHeight:barWindow.screen.height,gridHeight:grid.height,viewport:dashboard.height,
|
|
cpu:[cpuBox.x,cpuBox.y,cpuBox.width,cpuBox.height],gpu:[gpuBox.x,gpuBox.y,gpuBox.width,gpuBox.height],
|
|
memory:[memoryBox.x,memoryBox.y,memoryBox.width,memoryBox.height],
|
|
processes:[processBox.x,processBox.y,processBox.width,processBox.height],
|
|
disk:[diskBox.x,diskBox.y,diskBox.width,diskBox.height],
|
|
network:[netBox.x,netBox.y,netBox.width,netBox.height],rows:processList.count,
|
|
cores:coreKeys().length,visible:visible,collect:service.processesVisible});
|
|
}''')
|
|
(monitor / name).write_text(content)
|
|
fixture = config / 'fixture.qml'
|
|
fixture.write_text('''//@ pragma UseQApplication
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import "systemmonitor"
|
|
ShellRoot {
|
|
PanelWindow {id:bar; anchors {top:true;left:true;right:true} implicitHeight:34; focusable:true; color:"#1e1e2e"
|
|
Item {id:indicator; x:parent.width-30; width:26; height:26}
|
|
}
|
|
QtObject { id:service; property bool processesVisible:false
|
|
property var data: ({memory:{used:8e9,total:16e9,available:8e9,cache:1e9,swapUsed:1e9,swapTotal:6e9},gpu:null,zram:null,
|
|
psi:{some:[1,2,3],full:[0,0,0]},temperatures:{cpu:65,gpu:null},disk:{used:400e9,total:1000e9,available:500e9,filesystem:"btrfs"},
|
|
load:["1.2","1.1","1.0"],frequencies:[{currentMHz:3000}],network:null})
|
|
property var usage: ({cpu:42,threads:{cpu0:10,cpu1:20,cpu2:30,cpu3:40,cpu4:50,cpu5:60,cpu6:70,cpu7:80,
|
|
cpu8:11,cpu9:21,cpu10:31,cpu11:41,cpu12:51,cpu13:61,cpu14:71,cpu15:81}})
|
|
property var rates: ({rx:100000,tx:50000})
|
|
property var processRows: []
|
|
property var histories: ({cpu:Array.from({length:600},(_,i)=>({value:i%100})),gpu:[],ram:[],psi:[],rx:[],tx:[]})
|
|
}
|
|
MonitorPanel {id:popup;barWindow:bar;indicator:indicator;service:service;}
|
|
IpcHandler { target:"fixture"
|
|
function state(): string {return popup.metrics()}
|
|
function setRows(count:int): void { service.processRows=Array.from({length:count},(_,i)=>({pid:i+1,name:"processus très long "+i,cpu:i%100,rss:i*1000000})); }
|
|
function hide(): void {popup.visible=false}
|
|
function show(): void {popup.visible=true}
|
|
}
|
|
}
|
|
''')
|
|
proc = lab.start([QS, '-p', str(fixture), '--no-color'])
|
|
deadline = time.monotonic() + 12
|
|
while True:
|
|
try:
|
|
call(lab, fixture, 'state')
|
|
break
|
|
except RuntimeError:
|
|
if time.monotonic() > deadline:
|
|
raise RuntimeError('Fixture failed to load: ' + str(lab.directory) + '\n' + (lab.directory / 'child-1.log').read_text()[-5000:])
|
|
time.sleep(.1)
|
|
call(lab,fixture,'show')
|
|
for scale in (1, 1.4):
|
|
lab.command(f'output HEADLESS-1 scale {scale}')
|
|
for count in (0, 80):
|
|
call(lab, fixture, 'setRows', count)
|
|
time.sleep(.15)
|
|
state = call(lab, fixture, 'state')
|
|
assert state['visible'] and state['collect'], state
|
|
assert state['cores'] == 16 and state['rows'] == count, state
|
|
assert state['width'] <= state['screenWidth'] - 20, state
|
|
assert state['height'] <= state['screenHeight'] - 34, state
|
|
for key in ('cpu','gpu','memory','processes','disk','network'):
|
|
x,y,w,h = state[key]
|
|
assert x >= 0 and y >= 0 and w > 0 and h > 0 and x+w <= state['width'] and y+h <= state['gridHeight'], (key,state)
|
|
if scale == 1 and count == 80:
|
|
image = Path('/tmp/step15b2-dashboard-headless.png')
|
|
subprocess.run(['grim','-o','HEADLESS-1',str(image)], env=lab.env, check=True)
|
|
print('SCREENSHOT',image)
|
|
print('PASS',scale,count,state['width'],state['height'])
|
|
call(lab,fixture,'hide')
|
|
assert not call(lab,fixture,'state')['collect']
|
|
call(lab,fixture,'show')
|
|
print('PASS close stops process collection')
|
|
stat = Path(f'/proc/{proc.pid}/stat').read_text().split()
|
|
rss = int(Path(f'/proc/{proc.pid}/status').read_text().split('VmRSS:')[1].split()[0])
|
|
print('FIXTURE_QS_RSS_KIB', rss)
|
|
|
|
errors = [line for line in (lab.directory / 'child-1.log').read_text().splitlines() if 'ERROR:' in line]
|
|
assert not errors, errors[-20:]
|