import json, os, pathlib, signal, subprocess, sys, time LAB=pathlib.Path(__file__).resolve().parent case=sys.argv[1] assert case in ('control','private_standard','private_mosaic','private_mosaic_hit') mode=sys.argv[2] if len(sys.argv)>2 else '' assert mode in ('','boundary_left','boundary_right','standard_return', 'lifecycle_switch','lifecycle_destroy','outside_left','visible_left', 'fully_visible') run=LAB/('run-'+case+('-'+mode if mode else '')) run.mkdir(exist_ok=True) run.chmod(0o700) config=('output HEADLESS-1 resolution 800x600 position 0 0\n' 'output HEADLESS-2 resolution 800x600 position 800 0\n' 'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n' 'focus_follows_mouse no\n') (run/'config').write_text(config) e=os.environ.copy() e.update(XDG_RUNTIME_DIR=str(run),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman') e.pop('WAYLAND_DISPLAY',None);e.pop('SWAYSOCK',None) compositor_env=e.copy() if case.startswith('private'): binary=LAB/'mosaic-build/sway/swayfx-mosaic' compositor_env['LD_LIBRARY_PATH']=str(LAB/'scenefx-build') else: binary=pathlib.Path('/usr/bin/sway') compositor_env.pop('LD_LIBRARY_PATH',None) e.pop('LD_LIBRARY_PATH',None) files=[];children=[] def spawn(args,name,debug=False): f=(run/name).open('w');files.append(f) env=e.copy() if debug:env['WAYLAND_DEBUG']='1' p=subprocess.Popen(args,env=env,stdout=f,stderr=subprocess.STDOUT,start_new_session=True) children.append(p) return p def wait(fn,seconds=10): end=time.monotonic()+seconds while time.monotonic()=1) if mode=='fully_visible':e['PROBE_POPUP_FULLY_VISIBLE']='1' client=spawn([str(LAB/'grab-client')],'client.log',True) wait(lambda:find_app(tree(),'org.mosaic.ValidGrabProbe')) for label in ('C','D'): spawn(['kitty','--class',label,'/usr/bin/sleep','60'],label+'.log') time.sleep(.35) ipc('workspace 2') spawn(['stdbuf','-oL','wev'],'wev.log') time.sleep(.5) ipc('workspace 1') ipc('[app_id="org.mosaic.ValidGrabProbe"] focus') if case.startswith('private_mosaic'): current=workspace(tree(),'1')['mosaic']['rows'][0]['scroll_x'] ipc(f'layout mosaic scroll {400-current:g}') time.sleep(.5) ws=workspace(tree(),'1');owner=find_app(ws,'org.mosaic.ValidGrabProbe') (run/'owner-before.json').write_text(json.dumps({'owner':owner,'workspace':ws.get('mosaic')},indent=2)) rect=owner['rect'] scroll=ws['mosaic']['rows'][0]['scroll_x'] if case.startswith('private_mosaic') else 0 x=int(max(30,min(770,rect['x']-scroll+int(rect['width']*.55))));y=rect['y']+130 print('OPEN_POINT',x,y,'OWNER_RECT',rect,flush=True) inject('left',x,y) wait(lambda:logged('POPUP_XDG_CONFIGURE')) time.sleep(.35) shot('open') print('POPUP_OPEN',logged('POPUP_GRAB_REQUEST'),'CLIENT_ALIVE',client.poll() is None,flush=True) if mode.startswith('boundary_'): inject('move',700,150);time.sleep(.2);shot('visible-hover') inject('right',700,150);time.sleep(.2);shot('visible-click') inject('move',900,150);time.sleep(.2);shot('boundary-hover') inject(mode.split('_')[1],900,150) time.sleep(.4);shot('boundary-click') print('BOUNDARY',mode,'POPUP_DONE',logged('POPUP_DONE'), 'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if mode=='standard_return': print('STANDARD_RESULT',ipc('layout splith'),flush=True) time.sleep(.4);shot('standard-open') inject('move',900,150);inject('right',900,150) time.sleep(.35);shot('standard-click') print('STANDARD_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if mode=='outside_left': inject('left',900,400) time.sleep(.4);shot('outside-left') print('OUTSIDE_LEFT_POPUP_DONE',logged('POPUP_DONE'), 'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if mode=='visible_left': inject('move',700,150);inject('left',700,150) time.sleep(.3);shot('visible-left') print('VISIBLE_LEFT_POPUP_DONE',logged('POPUP_DONE'), 'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if mode=='fully_visible': inject('move',350,150);inject('left',350,150);inject('right',350,150) time.sleep(.3);shot('fully-visible-clicks') print('FULLY_VISIBLE_POPUP_DONE',logged('POPUP_DONE'), 'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if case.startswith('private_mosaic'): print('SCROLL_RESULT',ipc('layout mosaic scroll 880'),flush=True) time.sleep(.6) shot('hidden') ws=workspace(tree(),'1');owner=find_app(ws,'org.mosaic.ValidGrabProbe') print('HIDDEN_OWNER_RECT',owner['rect'],'MOSAIC',ws.get('mosaic'),flush=True) print('POPUP_DONE_BEFORE_CLICK',logged('POPUP_DONE'),flush=True) if mode=='lifecycle_switch': print('SWITCH_RESULT',ipc('workspace 2'),flush=True) time.sleep(.45);shot('switched') print('SWITCH_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) if mode=='lifecycle_destroy': print('DESTROY_RESULT',ipc(f'[con_id={owner["id"]}] kill'),flush=True) time.sleep(.45);shot('destroyed') print('DESTROY_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True) sys.exit(0) inject('move',100,150) time.sleep(.25);shot('hidden-hover') print('POPUP_DONE_AFTER_HOVER',logged('POPUP_DONE'),flush=True) if case.endswith('_hit'): inject('right',100,150) time.sleep(.25);shot('hidden-click') print('POPUP_DONE_AFTER_HIDDEN_CLICK',logged('POPUP_DONE'),flush=True) print('RETURN_RESULT',ipc('layout mosaic scroll -880'),flush=True) time.sleep(.5);shot('returned') print('POPUP_DONE_AFTER_RETURN',logged('POPUP_DONE'),flush=True) # A click outside the popup is compared against the control. Capture before # and after separately so dismissal cannot masquerade as clipping evidence. inject('right',900,400) time.sleep(.5);shot('outside') print('POPUP_DONE_OUTSIDE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True) if case.startswith('private_mosaic'): print('STANDARD_RESULT',ipc('layout splith'),flush=True) time.sleep(.4);shot('standard') finally: for child in children: if child.poll() is None:os.killpg(child.pid,signal.SIGTERM) if compositor.poll() is None:os.killpg(compositor.pid,signal.SIGTERM) compositor.wait(timeout=5) for f in files:f.close()