Dotfiles_ArchAP/tests/systemmonitor/visual.py

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:]