Verify private relative mouse input against native player camera
This commit is contained in:
@@ -281,6 +281,27 @@ class SupervisionTests(unittest.TestCase):
|
||||
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)
|
||||
@@ -304,4 +325,158 @@ class SupervisionTests(unittest.TestCase):
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user