lazyBoy/scripts/cua-cursor-color-test.py

108 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Verify selected colors in the actual VNC framebuffer, without VNC input."""
import importlib.machinery
import importlib.util
import struct
import socket
import subprocess
import time
import zlib
from pathlib import Path
loader = importlib.machinery.SourceFileLoader('adapter', '/usr/local/bin/lazyboy-cua-adapter-test')
spec = importlib.util.spec_from_loader(loader.name, loader)
adapter = importlib.util.module_from_spec(spec)
loader.exec_module(adapter)
def capture():
with socket.create_connection(('127.0.0.1', 5900), 5) as stream:
def read(size):
data = bytearray()
while len(data) < size:
chunk = stream.recv(size - len(data))
assert chunk, 'VNC closed before a complete framebuffer'
data.extend(chunk)
return bytes(data)
assert read(12).startswith(b'RFB ')
stream.sendall(b'RFB 003.008\n')
assert 1 in read(read(1)[0]), 'disposable desktop must offer unauthenticated loopback VNC'
stream.sendall(b'\x01')
assert read(4) == b'\0' * 4
stream.sendall(b'\x01')
width, height = struct.unpack('>HH', read(4))
read(16)
read(struct.unpack('>I', read(4))[0])
assert 0 < width <= 4096 and 0 < height <= 4096
# Request raw pixels in RGBX byte order. Never send keys or pointer input.
stream.sendall(b'\0' * 4 + struct.pack('>BBBBHHHBBBxxx', 32, 24, 0, 1, 255, 255, 255, 0, 8, 16))
stream.sendall(struct.pack('>BBHi', 2, 0, 1, 0))
stream.sendall(struct.pack('>BBHHHH', 3, 0, 0, 0, width, height))
pixels = bytearray(width * height * 4)
while True:
kind = read(1)[0]
if kind == 2:
continue
if kind == 3:
read(3)
read(struct.unpack('>I', read(4))[0])
continue
assert kind == 0, kind
read(1)
for _ in range(struct.unpack('>H', read(2))[0]):
x, y, w, h, encoding = struct.unpack('>HHHHi', read(12))
assert encoding == 0 and x + w <= width and y + h <= height
rectangle = read(w * h * 4)
for row in range(h):
start = ((y + row) * width + x) * 4
pixels[start:start + w * 4] = rectangle[row * w * 4:(row + 1) * w * 4]
return width, height, pixels
def save_png(path, width, height, pixels):
pixels[3::4] = b'\xff' * (width * height)
rows = b''.join(b'\0' + pixels[y * width * 4:(y + 1) * width * 4] for y in range(height))
def chunk(kind, data):
return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data))
Path(path).write_bytes(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(rows)) + chunk(b'IEND', b''))
adapter.wait_health()
root = Path('/tmp/lazyboy')
files = [root / 'screen-1.agent-name', root / 'screen-1.agent-color', root / 'screen-2.agent-color']
previous = {path: path.read_bytes() if path.exists() else None for path in files}
name = '小幫手 Alice'
try:
subprocess.run(['lazyboy-screen', 'ensure', '0', '', name, '#8B5CF6'], check=True)
adapter.api('/browser', {'action': 'snapshot', 'ensure': True})
adapter.api('/browser', {'action': 'navigate', 'url': 'about:blank', 'ensure': True})
pid = (root / 'screen-1-cua.pid').read_text()
for color, label in [('#8B5CF6', 'purple'), ('#22C55E', 'green'), ('#E11D48', 'pink')]:
subprocess.run(['lazyboy-screen', 'color', '0', color], check=True)
subprocess.run(['lazyboy-screen', 'color', '1', '#123456'], check=True)
assert (root / 'screen-1-cua.pid').read_text() == pid, 'color change restarted Cua'
assert files[0].read_text() == name, 'color change renamed the session'
adapter.api('/act', {'actions': [{'kind': 'pointer', 'type': 'click', 'x': 640, 'y': 400}], 'observe': False, 'settle_ms': 100})
expected = bytes.fromhex(color[1:])
deadline = time.monotonic() + 5
while True:
width, height, pixels = capture()
matches = sum(pixels[(y * width + x) * 4:(y * width + x) * 4 + 3] == expected
for y in range(380, 425) for x in range(620, 665))
if matches >= 3:
break
assert time.monotonic() < deadline, (color, 'selected RGB absent from VNC cursor')
time.sleep(0.1)
save_png(f'/tmp/cua-cursor-{label}.png', width, height, pixels)
print(f'VNC cursor {color}: {matches} exact RGB pixels, same session and daemon', flush=True)
rejected = subprocess.run(['lazyboy-screen', 'color', '0', 'red'], capture_output=True)
assert rejected.returncode != 0 and files[1].read_text() == '#E11D48'
finally:
adapter.smoke.cua_call('end_session', {'session': name})
for path, value in previous.items():
if value is None:
path.unlink(missing_ok=True)
else:
path.write_bytes(value)
print('Selected cursor colors, live updates, display isolation and invalid-color rejection passed')