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()
|
||||
|
||||
@@ -7,7 +7,7 @@ def cpp(s):
|
||||
s=s.replace(t+' mode',t+'* mode').replace(t+' controller',t+'* controller').replace(t+' actor',t+'* actor').replace(t+' life',t+'* life').replace(t+' request',t+'* request').replace(t+' current',t+'* current').replace(t+' manager',t+'* manager').replace(t+' menu',t+'* menu').replace(t+' root',t+'* root').replace(t+' expectedMenu',t+'* expectedMenu').replace(t+' expectedRoot',t+'* expectedRoot').replace(t+' overlay',t+'* overlay')
|
||||
for v in ('mode','controller','actor','request','life','current','manager','menu','root','m_Mode','m_World','m_Controller','m_Actor','m_Character','m_Menu','s_Request','expectedMenu','expectedRoot','m_CaptureMenu','m_CaptureRoot','overlay','m_aHitZonePoints'):
|
||||
s=s.replace(v+'.',v+'->')
|
||||
for scope in ('System','Replication','ERR_Phase','ERR_GameMode','ERR_Actors','SCR_InventoryMenuUI','ERR_OrdinaryInputObserverRules','vector','Math'):
|
||||
for scope in ('System','Replication','ERR_Phase','ERR_GameMode','ERR_Actors','SCR_InventoryMenuUI','ERR_OrdinaryInputObserverRules','ERR_OrdinaryLookObserver','vector','Math'):
|
||||
s=s.replace(scope+'.',scope+'::')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('GetInputManager().','GetInputManager()->').replace('GetMenuManager().','GetMenuManager()->').replace('GetCallqueue().','GetCallqueue()->').replace('Players().','Players()->')
|
||||
s=s.replace('CharacterControllerComponent.Cast(actor->FindComponent(CharacterControllerComponent))','actor->FindComponent(0)').replace('FindComponent(CharacterControllerComponent)','FindComponent(0)')
|
||||
@@ -60,6 +60,12 @@ void PrintFormat(const char*,string nonce,int,int,float,vector,float,vector,vect
|
||||
struct ERR_OrdinaryInputObserverRules {
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct ERR_OrdinaryLookObserver {
|
||||
static void Sample(ERR_GameMode* mode,BaseWorld* world,PlayerController* controller,IEntity* actor,string nonce,int started,int index,int now){
|
||||
assert(mode==ERR_GameMode::Get()&&world==game.world&&controller==game.controller&&actor==controller->actor);
|
||||
assert(nonce=="Run42"&&index==samples&&now==System::now&&now>=started);
|
||||
}
|
||||
};
|
||||
struct ERR_OrdinaryInputObserver {
|
||||
static ERR_OrdinaryInputObserver* s_Request;static bool s_Attempted;
|
||||
ERR_GameMode* m_Mode=nullptr;BaseWorld* m_World=nullptr;PlayerController* m_Controller=nullptr;IEntity* m_Actor=nullptr;CharacterControllerComponent* m_Character=nullptr;ERR_PlayerLife* m_Life=nullptr;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Adversarial raw-record tests; no native input, camera or display operations."""
|
||||
import copy
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools'))
|
||||
from ordinary_look_evidence import validate_look_evidence
|
||||
|
||||
NONCE = 'nativeLook123'
|
||||
|
||||
def fixture(sign=1, base=0):
|
||||
def vector(yaw):
|
||||
r = math.radians(yaw)
|
||||
return '<%.9f,0,%.9f>' % (math.sin(r), math.cos(r))
|
||||
lines = [f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample=0 real=1000 world=5000 QUEUED']
|
||||
for i in range(1, 31):
|
||||
clock = 1000+i*100
|
||||
opened = i >= 29
|
||||
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} SAMPLE origin=<100,20,300> forward=0 input=<0,0,0> velocity=<0,0,0> inventory={int(opened)}')
|
||||
if i == 20:
|
||||
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} READY')
|
||||
if i == 1:
|
||||
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample=1 real={clock} BASIS native-player-camera-world-transform')
|
||||
if i < 29:
|
||||
yaw = base + (sign*2 if 23 <= i <= 25 else sign*.1 if i >= 26 else 0)
|
||||
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample={i} real={clock} SAMPLE direction={vector(yaw)} camera=<100,21.7,300> body=<100,20,300> bodyForward=<0,0,1> turn=<0,0,0>')
|
||||
elif i == 29:
|
||||
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample={i} real={clock} CANCELLED menu-opened')
|
||||
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} INVENTORY_OPEN')
|
||||
proof = dict(requested=True, completed=True, started_monotonic=10.0, finished_monotonic=11.0,
|
||||
baseline_samples=[21,22], response_sample=24, return_sample=27,
|
||||
motions=[dict(dx=24,dy=0,monotonic=10.3,after_sample=22,after_real_ms=3200),
|
||||
dict(dx=-24,dy=0,monotonic=10.6,after_sample=24,after_real_ms=3400)])
|
||||
keys = [dict(key='W', down=True, monotonic=11.1), dict(key='W', down=False, monotonic=11.4)]
|
||||
return '\n'.join(lines)+'\n', proof, keys
|
||||
|
||||
class LookEvidenceTests(unittest.TestCase):
|
||||
def verdict(self, content=None, proof=None, keys=None):
|
||||
raw,p,k=fixture()
|
||||
return validate_look_evidence(raw if content is None else content,NONCE,p if proof is None else proof,k if keys is None else keys)
|
||||
def reject(self, **changes):
|
||||
result=self.verdict(**changes)
|
||||
self.assertFalse(result['verified'], result)
|
||||
self.assertTrue(result['errors'])
|
||||
def test_native_recomputed_positive_and_sign_and_wrap(self):
|
||||
for sign,base in ((1,0),(-1,0),(1,179),(-1,-179)):
|
||||
raw,p,k=fixture(sign,base)
|
||||
result=validate_look_evidence(raw,NONCE,p,k)
|
||||
self.assertTrue(result['verified'], result)
|
||||
self.assertAlmostEqual(result['response_yaw_degrees'],sign*2,places=5)
|
||||
self.assertAlmostEqual(result['return_yaw_degrees'],sign*.1,places=5)
|
||||
self.assertTrue(result['keyboard_order_verified'])
|
||||
def test_ready_separate_clock_read_binds_latest_sample(self):
|
||||
raw,p,k=fixture()
|
||||
marker=f'ERR INPUT OBSERVE: run={NONCE} sample=20 real=3000 world=7000 READY'
|
||||
# Actual native shape: SAMPLE, then LOOK at T, then READY at T+1.
|
||||
ready_line=next(line for line in raw.splitlines() if marker in line)
|
||||
look_line=next(line for line in raw.splitlines() if 'sample=20 real=3000 SAMPLE direction=' in line)
|
||||
changed=raw.replace(ready_line+'\n','').replace(look_line+'\n',look_line+'\n'+ready_line.replace('real=3000','real=3001')+'\n')
|
||||
result=validate_look_evidence(changed,NONCE,p,k)
|
||||
self.assertTrue(result['verified'],result)
|
||||
for replacement in ('sample=19 real=3001 world=7000', 'sample=20 real=2999 world=7000',
|
||||
'sample=20 real=3501 world=7000', 'sample=20 real=3001 world=7001',
|
||||
'sample=20 real=3101 world=7000'):
|
||||
self.reject(content=changed.replace('sample=20 real=3001 world=7000 READY',replacement+' READY'))
|
||||
# READY cannot bind an older sample once the next SAMPLE was emitted.
|
||||
line=ready_line.replace('real=3000','real=3001')
|
||||
next_line=next(x for x in changed.splitlines() if 'sample=21 real=3100 world=7100 SAMPLE' in x)
|
||||
self.reject(content=changed.replace(line+'\n','').replace(next_line+'\n',next_line+'\n'+line+'\n'))
|
||||
self.reject(content=changed.replace(line+'\n',line+'\n'+line+'\n'))
|
||||
|
||||
def test_optional_keyboard_is_explicitly_unverified(self):
|
||||
raw,p,_=fixture(); result=validate_look_evidence(raw,NONCE,p)
|
||||
self.assertTrue(result['verified'],result)
|
||||
self.assertFalse(result['keyboard_order_verified'])
|
||||
def test_missing_ack_foreign_nonce_and_unknown_events(self):
|
||||
raw,_,_=fixture()
|
||||
for changed in (raw.replace('BASIS native-player-camera-world-transform','BASIS guessed'),
|
||||
'\n'.join(x for x in raw.splitlines() if ' BASIS ' not in x),
|
||||
raw.replace('ERR LOOK OBSERVE: run='+NONCE,'ERR LOOK OBSERVE: run=foreign',1),
|
||||
raw.replace('ERR INPUT OBSERVE: run='+NONCE,'ERR INPUT OBSERVE: run=foreign',1),
|
||||
raw+'ERR LOOK OBSERVE: malformed\n',
|
||||
raw.replace('CANCELLED menu-opened','CANCELLED invented'),
|
||||
raw+'ERR INPUT OBSERVE: run='+NONCE+' sample=30 real=4000 world=8000 UNKNOWN\n'):
|
||||
self.reject(content=changed)
|
||||
def test_duplicate_missing_reversed_native_samples(self):
|
||||
raw,_,_=fixture(); lines=raw.splitlines()
|
||||
for predicate in ('sample=24 real=3400 SAMPLE direction=', 'sample=24 real=3400 world=7400 SAMPLE'):
|
||||
index=next(i for i,line in enumerate(lines) if predicate in line)
|
||||
duplicate=lines[:index]+[lines[index]]+lines[index:]
|
||||
self.reject(content='\n'.join(duplicate))
|
||||
self.reject(content='\n'.join(lines[:index]+lines[index+1:]))
|
||||
self.reject(content=raw.replace('sample=24 real=3400 SAMPLE direction=', 'sample=24 real=3199 SAMPLE direction='))
|
||||
self.reject(content=raw.replace('world=7400 SAMPLE','world=7000 SAMPLE'))
|
||||
def test_cancel_before_return_or_revival_refused(self):
|
||||
raw,_,_=fixture()
|
||||
self.reject(content=raw.replace('sample=29 real=3900 CANCELLED','sample=26 real=3600 CANCELLED'))
|
||||
extra=next(x for x in raw.splitlines() if 'sample=28 real=3800 SAMPLE direction=' in x)
|
||||
self.reject(content=raw+extra+'\n')
|
||||
def test_strict_proof_shape_types_timestamps_and_fixed_motions(self):
|
||||
_,p,_=fixture()
|
||||
mutations=[lambda x:x.update(completed=False),lambda x:x.update(requested=1),
|
||||
lambda x:x.update(extra='claimed'),lambda x:x.update(started_monotonic=True),
|
||||
lambda x:x.update(finished_monotonic=float('nan')),lambda x:x.update(finished_monotonic=15),
|
||||
lambda x:x.update(baseline_samples=[20,22]),lambda x:x.update(baseline_samples=[True,22]),
|
||||
lambda x:x.update(response_sample=28),lambda x:x.update(return_sample=24),
|
||||
lambda x:x['motions'][0].update(dx=-24),lambda x:x['motions'][0].update(dy=1),
|
||||
lambda x:x['motions'][0].update(after_real_ms=3199),lambda x:x['motions'][1].update(after_sample=25),
|
||||
lambda x:x['motions'][1].update(monotonic=10.2),lambda x:x['motions'].append(copy.deepcopy(x['motions'][0])),
|
||||
lambda x:x['motions'][0].update(native_yaw_claim=99)]
|
||||
for mutation in mutations:
|
||||
value=copy.deepcopy(p);mutation(value)
|
||||
with self.subTest(value=value):self.reject(proof=value)
|
||||
def test_raw_stationary_input_and_body_not_helper_claim(self):
|
||||
raw,_,_=fixture()
|
||||
input_line=next(x for x in raw.splitlines() if 'sample=23 real=3300 world=7300 SAMPLE' in x)
|
||||
for field,value in (('forward=0','forward=0.5'),('input=<0,0,0>','input=<0,0,1>'),
|
||||
('velocity=<0,0,0>','velocity=<0.2,0,0>'),('origin=<100,20,300>','origin=<100.1,20,300>'),('inventory=0','inventory=1')):
|
||||
self.reject(content=raw.replace(input_line,input_line.replace(field,value)))
|
||||
pose=next(x for x in raw.splitlines() if 'sample=23 real=3300 SAMPLE direction=' in x)
|
||||
self.reject(content=raw.replace(pose,pose.replace('body=<100,20,300>','body=<101,20,300>')))
|
||||
# Also reject movement after READY but before the selected baseline.
|
||||
early=next(x for x in raw.splitlines() if 'sample=20 real=3000 world=7000 SAMPLE' in x)
|
||||
self.reject(content=raw.replace(early,early.replace('forward=0','forward=1')))
|
||||
def test_invalid_pose_unit_nonfinite_no_response_and_no_return(self):
|
||||
raw,_,_=fixture()
|
||||
for vector in ('<0,0,0>','<0,0,2>','<0,1,0>','<nan,0,1>','<1e999,0,1>','<1,2>'):
|
||||
self.reject(content=raw.replace('direction=<0.000000000,0,1.000000000>', 'direction='+vector,1))
|
||||
p24=next(x for x in raw.splitlines() if 'sample=24 real=3400 SAMPLE direction=' in x)
|
||||
p27=next(x for x in raw.splitlines() if 'sample=27 real=3700 SAMPLE direction=' in x)
|
||||
direction=p24.split('direction=')[1].split(' camera=')[0]
|
||||
self.reject(content=raw.replace(p24,p24.replace(direction,'<0,0,1>')))
|
||||
old=p27.split('direction=')[1].split(' camera=')[0]
|
||||
self.reject(content=raw.replace(p27,p27.replace(old,direction)))
|
||||
def test_keyboard_before_return_unknown_or_reversed_refused(self):
|
||||
_,_,keys=fixture()
|
||||
for changed in ([],[dict(key='Tab',down=True,monotonic=11.1)],
|
||||
[dict(key='W',down=True,monotonic=10.9)],
|
||||
[dict(key='W',down=1,monotonic=11.1)],
|
||||
[dict(key='W',down=True,monotonic=float('inf'))],list(reversed(keys))):
|
||||
self.reject(keys=changed)
|
||||
def test_invalid_top_level_and_nonce_never_throw(self):
|
||||
raw,p,k=fixture()
|
||||
for value in (None,[],{},True):
|
||||
self.assertFalse(validate_look_evidence(raw,NONCE,value,k)['verified'])
|
||||
for nonce in ('','../x','a'*33,None):
|
||||
self.assertFalse(validate_look_evidence(raw,nonce,p,k)['verified'])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Execute actual observer methods against fake read-only native boundaries."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/ERR_OrdinaryLookObserver.c'
|
||||
def cpp(s):
|
||||
s=s.replace('protected ', '')
|
||||
types=('ERR_GameMode','BaseWorld','PlayerController','IEntity','PlayerCamera','ERR_PlayerLife','PlayerManager','CameraManager')
|
||||
for typ in types:s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for name in ('mode','world','controller','actor','manager','cameras','m_Mode','m_World','m_Controller','m_Actor','m_Camera','m_Life','s_Request'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for scope in ('System','Replication','RplSession','RplMode','ERR_Phase','ERR_GameMode','ERR_Actors','ERR_OrdinaryInputObserverRules'):
|
||||
s=s.replace(scope+'.',scope+'::')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('Players().','Players()->').replace('GetInputManager().','GetInputManager()->').replace('GetMenuManager().','GetMenuManager()->')
|
||||
s=s.replace('null','nullptr').replace('new ERR_OrdinaryLookObserver()', 'new ERR_OrdinaryLookObserver()')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,signature)) for signature in (
|
||||
'protected static bool Enabled(', 'static void Sample(', 'protected void Cancel(', 'protected bool Context(', 'protected static bool FiniteVector(', 'protected void Observe('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;using UUID=string;
|
||||
struct vector {float v[3]={0,0,0}; float& operator[](int i){return v[i];}float Length(){return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}vector operator/(float n){return {{v[0]/n,v[1]/n,v[2]/n}};}};
|
||||
vector Vector(float a,float b,float c){return {{a,b,c}};}
|
||||
struct BaseWorld {};
|
||||
struct IEntity {BaseWorld* world=nullptr;bool living=true; vector matrix[4]={Vector(1,0,0),Vector(0,1,0),Vector(0,0,1),Vector(100,2,100)};BaseWorld* GetWorld(){return world;}void GetTransform(vector out[4]){for(int i=0;i<4;i++)out[i]=matrix[i];}};
|
||||
struct CameraManager;
|
||||
struct PlayerCamera:IEntity {CameraManager* replaceDuringRead=nullptr;void GetWorldCameraTransform(vector out[4]);};
|
||||
struct CameraManager {PlayerCamera* current=nullptr;PlayerCamera* CurrentCamera(){return current;}};
|
||||
void PlayerCamera::GetWorldCameraTransform(vector out[4]){GetTransform(out);if(replaceDuringRead)replaceDuringRead->current=nullptr;}
|
||||
struct PlayerController {int id=1;IEntity* actor=nullptr;PlayerCamera* camera=nullptr;int GetPlayerId(){return id;}IEntity* GetControlledEntity(){return actor;}PlayerCamera* GetPlayerCamera(){return camera;}};
|
||||
struct PlayerManager {PlayerController* controller=nullptr;IEntity* actor=nullptr;PlayerController* GetPlayerController(int){return controller;}IEntity* GetPlayerControlledEntity(int){return actor;}};
|
||||
struct ERR_PlayerLife {UUID AccountId="account";};
|
||||
struct Roster {ERR_PlayerLife* life=nullptr;IEntity* actor=nullptr;ERR_PlayerLife* CurrentActor(IEntity* value){return value==actor?life:nullptr;}};
|
||||
namespace ERR_Phase {enum {PRISON,EXTRACTION};}
|
||||
struct ERR_GameMode {static ERR_GameMode* current;bool active=true;int phase=ERR_Phase::PRISON;BaseWorld* world=nullptr;Roster* roster=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}BaseWorld* GetWorld(){return world;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct InputManager {float right=0,up=0;float GetActionValue(string name){return name=="CharacterTurnRight"?right:up;}};
|
||||
struct MenuManager {bool open=false;void* GetTopMenu(){return open?this:nullptr;}};
|
||||
struct Game {BaseWorld* world=nullptr;PlayerController* controller=nullptr;PlayerManager* players=nullptr;CameraManager* cameras=nullptr;InputManager* input=nullptr;MenuManager* menus=nullptr;BaseWorld* GetWorld(){return world;}PlayerController* GetPlayerController(){return controller;}PlayerManager* GetPlayerManager(){return players;}CameraManager* GetCameraManager(){return cameras;}InputManager* GetInputManager(){return input;}MenuManager* GetMenuManager(){return menus;}} game;Game* activeGame=&game;Game* GetGame(){return activeGame;}
|
||||
namespace System {bool console=false;std::map<string,string> cli;bool IsConsoleApp(){return console;}bool GetCLIParam(string key,string& value){if(!cli.count(key))return false;value=cli[key];return true;}}
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace RplMode {enum {None,Listen,Dedicated};}namespace RplSession {int mode=RplMode::None;int Mode(){return mode;}}
|
||||
namespace ERR_OrdinaryInputObserverRules {bool Nonce(string n){return !n.empty()&&n.size()<=32&&n.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")==string::npos;}}
|
||||
namespace ERR_Actors {bool CanFight(IEntity* actor){return actor&&actor->living;}}
|
||||
int headers=0,samples=0,cancels=0;string reason;vector lastDirection,lastCamera,lastBody,lastFacing,lastTurn;
|
||||
void PrintFormat(string,string,int,int){headers++;}
|
||||
void PrintFormat(string,string,int,int,string why){cancels++;reason=why;}
|
||||
void PrintFormat(string,string,int,int,vector direction,vector camera,vector actor,vector facing,vector turn){samples++;lastDirection=direction;lastCamera=camera;lastBody=actor;lastFacing=facing;lastTurn=turn;}
|
||||
class ERR_OrdinaryLookObserver {public:
|
||||
static ERR_OrdinaryLookObserver* s_Request;static bool s_Attempted;
|
||||
ERR_GameMode* m_Mode=nullptr;BaseWorld* m_World=nullptr;PlayerController* m_Controller=nullptr;IEntity* m_Actor=nullptr;PlayerCamera* m_Camera=nullptr;ERR_PlayerLife* m_Life=nullptr;
|
||||
UUID m_Account;string m_Nonce;int m_Start=0,m_Previous=0,m_Index=0;bool m_Stopped=false;
|
||||
'''+methods+r'''
|
||||
};
|
||||
ERR_OrdinaryLookObserver* ERR_OrdinaryLookObserver::s_Request=nullptr;bool ERR_OrdinaryLookObserver::s_Attempted=false;
|
||||
struct Fixture {BaseWorld world,foreignWorld;ERR_GameMode mode;IEntity actor,otherActor;PlayerCamera camera,otherCamera;PlayerController controller,otherController;PlayerManager players;CameraManager cameras;Roster roster;ERR_PlayerLife life,otherLife;InputManager input;MenuManager menus;
|
||||
Fixture(){ERR_OrdinaryLookObserver::s_Request=nullptr;ERR_OrdinaryLookObserver::s_Attempted=false;headers=samples=cancels=0;reason="";System::console=false;System::cli={{"errOrdinaryScenario","core"},{"errOrdinaryInputObserve","Run42"},{"errOrdinaryLookObserve","Run42"}};Replication::server=true;RplSession::mode=RplMode::None;ERR_GameMode::current=&mode;activeGame=&game;mode.world=&world;mode.roster=&roster;actor.world=&world;camera.world=&world;controller.actor=&actor;controller.camera=&camera;players.controller=&controller;players.actor=&actor;cameras.current=&camera;roster.actor=&actor;roster.life=&life;game={&world,&controller,&players,&cameras,&input,&menus};camera.matrix[2]=Vector(2,0,0);camera.matrix[3]=Vector(100,3.6,100);input.right=.25;input.up=-.1;}
|
||||
void sample(int index=1,int now=1100){ERR_OrdinaryLookObserver::Sample(&mode,&world,&controller,&actor,"Run42",1000,index,now);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.sample();assert(headers==1&&samples==1&&!cancels);assert(lastDirection[0]==1&&lastDirection[2]==0);assert(lastFacing[2]==1);assert(lastCamera[1]!=lastBody[1]);assert(lastTurn[0]==.25f&&lastTurn[1]==-.1f);f.sample(2,1200);assert(headers==1&&samples==2);}
|
||||
for(int fault=0;fault<7;fault++){Fixture f;if(fault==0)System::cli.erase("errOrdinaryLookObserve");if(fault==1)System::cli["errOrdinaryLookObserve"]="foreign";if(fault==2)System::cli["errOrdinaryInputObserve"]="foreign";if(fault==3)System::console=true;if(fault==4)Replication::server=false;if(fault==5)RplSession::mode=RplMode::Listen;if(fault==6)System::cli["errOrdinaryScenario"]="other";f.sample();assert(!samples&&!headers&&!cancels&&!ERR_OrdinaryLookObserver::s_Attempted);}
|
||||
{Fixture f;System::cli["errOrdinaryScenario"]="chat";f.sample();assert(samples==1);System::cli.erase("errOrdinaryLookObserve");f.sample(2,1200);assert(cancels==1);System::cli["errOrdinaryLookObserve"]="Run42";f.sample(3,1300);assert(samples==1&&cancels==1);}
|
||||
for(int fault=0;fault<16;fault++){Fixture f;f.sample();if(fault==0)f.cameras.current=&f.otherCamera;if(fault==1)f.controller.camera=&f.otherCamera;if(fault==2)game.controller=&f.otherController;if(fault==3)f.controller.actor=&f.otherActor;if(fault==4)f.players.actor=&f.otherActor;if(fault==5)f.players.controller=&f.otherController;if(fault==6)f.life.AccountId="replacement";if(fault==7)f.roster.life=&f.otherLife;if(fault==8)f.actor.world=&f.foreignWorld;if(fault==9)f.camera.world=&f.foreignWorld;if(fault==10)f.mode.active=false;if(fault==11)f.mode.phase=ERR_Phase::EXTRACTION;if(fault==12)f.actor.living=false;if(fault==13)game.cameras=nullptr;if(fault==14)activeGame=nullptr;if(fault==15)f.menus.open=true;f.sample(2,1200);assert(samples==1&&cancels==1);if(fault==15)assert(reason=="menu-opened");f.sample(3,1300);assert(cancels==1);}
|
||||
for(int fault=0;fault<5;fault++){Fixture f;f.sample();int index=2,now=1200;if(fault==0)index=1;if(fault==1)index=3;if(fault==2)index=451;if(fault==3)now=1099;if(fault==4)now=46000;f.sample(index,now);assert(samples==1&&cancels==1&&reason=="sample-or-clock-bound");}
|
||||
{Fixture f;f.sample();for(int i=2;i<=450;i++)f.sample(i,1000+i*99);assert(samples==450&&!cancels);f.sample(451,45649);assert(cancels==1);}
|
||||
{Fixture f;f.camera.replaceDuringRead=&f.cameras;f.sample();assert(!headers&&!samples&&cancels==1);}
|
||||
for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)f.camera.matrix[2]=Vector(0,0,0);if(fault==1)f.camera.matrix[3][0]=std::numeric_limits<float>::infinity();if(fault==2)f.input.up=std::numeric_limits<float>::quiet_NaN();if(fault==3)f.actor.matrix[2]=Vector(3,0,0);f.sample();assert(!samples&&cancels==1&&reason=="invalid-native-pose");}
|
||||
{Fixture f;f.sample();ERR_OrdinaryLookObserver::Sample(&f.mode,&f.foreignWorld,&f.controller,&f.actor,"Run42",1000,2,1200);assert(cancels==1&&reason=="original-binding-changed");}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual read-only look observer methods enforce explicit scope, pinned native camera/body/identity, normalized pose, terminal cancellation and original sample/time bounds')
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Optional mouse-proof launch and evidence boundaries; never launch a display."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace, ModuleType
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import headless_game as r
|
||||
import test_headless_input_exercise_runner as fixtures
|
||||
from test_headless_input_exercise_runner import NONCE
|
||||
|
||||
class LookRunner(unittest.TestCase):
|
||||
def test_optin_requires_all_existing_input_guards_before_side_effects(self):
|
||||
valid=dict(exercise_relative_look=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
||||
r.validate_input_exercise(SimpleNamespace(**valid))
|
||||
for field,value in [('exercise_player_input',False),('observe_player_input',False),('inspect_headless_display',False),('project',r.CONTROL),('settle',49),('packed_core',Path('/packed'))]:
|
||||
args=SimpleNamespace(**{**valid,field:value})
|
||||
with self.subTest(field=field),patch.object(r,'assert_no_game',side_effect=AssertionError('must refuse before process access')):
|
||||
with self.assertRaises(ValueError):r.run(args)
|
||||
r.validate_input_exercise(SimpleNamespace())
|
||||
|
||||
def test_native_flag_is_explicit_bound_and_default_has_no_look(self):
|
||||
base=['gamescope','--backend','headless','--','/proton','run','/game']
|
||||
ordinary=r.input_observer_command(base,'chat',None,NONCE)
|
||||
self.assertNotIn('-errOrdinaryLookObserve',ordinary)
|
||||
look=r.input_observer_command(base,'chat',None,NONCE,True)
|
||||
self.assertEqual(look[-2:],['-errOrdinaryLookObserve',NONCE]);self.assertEqual(look[:-2],ordinary)
|
||||
for command,flag in [(base,1),(look,True),(base+['-errOrdinaryLookObserve','foreign'],False)]:
|
||||
with self.assertRaises(ValueError):r.input_observer_command(command,'chat',None,NONCE,flag)
|
||||
|
||||
def test_helper_optin_requires_exact_matching_native_nonce_and_keeps_private_boundary(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
||||
for name in ('headless_input_proof.py','headless_display_probe.py'):(root/'tools'/name).write_text('# fake')
|
||||
base=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE]
|
||||
for native,flag,valid in [(base,False,True),(base,True,False),(base+['-errOrdinaryLookObserve',NONCE],True,True),(base+['-errOrdinaryLookObserve','foreign'],True,False),(base+['-errOrdinaryLookObserve',NONCE],False,False),(base+['-errOrdinaryLookObserve'],True,False),(base+['-errOrdinaryLookObserve',NONCE,'-errOrdinaryLookObserve',NONCE],True,False)]:
|
||||
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
||||
if not valid:
|
||||
with self.assertRaises(ValueError):r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
|
||||
continue
|
||||
command,context=r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
|
||||
self.assertEqual(command[:4],base[:4]);self.assertEqual(command[-len(native[4:]):],native[4:])
|
||||
self.assertEqual('--enable-private-look-proof' in command,flag);self.assertEqual(context.get('look_requested',False),flag)
|
||||
|
||||
def test_final_evidence_is_independent_and_earlier_failures_survive(self):
|
||||
for verified,prior in [(True,'game-started'),(False,'game-started'),(True,'game-failed'),(False,'cleanup-failed')]:
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
||||
context['look_requested']=True;report['look_requested']=True;report['look_proof']={'requested':True,'completed':True}
|
||||
raw='native sealed console bytes';(session/'console.log').write_text(raw);Path(context['report']).write_text(json.dumps(report))
|
||||
module=ModuleType('ordinary_look_evidence');calls=[]
|
||||
def validate(*args):calls.append(args);return {'verified':verified,'errors':[] if verified else ['native turn missing']}
|
||||
module.validate_look_evidence=validate
|
||||
with patch.dict(sys.modules,{'ordinary_look_evidence':module}):
|
||||
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
||||
self.assertEqual(calls,[(raw,NONCE,report['look_proof'],report['events'])])
|
||||
self.assertEqual(proof['verified'],verified)
|
||||
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
|
||||
self.assertEqual((session/'input-proof.json').read_bytes(),Path(context['report']).read_bytes())
|
||||
|
||||
def test_unrequested_or_mismatched_mouse_claim_cannot_qualify(self):
|
||||
for request,reported,payload in [(False,True,{}),(True,False,None),(False,False,{}),(False,1,None)]:
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
||||
context['look_requested']=request;report['look_requested']=reported
|
||||
if payload is not None:report['look_proof']=payload
|
||||
Path(context['report']).write_text(json.dumps(report))
|
||||
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
||||
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user