Files
EscapistsReintroduced/tests/test_ordinary_look_evidence.py
T

152 lines
10 KiB
Python

#!/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()