Files
EscapistsReintroduced/tests/test_headless_input_proof.py
T

483 lines
30 KiB
Python

#!/usr/bin/env python3
"""Fake-only input policy tests; never load/connect native X11 or send input."""
import importlib.util
import os
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools'))
import headless_input_proof as proof
class Clock:
def __init__(self): self.now = 0
def __call__(self): return self.now
def sleep(self, seconds): self.now += seconds
class FakeObserver:
def __init__(self): self.events = []; self.polls = 0; self.fail = False
def poll(self):
self.polls += 1
if self.fail: raise proof.Refused('observer cancelled')
if not self.events: self.events.append('READY')
def fresh(self): self.poll()
class FakeGuard:
def __init__(self): self.calls = 0; self.fail = False
def check(self):
self.calls += 1
if self.fail: raise proof.Refused('focus lost')
class FakeKeyboard:
def __init__(self, observer):
self.observer = observer; self.events = []; self.held = None; self.on_send = None
def require_idle(self):
if self.held: raise proof.Refused('key already held')
def send(self, key, down):
self.events.append((key, down)); self.held = key if down else None
if key == 'Tab' and not down:
self.observer.events.append('INVENTORY_OPEN' if len(self.observer.events) == 1 else 'INVENTORY_CLOSED')
if self.on_send: self.on_send(key, down)
class SequenceTests(unittest.TestCase):
def setup_sequence(self):
self.clock = Clock(); self.observer = FakeObserver(); self.guard = FakeGuard()
self.keyboard = FakeKeyboard(self.observer); self.report = {'input_sent': False}
def run_sequence(self):
proof.sequence(self.guard, self.keyboard, self.observer, self.report, self.clock, self.clock.sleep)
def test_fixed_pairs_and_native_handshake(self):
self.setup_sequence(); self.run_sequence()
self.assertEqual(self.keyboard.events, [('W', True), ('W', False), ('Tab', True), ('Tab', False), ('Tab', True), ('Tab', False)])
self.assertTrue(self.report['sequence_completed'])
self.assertAlmostEqual(self.clock(), 3.4)
self.assertGreater(self.guard.calls, len(self.keyboard.events))
def test_focus_loss_aborts_and_attempts_release_without_foreign_event(self):
self.setup_sequence()
self.keyboard.on_send = lambda key, down: setattr(self.guard, 'fail', True) if down else None
with self.assertRaisesRegex(proof.Refused, 'focus lost'): self.run_sequence()
self.assertEqual(self.keyboard.events, [('W', True)])
self.assertEqual(self.report['unconfirmed_held_key'], 'W')
self.assertEqual(len(self.report['paired_release_errors']), 1)
self.assertFalse(self.report['sequence_completed'])
def test_observer_cancel_still_releases_on_proven_connection(self):
self.setup_sequence()
self.keyboard.on_send = lambda key, down: setattr(self.observer, 'fail', True) if down else None
with self.assertRaisesRegex(proof.Refused, 'observer cancelled'): self.run_sequence()
self.assertEqual(self.keyboard.events, [('W', True), ('W', False)])
self.assertIsNone(self.report['unconfirmed_held_key'])
def test_uncertain_native_down_exception_releases(self):
self.setup_sequence()
def fail(key, down):
if down: raise OSError('XTest returned unknown')
self.keyboard.on_send = fail
with self.assertRaises(OSError): self.run_sequence()
self.assertEqual(self.keyboard.events, [('W', True), ('W', False)])
def test_missing_inventory_ack_never_sends_second_tab(self):
self.setup_sequence()
def erase(key, down):
if key == 'Tab' and not down: self.observer.events = ['READY']
self.keyboard.on_send = erase
with self.assertRaisesRegex(proof.Refused, 'INVENTORY_OPEN'): self.run_sequence()
self.assertEqual(self.keyboard.events.count(('Tab', True)), 1)
self.assertLess(self.clock(), 25)
def test_final_native_key_state_is_required_before_completion(self):
self.setup_sequence()
calls = []
original = self.keyboard.require_idle
def idle():
calls.append(True)
original()
if len(calls) == 7:
raise proof.Refused('native key state not idle')
self.keyboard.require_idle = idle
with self.assertRaisesRegex(proof.Refused, 'native key state'): self.run_sequence()
self.assertEqual(len(self.keyboard.events), 6)
self.assertFalse(self.report['sequence_completed'])
self.assertEqual(len(calls), 7)
def test_no_ready_no_input(self):
self.setup_sequence(); self.observer.poll = lambda: None
with self.assertRaisesRegex(proof.Refused, 'READY'): self.run_sequence()
self.assertEqual(self.keyboard.events, [])
def test_existing_held_key_refuses_without_releasing_it(self):
self.setup_sequence(); self.keyboard.held = 'Other'
with self.assertRaisesRegex(proof.Refused, 'already held'): self.run_sequence()
self.assertEqual(self.keyboard.events, [])
class ReleaseStateTests(unittest.TestCase):
def test_busy_then_idle_polls_without_resending_input(self):
clock = Clock(); guard = FakeGuard(); report = {}; states = [True, True, False]
def idle():
if states.pop(0): raise proof.KeyStateBusy('still held')
keyboard = SimpleNamespace(require_idle=idle)
proof.confirm_release(guard, keyboard, report, 'W', clock, clock.sleep)
self.assertEqual(guard.calls, 3)
self.assertEqual(report['release_observations'][0]['attempts'], 3)
self.assertTrue(report['release_observations'][0]['confirmed'])
self.assertAlmostEqual(clock(), .05)
def test_persistent_state_remains_bounded_and_unconfirmed(self):
clock = Clock(); report = {}
def idle(): raise proof.KeyStateBusy('still held')
with self.assertRaisesRegex(proof.Refused, '350ms'):
proof.confirm_release(FakeGuard(), SimpleNamespace(require_idle=idle), report, 'W', clock, clock.sleep)
self.assertLessEqual(clock(), .35)
self.assertFalse(report['release_observations'][0]['confirmed'])
def test_focus_loss_and_native_query_error_are_never_retried(self):
for focus_lost in (False, True):
clock = Clock(); guard = FakeGuard(); guard.fail = focus_lost; calls = []
def idle():
calls.append(True)
raise proof.Refused('native query failed')
with self.assertRaises(proof.Refused):
proof.confirm_release(guard, SimpleNamespace(require_idle=idle), {}, 'W', clock, clock.sleep)
self.assertEqual(len(calls), 0 if focus_lost else 1)
self.assertEqual(clock(), 0)
def test_tab_release_has_one_fresh_guard_after_its_hold(self):
clock = Clock(); observer = FakeObserver(); guard = FakeGuard(); keyboard = FakeKeyboard(observer)
original = guard.check
def costly_guard():
original(); clock.sleep(.10)
guard.check = costly_guard; report = {'input_sent': False}
proof.sequence(guard, keyboard, observer, report, clock, clock.sleep)
for down, up in ((2, 3), (4, 5)):
self.assertAlmostEqual(report['events'][up]['monotonic'] - report['events'][down]['monotonic'], .15)
self.assertTrue(report['sequence_completed'])
def test_delayed_w_release_keeps_exact_six_events(self):
clock = Clock(); observer = FakeObserver(); guard = FakeGuard(); keyboard = FakeKeyboard(observer)
original = keyboard.require_idle; calls = []
def delayed():
calls.append(True)
if len(calls) == 2: raise proof.KeyStateBusy('delayed W state')
original()
keyboard.require_idle = delayed; report = {'input_sent': False}
proof.sequence(guard, keyboard, observer, report, clock, clock.sleep)
self.assertEqual(len(keyboard.events), 6)
self.assertEqual(report['release_observations'][0]['attempts'], 2)
self.assertTrue(report['all_attempted_presses_released'])
self.assertTrue(report['sequence_completed'])
class ObserverTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(); self.path = Path(self.tmp.name) / 'console.log'
self.path.write_text('ERR INPUT OBSERVE: run=test sample=1 real=1 world=1 READY\n')
self.clock = Clock(); self.observer = proof.Observer(self.path, 'test', self.clock)
def tearDown(self): self.observer.close(); self.tmp.cleanup()
def append(self, text):
with self.path.open('a') as f: f.write(text)
def record(self, sample, real, detail):
self.append(f'SCRIPT : ERR INPUT OBSERVE: run=test sample={sample} real={real} world={real}.000 {detail}\n')
def test_old_ready_does_not_authorize_and_new_exact_sequence_does(self):
self.observer.poll(); self.assertEqual(self.observer.events, [])
self.record(2, 20, 'SAMPLE origin=0 0 0'); self.record(2, 20, 'READY')
self.observer.fresh(); self.assertEqual(self.observer.events, ['READY'])
self.record(3, 30, 'SAMPLE origin=0 0 0'); self.record(3, 30, 'INVENTORY_OPEN')
self.observer.fresh(); self.record(4, 40, 'INVENTORY_CLOSED'); self.observer.poll()
self.assertEqual(self.observer.events, ['READY', 'INVENTORY_OPEN', 'INVENTORY_CLOSED'])
def test_stale_and_cancellation_fail_closed(self):
self.record(2, 20, 'SAMPLE x'); self.record(2, 20, 'READY'); self.observer.fresh()
self.clock.now = 1.01
with self.assertRaisesRegex(proof.Refused, 'stale'): self.observer.fresh()
self.record(3, 30, 'CANCELLED original-admission-lost')
with self.assertRaisesRegex(proof.Refused, 'ended'): self.observer.poll()
def test_reverse_clock_or_repeated_sample_refused(self):
self.record(2, 20, 'SAMPLE x'); self.observer.poll(); self.record(2, 20, 'SAMPLE x')
with self.assertRaisesRegex(proof.Refused, 'Repeated'): self.observer.poll()
def test_replace_truncate_and_matching_malformed_refused(self):
self.path.write_text('')
with self.assertRaisesRegex(proof.Refused, 'truncated'): self.observer.poll()
def test_different_nonce_ignored(self):
self.append('ERR INPUT OBSERVE: run=foreign sample=2 real=20 world=20 READY\n')
self.observer.poll(); self.assertEqual(self.observer.raw, [])
class GuardTests(unittest.TestCase):
def setUp(self):
self.args = SimpleNamespace(host_display=':0', prefix=Path('/tmp/p/pfx'), game_executable=Path('/tmp/game'), profile=Path('/tmp/profile'), nonce='test')
self.bound = {'display': ':2'}
self.owner = {'window': 100, 'pid': 20, 'server_pid': 20, 'process': {'pid': 20, 'start': 99}}
self.query = SimpleNamespace(prop=lambda *a: 20, server_pid=lambda *a: 20)
def test_full_live_gates_rechecked_before_acceptance(self):
with patch.object(proof.display, 'binding', return_value=self.bound) as binding, patch.object(proof.display, 'owned_window', return_value=self.owner), patch.object(proof, 'game_arguments') as argv, patch.object(proof.display, 'input_target', return_value={'verified': True}):
proof.Guard(self.args, self.bound, self.query, self.owner).check()
self.assertEqual(binding.call_count, 2); argv.assert_called_once()
def test_binding_window_focus_failures(self):
for bad in ('binding', 'window', 'focus'):
with self.subTest(bad=bad), patch.object(proof.display, 'binding', return_value={} if bad == 'binding' else self.bound), patch.object(proof.display, 'owned_window', return_value=None if bad == 'window' else self.owner), patch.object(proof, 'game_arguments'), patch.object(proof.display, 'input_target', return_value={'verified': bad != 'focus'}):
with self.assertRaises(proof.Refused): proof.Guard(self.args, self.bound, self.query, self.owner).check()
def test_native_argv_profile_nonce_are_exact_not_prefix_matches(self):
native = {'pid': 20, 'start': 99, 'args': ['exe', '-errOrdinaryInputObserve', 'test', '-errOrdinaryScenario', 'core', '-profile', 'Z:/tmp/profile']}
with patch.object(proof.display, 'process', return_value=native), patch.object(proof.display, 'identity', return_value=self.owner['process']):
proof.game_arguments(self.owner, self.args)
native['args'][-1] += '-neighbour'
with self.assertRaisesRegex(proof.Refused, 'profile'): proof.game_arguments(self.owner, self.args)
def test_opt_in_required(self):
with self.assertRaises(SystemExit), patch('sys.stderr'): proof.arguments([])
class SupervisionTests(unittest.TestCase):
def test_only_exact_child_identity_and_argv_can_receive_stop(self):
record = {'identity': {'pid': 222, 'start': 999}, 'command': ['python', 'owned-wrapper']}
native = {'pid': 222, 'start': 999, 'uid': os.getuid(), 'args': record['command']}
with patch.object(proof.display, 'identity', side_effect=lambda p: {'pid': p['pid'], 'start': p['start']}):
for change in ({}, {'start': 1000}, {'args': ['other']}):
sent = []
accepted = proof.stop_exact_child(record, lambda p: {**native, **change}, lambda *v: sent.append(v))
self.assertEqual(accepted, not bool(change))
self.assertEqual(len(sent), 0 if change else 1)
def test_timeout_records_uncertain_press_in_exact_report(self):
import json
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / 'proof.json'; path.write_text('')
info = path.stat()
record = {'path': str(path), 'device': info.st_dev, 'inode': info.st_ino,
'metadata': {'schema': 1, 'input_sent': False}}
proof.supervision_failure(record, 'W', 'timeout')
result = json.loads(path.read_text())
self.assertTrue(result['supervision_failed']); self.assertTrue(result['input_attempted'])
self.assertFalse(result['sequence_completed']); self.assertFalse(result['all_attempted_presses_released'])
self.assertEqual(result['unconfirmed_held_key'], 'W')
record['inode'] += 1
with self.assertRaises(proof.Refused): proof.supervision_failure(record, None, 'wrong inode')
def test_supervisor_preserves_worker_error_and_uncertain_release(self):
import json
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / 'proof.json'
path.write_text(json.dumps({'error': 'actual keymap error', 'all_attempted_presses_released': False,
'unconfirmed_held_key': 'Tab'}))
info = path.stat()
record = {'path': str(path), 'device': info.st_dev, 'inode': info.st_ino, 'metadata': {}}
proof.supervision_failure(record, None, 'worker exited')
result = json.loads(path.read_text())
self.assertEqual(result['error'], 'actual keymap error')
self.assertEqual(result['supervision_error'], 'worker exited')
self.assertFalse(result['all_attempted_presses_released'])
self.assertEqual(result['unconfirmed_held_key'], 'Tab')
def test_press_watchdog_terminates_worker_and_owned_wrapper_without_retry(self):
events = [{'kind': 'report', 'metadata': {}}, {'kind': 'child', 'identity': {}}, {'kind': 'press-pending', 'key': 'W'}]
class Pipe:
def poll(self, *a): return bool(events)
def recv(self): return events.pop(0)
def close(self): pass
class Process:
pid = 123; exitcode = 0; alive = True; terminated = 0
def start(self): pass
def is_alive(self): return self.alive
def terminate(self): self.terminated += 1
def join(self, *a): self.alive = False
def kill(self): self.alive = False
child = Process(); pipe = Pipe()
context = SimpleNamespace(Pipe=lambda **k: (pipe, pipe), Process=lambda **k: child)
times = iter([0, 0, 0, 0, 2])
with patch.object(proof.multiprocessing, 'get_context', return_value=context), patch.object(proof.time, 'monotonic', side_effect=lambda: next(times)), patch.object(proof.signal, 'signal'), patch.object(proof, 'stop_exact_child') as stop, patch.object(proof, 'supervision_failure') as failed:
self.assertEqual(proof.supervise([]), 1)
self.assertEqual(child.terminated, 1); stop.assert_called_once()
self.assertEqual(failed.call_args.args[1], 'W')
def test_look_watchdog_stops_exact_child_without_key_or_mouse_retry(self):
events = [{'kind': 'report', 'metadata': {}}, {'kind': 'child', 'identity': {}}, {'kind': 'look-pending'}]
class Pipe:
def poll(self, *a): return bool(events)
def recv(self): return events.pop(0)
def close(self): pass
class Process:
pid = 123; exitcode = 0; alive = True; terminated = 0
def start(self): pass
def is_alive(self): return self.alive
def terminate(self): self.terminated += 1
def join(self, *a): self.alive = False
def kill(self): self.alive = False
child = Process(); pipe = Pipe()
context = SimpleNamespace(Pipe=lambda **k: (pipe, pipe), Process=lambda **k: child)
times = iter([0, 0, 0, 0, 4.01])
with patch.object(proof.multiprocessing, 'get_context', return_value=context), patch.object(proof.time, 'monotonic', side_effect=lambda: next(times)), patch.object(proof.signal, 'signal'), patch.object(proof, 'stop_exact_child') as stop, patch.object(proof, 'supervision_failure') as failed:
self.assertEqual(proof.supervise([]), 1)
self.assertEqual(child.terminated, 1); stop.assert_called_once()
self.assertIsNone(failed.call_args.args[1])
def test_wrapper_requires_matching_existing_readonly_wrapper(self):
args = SimpleNamespace(command=['/bin/echo', 'bad'], console=None)
with self.assertRaises(proof.Refused): proof.wrapper_command(args)
args.command = []
with self.assertRaises(proof.Refused): proof.wrapper_command(args)
args.console = Path('/tmp/exact/console.log')
self.assertEqual(proof.wrapper_command(args), [])
def test_ctypes_keyboard_uses_exact_xtest_signatures_and_no_native_library(self):
class Function:
def __init__(self, call): self.call = call
def __call__(self, *args): return self.call(*args)
xt = SimpleNamespace(XTestQueryExtension=Function(lambda *a: 1), XTestFakeKeyEvent=Function(lambda *a: 1))
x = SimpleNamespace(XKeysymToKeycode=Function(lambda d,k: 25 if k == 0x77 else 23),
XKeycodeToKeysym=Function(lambda d,c,l: 0x77 if c == 25 else 0xff09),
XQueryKeymap=Function(lambda *a: 1), XFlush=Function(lambda *a: 0))
with patch.object(proof.C, 'CDLL', return_value=xt):
keyboard = proof.Keyboard(SimpleNamespace(x=x, d=123))
keyboard.require_idle(); keyboard.send('W', True); keyboard.send('W', False)
self.assertEqual(xt.XTestFakeKeyEvent.argtypes, [proof.C.c_void_p, proof.C.c_uint, proof.C.c_int, proof.C.c_ulong])
self.assertEqual(keyboard.codes, {'W': 25, 'Tab': 23})
with self.assertRaises(proof.Refused): keyboard.send('Escape', True)
class LookObserver(FakeObserver):
def __init__(self):
super().__init__(); self.events=['READY']; self.ready_sample=10
self.look_records=[{'kind':'basis','sample':1,'real_ms':100}]; self.input_samples=[]
self.add(10,0); self.add(11,0)
def add(self,index,angle,position=None):
import math
pos=position or [0.,0.,0.]
self.look_records.append({'kind':'sample','sample':index,'real_ms':index*100,
'direction':[math.sin(math.radians(angle)),0.,math.cos(math.radians(angle))],
'camera':[0.,1.7,0.],'body':pos,'bodyForward':[0.,0.,1.],'turn':[0.,0.,0.]})
self.input_samples.append({'sample':index,'real_ms':index*100,'origin':pos,'forward':0.,
'input':[0.,0.,0.],'velocity':[0.,0.,0.],'inventory':False})
class LookMouse:
def __init__(self,observer,clock): self.observer=observer;self.clock=clock;self.events=[];self.ack=True;self.returned=True
def move(self,dx,dy,guard):
guard.check();self.events.append((dx,dy));self.clock.sleep(.01)
if self.ack:
self.observer.add(12 if dx>0 else 13,2 if dx>0 or not self.returned else 0)
class LookTests(unittest.TestCase):
def setup_look(self):
self.clock=Clock();self.observer=LookObserver();self.guard=FakeGuard()
self.keyboard=FakeKeyboard(self.observer);self.mouse=LookMouse(self.observer,self.clock);self.report={}
def perform(self):
proof.perform_look(self.guard,self.keyboard,self.observer,self.mouse,self.report,self.clock,self.clock.sleep)
def test_two_native_acknowledgements_before_any_keyboard(self):
self.setup_look();self.perform()
p=self.report['look_proof']
self.assertTrue(p['completed']);self.assertEqual(p['baseline_samples'],[10,11])
self.assertEqual((p['response_sample'],p['return_sample']),(12,13))
self.assertEqual([(m['dx'],m['dy']) for m in p['motions']],[(24,0),(-24,0)])
self.assertEqual([m['after_sample'] for m in p['motions']],[11,12])
self.assertEqual(self.keyboard.events,[]);self.assertLessEqual(p['finished_monotonic']-p['started_monotonic'],4)
def test_missing_native_rotation_no_blind_return_or_keyboard(self):
self.setup_look();self.mouse.ack=False
with self.assertRaisesRegex(proof.Refused,'acknowledgement|four seconds'):self.perform()
self.assertEqual(self.mouse.events,[(24,0)]);self.assertEqual(self.keyboard.events,[])
self.assertFalse(self.report['look_proof']['completed'])
def test_failed_return_is_not_assumed_from_opposite_pixels(self):
self.setup_look();self.mouse.returned=False
with self.assertRaises(proof.Refused):self.perform()
self.assertEqual(self.mouse.events,[(24,0),(-24,0)])
self.assertFalse(self.report['look_proof']['completed'])
def test_early_motion_inventory_or_cancel_refuses_before_mouse(self):
for fault in ('origin','input','velocity','forward','inventory','cancel','clock'):
self.setup_look()
if fault=='origin':
self.observer.look_records[-1]['body']=[1.,0.,0.];self.observer.input_samples[-1]['origin']=[1.,0.,0.]
elif fault in ('input','velocity'):self.observer.input_samples[-1][fault]=[0.,0.,1.]
elif fault=='forward':self.observer.input_samples[-1]['forward']=1.
elif fault=='inventory':self.observer.input_samples[-1]['inventory']=True
elif fault=='clock':self.observer.input_samples[-1]['real_ms']+=1
else:self.observer.look_records.append({'kind':'cancelled','sample':11,'real_ms':1100})
with self.subTest(fault=fault),self.assertRaises(proof.Refused):self.perform()
self.assertEqual(self.mouse.events,[])
def test_default_sequence_has_no_look_or_mouse_and_optin_failure_blocks_w(self):
self.setup_look();self.mouse.ack=False
with self.assertRaises(proof.Refused):proof.sequence(self.guard,self.keyboard,self.observer,self.report,self.clock,self.clock.sleep,mouse=self.mouse)
self.assertEqual(self.keyboard.events,[])
self.setup_look();proof.sequence(self.guard,self.keyboard,self.observer,self.report,self.clock,self.clock.sleep)
self.assertNotIn('look_proof',self.report);self.assertEqual(len(self.keyboard.events),6)
def test_strict_native_look_parser_and_vectors(self):
prefix='SCRIPT : ERR LOOK OBSERVE: run=N sample=1 real=100 '
sample='SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>'
result=proof.parse_look_record(prefix+sample,'N');self.assertEqual(result['direction'],[0,0,1])
self.assertEqual(proof.parse_look_record(prefix+'BASIS native-player-camera-world-transform','N')['kind'],'basis')
for invalid in (sample.replace('direction=<0,0,1>','direction=<0,0,0>'),sample.replace('camera=<0,1.7,0>','camera=<NaN,1.7,0>'),sample+' extra'):
with self.assertRaises(proof.Refused):proof.parse_look_record(prefix+invalid,'N')
with self.assertRaises(proof.Refused):proof.parse_look_record(prefix+sample,'foreign')
self.assertAlmostEqual(proof.yaw_delta([.01745,0,-.99985],[-.01745,0,-.99985]),2,places=2)
def test_stale_pose_and_sampling_gap_refuse_before_mouse(self):
self.setup_look()
self.observer.input_samples.append({**self.observer.input_samples[-1], 'sample': 17, 'real_ms': 1700})
with self.assertRaisesRegex(proof.Refused, 'stale'): self.perform()
self.assertEqual(self.mouse.events, [])
self.setup_look(); self.observer.add(13, 0)
with self.assertRaisesRegex(proof.Refused, 'sampling gap'): self.perform()
self.assertEqual(self.mouse.events, [])
def test_real_observer_joins_pinned_console_and_keeps_later_menu_cancel(self):
with tempfile.TemporaryDirectory() as folder:
path=Path(folder)/'console.log'; path.write_text('old irrelevant lines\n')
observer=proof.Observer(path,'N',clock=lambda: 1.,enable_look=True)
try:
with path.open('a') as out:
out.write('ERR INPUT OBSERVE: run=N sample=1 real=100 world=100 SAMPLE origin=<0,0,0> forward=0 input=<0,0,0> velocity=<0,0,0> inventory=0\n')
out.write('ERR INPUT OBSERVE: run=N sample=1 real=100 world=100 READY\n')
out.write('ERR LOOK OBSERVE: run=N sample=1 real=100 BASIS native-player-camera-world-transform\n')
out.write('ERR LOOK OBSERVE: run=N sample=1 real=100 SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>\n')
observer.fresh()
self.assertEqual(observer.ready_sample,1)
self.assertEqual(observer.input_samples[0]['origin'],observer.look_records[-1]['body'])
self.assertEqual(observer.input_samples[0]['real_ms'],observer.look_records[-1]['real_ms'])
with path.open('a') as out:
out.write('ERR LOOK OBSERVE: run=N sample=2 real=200 CANCELLED menu-opened\n')
out.write('ERR INPUT OBSERVE: run=N sample=2 real=200 world=200 SAMPLE origin=<0,0,0> forward=0 input=<0,0,0> velocity=<0,0,0> inventory=1\n')
out.write('ERR INPUT OBSERVE: run=N sample=2 real=200 world=200 INVENTORY_OPEN\n')
observer.fresh()
self.assertEqual(observer.events,['READY','INVENTORY_OPEN'])
self.assertEqual(observer.look_records[-1]['reason'],'menu-opened')
with path.open('a') as out:
out.write('ERR LOOK OBSERVE: run=N sample=3 real=300 SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>\n')
with self.assertRaisesRegex(proof.Refused,'after cancellation'):observer.poll()
finally:observer.close()
def test_real_main_report_binds_look_optin_even_before_console_failure(self):
import json
for enabled in (False,True):
with self.subTest(enabled=enabled),tempfile.TemporaryDirectory() as folder:
args=SimpleNamespace(enable_private_look_proof=enabled,nonce='N',
report=Path(folder)/'proof.json',console=Path(folder)/'console.log',
prefix=Path(folder)/'pfx',profile=Path(folder),game_executable=Path(folder)/'game',host_display=':0')
with patch.object(proof,'arguments',return_value=args),patch.object(proof,'wrapper_command',return_value=[]),patch.object(proof,'console_path'),patch.object(proof.display,'validate_paths'),patch.object(proof.display,'binding',return_value={'display':':2'}),patch.object(proof,'Observer',side_effect=OSError('bounded test stop')),patch.object(proof.signal,'signal'):
self.assertEqual(proof.main([]),1)
report=json.loads(args.report.read_text())
self.assertIs(report['look_requested'],enabled)
self.assertFalse(report['input_sent'])
self.assertEqual(report['error'],'bounded test stop')
def test_cli_mouse_optin_requires_keyboard_optin(self):
with self.assertRaises(SystemExit),patch('sys.stderr'):
proof.arguments(['--enable-private-look-proof'])
class PointerTests(unittest.TestCase):
def test_pointer_bounds_root_masks_and_child_pid(self):
mouse=object.__new__(proof.RelativeMouse)
mouse.owner={'window':100,'pid':9};mouse.query=SimpleNamespace(server_pid=lambda child:9 if child==77 else 88)
target={'geometry':{'root':5},'focused_clipped_bounds':[0,0,100,100]}
base={'root':5,'child':77,'root_x':50,'root_y':50,'mask':0}
mouse.pointer=lambda:base.copy();mouse.validate_pointer(target,24,0);mouse.validate_pointer(target,-24,0)
for change in ({'root':6},{'mask':1},{'mask':256},{'root_x':90},{'root_y':101},{'child':78}):
mouse.pointer=lambda:{**base,**change}
with self.subTest(change=change),self.assertRaises(proof.Refused):mouse.validate_pointer(target,24,0)
def test_only_fixed_relative_events_use_same_connection(self):
class Fn:
def __init__(self,fn):self.fn=fn
def __call__(self,*args):return self.fn(*args)
events=[];xt=SimpleNamespace(XTestFakeRelativeMotionEvent=Fn(lambda *args:events.append(args) or 1))
x=SimpleNamespace(XQueryPointer=Fn(lambda *a:1),XFlush=lambda d:None)
keyboard=SimpleNamespace(xt=xt,require_idle=lambda:None);query=SimpleNamespace(x=x,d=123)
mouse=proof.RelativeMouse(query,keyboard,{'window':100,'pid':9});mouse.validate_pointer=lambda *a:None
guard=SimpleNamespace(check=lambda:{'verified':True})
mouse.move(24,0,guard);mouse.move(-24,0,guard)
self.assertEqual(events,[(123,24,0,0),(123,-24,0,0)])
self.assertEqual(xt.XTestFakeRelativeMotionEvent.argtypes,[proof.C.c_void_p,proof.C.c_int,proof.C.c_int,proof.C.c_ulong])
for dx,dy in ((25,0),(24,1),(True,0)):
with self.assertRaises(proof.Refused):mouse.move(dx,dy,guard)
if __name__ == '__main__': unittest.main()