#!/usr/bin/env python3 """One headless run of staged static compositor with measured tiled crossing. WHY: resolved relocations do not demonstrate real compositor execution. CONTRACT: verify /proc executable/library identity before mapping any client. INVARIANT: owned sockets/groups only, one camera correction, durable captures. """ import argparse import json import os from pathlib import Path import re import shutil import signal import subprocess import tempfile import threading import time from PIL import Image,ImageChops from support import environment,reserve,save,sha p=argparse.ArgumentParser();p.add_argument('lab',type=Path);a=p.parse_args();lab=a.lab.resolve() assert json.loads((lab/'results/static-stage-manifest.json').read_text())['status']=='PASS' assert json.loads((lab/'results/binary-elf.json').read_text())['status']=='PASS' reserve(lab,'run');run=lab/'run';run.mkdir();runtime=Path(tempfile.mkdtemp(prefix='msr-',dir='/tmp')) runtime.chmod(0o700);(run/'runtime-path').write_text(str(runtime)+'\n') (run/'config').write_text('output HEADLESS-1 resolution 800x600 scale 1 transform normal position -800 0\noutput HEADLESS-2 resolution 800x600 scale 1 transform normal position 0 0\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\n') env=environment();env.update(XDG_RUNTIME_DIR=str(runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman') binary=lab/'stage-static/usr/bin/swayfx-mosaic';processes=[];files=[];failure=None;compositor=None;events=[] started=time.monotonic();finished=threading.Event();expired=threading.Event() def event(kind,**data): row={'ns':time.monotonic_ns(),'kind':kind,**data};events.append(row) with (run/'events.jsonl').open('a') as out:out.write(json.dumps(row)+'\n') def stop_owned(): for proc in list(processes): try:os.killpg(proc.pid,signal.SIGTERM) except ProcessLookupError:pass def watchdog(): while not finished.wait(.1): if time.monotonic()-started>150 or any(x.stat().st_size>64*1024*1024 for x in run.glob('*.log')): expired.set();stop_owned();return thread=threading.Thread(target=watchdog,daemon=True);thread.start() def spawn(name,argv,stdin=False,extra=None): assert not expired.is_set() out=(run/(name+'.log')).open('w');files.append(out) proc=subprocess.Popen(list(map(str,argv)),env={**env,**(extra or {})},stdin=subprocess.PIPE if stdin else subprocess.DEVNULL,stdout=out,stderr=subprocess.STDOUT,text=True,start_new_session=True) processes.append(proc);event('spawn',name=name,pid=proc.pid,argv=list(map(str,argv)));return proc def wait(predicate,seconds=8): end=time.monotonic()+seconds while time.monotonic()before) time.sleep(.15);event('pointer',action=action,global_point=[x,y],target_tail=log('target').splitlines()[-6:] if (run/'target.log').exists() else []) def capture(tag,output): reserve(lab,'capture');path=run/(tag+'.png') subprocess.run(['grim','-s','1','-o',output,str(path)],env=env,check=True,timeout=5,stdout=subprocess.DEVNULL) im=Image.open(path).convert('RGB');assert im.size==(800,600) event('capture',file=path.name,output=output,sha256=sha(path));return im def measure(owner): rows=[x for x in log('compositor').splitlines() if 'PI_OWNER output=HEADLESS-1 ' in x and f' id={owner["id"]} ' in x] assert rows,'applied owner trace unavailable' line=rows[-1];m=re.search(r'scene_valid=1 scene=(-?\d+),(-?\d+) applied_size=(\d+),(\d+)',line);assert m,line x,y,w,h=map(int,m.groups());win=owner['window_rect'] return {'id':owner['id'],'scene':[x,y,w,h],'window_rect':win,'source_box':[x+win['x'],y+win['y'],win['width'],win['height']],'raw':line} try: compositor=spawn('compositor',[binary,'-d','-c',run/'config'],extra={'LD_DEBUG':'bindings','LD_BIND_NOW':'1'}) sock=wait(lambda:next(iter(runtime.glob('sway-ipc.*.sock')),None)) display=wait(lambda:next((x for x in runtime.glob('wayland-*') if x.is_socket()),None)) env.update(SWAYSOCK=str(sock),WAYLAND_DISPLAY=display.name) maps=Path(f'/proc/{compositor.pid}/maps').read_text();(run/'proc-maps.txt').write_text(maps) exe=Path(f'/proc/{compositor.pid}/exe').resolve();assert exe==binary and sha(exe)==sha(binary),'wrong executable' paths=sorted({line.split()[-1] for line in maps.splitlines() if len(line.split())>=6 and line.split()[-1].startswith('/')}) pair=json.loads((lab/'results/wlroots-qualified-pair.json').read_text());wl=Path(pair['library']) assert not any('libscenefx' in x for x in paths),'wrong SceneFX shared mapping' loaded_wl=[Path(x) for x in paths if 'libwlroots' in x];assert len(loaded_wl)==1 and loaded_wl[0].resolve()==wl and sha(wl)==pair['sha256'],'wrong wlroots mapping' assert not any('/swayfx-mosaic-' in x and Path(x)!=binary for x in paths),'old LAB dependency' save(lab,'loader-mappings',{'status':'PASS','pid':compositor.pid,'exe':str(exe),'sha256':sha(exe),'paths':paths,'wlroots_sha256':sha(wl),'SceneFX_shared':False}) outputs=ipc('-t','get_outputs');actual={o['name']:(o['rect']['x'],o['rect']['y'],o['rect']['width'],o['rect']['height'],o['scale'],o['transform']) for o in outputs if o['active']} assert actual=={'HEADLESS-1':(-800,0,800,600,1.0,'normal'),'HEADLESS-2':(0,0,800,600,1.0,'normal')},actual save(lab,'headless-outputs',outputs) pointer=spawn('pointer',[lab/'pointer-daemon','1600','600'],stdin=True);wait(lambda:'POINTER_READY' in log('pointer')) cmd('workspace 2');ref=spawn('reference',[lab/'static-reference'],stdin=True);wait(lambda:find('org.mosaic.StaticReference')) time.sleep(.4);inject('move',-790,590);baseline=capture('h2-reference','HEADLESS-2') cmd('workspace 1');filler=spawn('filler',[lab/'static-reference'],stdin=True);wait(lambda:len([n for n in nodes(ipc('-t','get_tree')) if n.get('app_id')=='org.mosaic.StaticReference'])==2) target=spawn('target',[lab/'saved-client'],stdin=True);wait(lambda:find('org.mosaic.SavedBufferProbe')) cmd('layout mosaic');time.sleep(.5) owner=find('org.mosaic.SavedBufferProbe');before=measure(owner);x,y,w,h=before['source_box'];desired=-w//2;delta=x-desired event('camera-calculation',before=before,desired_left=desired,delta=delta) if delta:reserve(lab,'camera');cmd(f'layout mosaic scroll {delta}') time.sleep(.5);owner=find('org.mosaic.SavedBufferProbe');after=measure(owner);x,y,w,h=after['source_box'] assert x<00 and right_count==0 and diff is None,'forbidden pixel leakage/reference difference' visible_x=max(-780,min(-20,x+max(20,w//4)));visible_y=min(550,y+80) target_before=log('target').count('BUTTON ');inject('right',visible_x,visible_y);visible_buttons=log('target').count('BUTTON ')-target_before assert visible_buttons>=2,'visible target input missing' target_before=log('target').count('BUTTON ');ref_before=log('reference').count('BUTTON ') clipped_x=max(20,min(x+w-20,200));inject('right',clipped_x,visible_y) invisible_buttons=log('target').count('BUTTON ')-target_before;reference_buttons=log('reference').count('BUTTON ')-ref_before save(lab,'input-analysis',{'visible_point':[visible_x,visible_y],'visible_target_button_events':visible_buttons,'forbidden_point':[clipped_x,visible_y],'forbidden_target_button_events':invisible_buttons,'reference_button_events':reference_buttons}) assert invisible_buttons==0 and reference_buttons>=2,'forbidden target hit/reference missing' inject('move',-790,590);repeat=capture('h2-mosaic-repeat','HEADLESS-2');assert ImageChops.difference(baseline,repeat).getbbox() is None,'unstable H2 reference' cmd('layout splith');time.sleep(.5);inject('move',-790,590) standard_left=capture('h1-standard','HEADLESS-1');standard_right=capture('h2-standard','HEADLESS-2') scene_lines=[s for s in log('compositor').splitlines() if 'PI_SCENE output=HEADLESS-1' in s] assert scene_lines and 'clip_valid=0' in scene_lines[-1],'residual Mosaic clip' standard_owner=find('org.mosaic.SavedBufferProbe');standard_measure=measure(standard_owner) sx,sy,sw,sh=standard_measure['source_box'];before_buttons=log('target').count('BUTTON ');inject('right',sx+sw//2,sy+min(80,sh//2));standard_buttons=log('target').count('BUTTON ')-before_buttons assert standard_buttons>=2 and ImageChops.difference(baseline,standard_right).getbbox() is None,'standard regression' save(lab,'standard-layout',{'status':'PASS sampled only','scene_trace':scene_lines[-1],'owner':standard_measure,'target_button_events':standard_buttons,'H2_equal':True}) save(lab,'popup-ownership',{'status':'NOT TESTED','reason':'root-grab fixture not added to the single minimal tiled scenario; historical pristine/grab evidence retained'}) cmd('exit');compositor.wait(timeout=5);assert compositor.returncode==0 except Exception as exc: failure=f'{type(exc).__name__}: {exc}';event('failure',message=failure) finally: finished.set();thread.join(timeout=1);stop_owned() for proc in processes: try:proc.wait(timeout=3) except subprocess.TimeoutExpired: os.killpg(proc.pid,signal.SIGKILL);proc.wait(timeout=3) try:os.killpg(proc.pid,signal.SIGKILL) except ProcessLookupError:pass for out in files:out.close() elapsed=time.monotonic()-started save(lab,'static-runtime',{'status':'PASS' if failure is None else 'INCONCLUSIVE','failure':failure,'duration_seconds':elapsed,'binary':str(binary),'sha256':sha(binary),'process_exit_codes':[{'pid':x.pid,'argv':x.args,'exit':x.returncode} for x in processes],'runtime_removed':str(runtime),'cleanup':'all owned groups reaped'}) shutil.rmtree(runtime) damage=[s for s in log('compositor').splitlines() if 'TEMPORAL_DAMAGE ' in s] save(lab,'damage-analysis',{'status':'OBSERVED' if damage else 'NOT TESTED','extents':damage,'all_frames':'NOT TESTED'}) print('STATIC_RUNTIME',failure or 'PASS') if failure:raise SystemExit(1)