Files
RustRaptor/tools/ship15_component_registration/test_ship15_component_cleanup.py
T

262 lines
13 KiB
Python

"""Synthetic only: no actual SHIP15 image, Source or recipe is rendered."""
from pathlib import Path
import copy
import importlib.util
import json
import struct
import tempfile
import unittest
from unittest.mock import patch
import numpy as np
from PIL import Image
HERE=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('private_ship15', HERE/'ship15_component_cleanup.py')
tool=importlib.util.module_from_spec(spec)
spec.loader.exec_module(tool)
DECODER=HERE.parent.parent/'tools/hd_frame_policy.py'
TONE=[(38,7,7,6),(42,7,3,1),(43,7,4,3),(46,13,31,30),(47,13,30,28),(48,13,29,27),
(49,13,28,26),(50,13,29,27),(51,13,27,24),(42,14,27,25),(43,14,29,27),(44,14,27,25)]
def binding(p):
return dict(path=str(p),sha256=tool.sha(p.read_bytes()))
def encode_source(arr):
out=bytearray(struct.pack('<5i',0,2,18,72,24))
for y in range(24):
x=0
while x<72:
if arr[y,x]==256:
x+=1
continue
end=x+1
while end<72 and arr[y,end]<256:end+=1
out.extend(struct.pack('<4i',x,y,y*72+x,end-x))
out.extend(bytes(arr[y,x:end].tolist()))
x=end
out.extend(struct.pack('<4i',0,0,-1,0))
return bytes(out)
class SyntheticFamily(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory()
self.root=Path(self.temp.name)
self.palette=np.zeros((256,3),np.uint8)
for i in range(32):self.palette[i]=63-2*i
self.palette[144:158]=[5,25,30]
self.palette[255]=[54,54,54]
pp=self.root/'PALETTE';pp.write_bytes(self.palette.tobytes())
base=np.full((24,72),256,np.uint16)
base[2:20]=144
base[7:15,36:53]=10
base[4:7,39:42]=10
base[15:18,39:42]=10
base[7:15,21:29]=10
base[7:15,55:63]=10
base[10:12,37]=255
tone_set={(x,y)for x,y,_,_ in TONE}
black=[]
for y,x in np.argwhere(base<32):
if (int(x),int(y))not in tone_set:
black.append((int(x),int(y)))
if len(black)==15:break
for x,y in black:base[y,x]=31
self.sources=[]
self.raw=np.zeros((144,144,4),np.uint8)
frames=[]
for i in range(3):
s=base.copy()
for x,y,v0,v1 in TONE:s[y,x]=v0 if i==0 else v1
self.sources.append(s)
p=self.root/f'SOURCE{i}';p.write_bytes(encode_source(s))
alpha=np.repeat(np.repeat(s<256,2,axis=0),2,axis=1)
gray=np.repeat(np.repeat(s<32,2,axis=0),2,axis=1)
yy,xx=np.indices(alpha.shape)
phase=np.zeros((48,144,4),np.uint8)
phase[:,:,:3][alpha]=np.stack([10+xx%5,90+yy%10,110+xx%10],axis=2)[alpha]
neutral=(60+xx%70+yy%11+i*7).astype(np.uint8)
for c in range(3):phase[:,:,c][gray]=neutral[gray]
phase[:,:,3][alpha]=255
self.raw[i*48:(i+1)*48]=phase
comps={}
for name,(sx,sy)in tool.AXES.items():
comps[name]=dict(source_x=sx,raw_x=[v*2 for v in sx],source_y=sy,raw_y=[v*2 for v in sy],
donor_bounds=[sx[0]*2,sy[0]*2,sx[-1]*2,sy[-1]*2])
frames.append(dict(asset=f'{0x1030b+i:05x}',frame=i,source=binding(p),logical_canvas=[72,24],origin=[0,0],
raw_crop=[0,i*48,144,48],components=comps,functional_original_cells=[]))
phases=np.any(np.stack(self.sources)!=self.sources[0],axis=0)&(self.sources[0]<32)
for f,s in zip(frames,self.sources):
f['functional_original_cells']=[[int(x),int(y)]for y,x in np.argwhere((s==31)|phases|(s==255))]
self.assertEqual(len(f['functional_original_cells']),29)
self.parent=self.root/'parent.png'
Image.fromarray(self.raw).save(self.parent)
self.recipe=dict(format_version=1,label='SYNTHETIC ONLY',decoder_module=binding(DECODER),palette=binding(pp),
parent_raw=binding(self.parent),output_scale=8,caps=copy.deepcopy(tool.CAPS),frames=frames)
def tearDown(self):
self.temp.cleanup()
def save_parent(self):
Image.fromarray(self.raw).save(self.parent)
self.recipe['parent_raw']=binding(self.parent)
def save_source(self,i):
p=self.root/f'SOURCE{i}';p.write_bytes(encode_source(self.sources[i]));self.recipe['frames'][i]['source']=binding(p)
def assert_fails_without_output(self,recipe,pattern):
rp=self.root/'bad-recipe.json';rp.write_text(json.dumps(recipe))
out=self.root/'MUST-NOT-EXIST'
with self.assertRaisesRegex((ValueError,AssertionError),pattern):tool.run_recipe(rp,out)
self.assertFalse(out.exists())
def test_identity_preserves_genuine_RGB_alpha_and29_Original_only(self):
arrays,report=tool.render_family(self.recipe,self.root)
pal=((self.palette.astype(np.uint16)<<2)|(self.palette.astype(np.uint16)>>4)).astype(np.uint8)
for i,(s,out)in enumerate(zip(self.sources,arrays)):
alpha=np.repeat(np.repeat(s<256,8,axis=0),8,axis=1)
self.assertTrue(np.array_equal(out[:,:,3]==255,alpha))
self.assertFalse(out[~alpha].any())
yy,xx=np.indices(alpha.shape);expected=self.raw[i*48+(yy//4),xx//4,:3]
f=np.zeros((24,72),bool)
for x,y in self.recipe['frames'][i]['functional_original_cells']:f[y,x]=True
fm=np.repeat(np.repeat(f,8,axis=0),8,axis=1)
srgb=np.repeat(np.repeat(pal[np.minimum(s,255)],8,axis=0),8,axis=1)
self.assertTrue(np.array_equal(out[:,:,:3][alpha&~fm],expected[alpha&~fm]))
self.assertTrue(np.array_equal(out[:,:,:3][fm],srgb[fm]))
self.assertEqual(sum(m['nearest_parent_extension_samples']for m in report['frames'][i]['component_donor_metrics']),0)
self.assertFalse(np.array_equal(arrays[0],arrays[1]))
def test_nonuniform_left_pod_transform_keeps_own_genuine_donor(self):
for i in range(3):
ph=self.raw[i*48:(i+1)*48]
original=ph[14:30,42:58].copy()
ph[14:30,42:60,:3]=[10,95,115]
ph[14:30,42:60,3]=255
ph[14:30,44:60]=original
m=self.recipe['frames'][i]['components']['left_pod']
m['raw_x']=[44,60];m['donor_bounds']=[44,14,60,30]
self.save_parent()
arrays,_=tool.render_family(self.recipe,self.root)
# An ordinary non-functional bottom-right pod point reads translated raw.
x,y=28*8+2,14*8+2
self.assertNotIn([28,14],self.recipe['frames'][0]['functional_original_cells'])
self.assertEqual(arrays[0][y,x,:3].tolist(),self.raw[28,58,:3].tolist())
self.assertEqual(int(arrays[0][y,x,3]),255)
def test_malformed_knots_and_relaxed_caps_fail_before_output(self):
mutations=[('raw_x',[72,78,78,106]),('raw_y',[8,float('nan'),30,36]),('source_x',[36,39,42,54]),
('raw_x',[True,78,84,106]),('donor_bounds',[73,8,106,36])]
for key,value in mutations:
with self.subTest(key=key,value=value):
r=copy.deepcopy(self.recipe);r['frames'][0]['components']['main_machinery_mast_hull'][key]=value
self.assert_fails_without_output(r,'knots|bounds')
r=copy.deepcopy(self.recipe);r['caps']['metal_max_logical_distance']=2
self.assert_fails_without_output(r,'caps')
r=copy.deepcopy(self.recipe);r['caps']['metal_max_logical_distance']=True
self.assert_fails_without_output(r,'caps')
def test_missing_alpha_no_donor_and_wrong_material_fail(self):
original=self.raw.copy()
self.raw[14:30,42:58,3]=0;self.save_parent()
self.assert_fails_without_output(self.recipe,'no genuine')
self.raw=original.copy();self.raw[14:30,42:58,:3]=[0,130,150];self.save_parent()
self.assert_fails_without_output(self.recipe,'no genuine')
self.raw=original.copy();self.raw[:,:,3]=255;self.save_parent()
self.assert_fails_without_output(self.recipe,'transparent alpha')
def test_donor_fraction_and_distance_are_separate_hard_stops(self):
original=self.raw.copy()
self.raw[14:30,42:46,3]=0;self.save_parent()
self.assert_fails_without_output(self.recipe,'missing fraction')
self.raw=original.copy();self.raw[18:24,86:92,3]=0;self.save_parent()
self.assert_fails_without_output(self.recipe,'distance exceeds')
def test_Original_selectors_and_Source_support_are_fail_closed(self):
for value in [self.recipe['frames'][0]['functional_original_cells'][:-1],
self.recipe['frames'][0]['functional_original_cells']+[self.recipe['frames'][0]['functional_original_cells'][0]]]:
r=copy.deepcopy(self.recipe);r['frames'][0]['functional_original_cells']=value
self.assert_fails_without_output(r,'Original cells')
original=self.sources[0].copy()
self.sources[0][1,20]=144;self.save_source(0)
self.assert_fails_without_output(self.recipe,'support outside')
self.sources[0]=original.copy();self.sources[0][10,37]=10;self.save_source(0)
self.assert_fails_without_output(self.recipe,'indicator changed')
self.sources[0]=original.copy();self.sources[0][5,5]=10;self.save_source(0)
self.assert_fails_without_output(self.recipe,'footprint changed|support outside')
self.sources[0]=original.copy();self.sources[0][5,5]=100;self.save_source(0)
self.assert_fails_without_output(self.recipe,'class outside')
def test_source_tone12_family_order_and_full_crop_not_optional(self):
r=copy.deepcopy(self.recipe);r['frames'][0]['frame']=False
self.assert_fails_without_output(r,'order')
r=copy.deepcopy(self.recipe);r['frames'][1]['raw_crop']=[0,49,144,47]
self.assert_fails_without_output(r,'full strips')
for i in (1,2):
self.sources[i]=self.sources[0].copy();self.save_source(i)
self.assert_fails_without_output(self.recipe,'phase12')
def test_hash_check_precedes_any_output_and_preserves_parent(self):
before={p:tool.sha(p.read_bytes())for p in self.root.iterdir()if p.is_file()}
r=copy.deepcopy(self.recipe);r['parent_raw']['sha256']='0'*64
self.assert_fails_without_output(r,'hash mismatch')
for p,sha in before.items():self.assertEqual(tool.sha(p.read_bytes()),sha)
def test_nearest_ties_are_raster_seeded_and_never_cascade(self):
trust=np.zeros((5,5),bool);trust[0,0]=True;trust[4,4]=True
distance,oy,ox=tool.nearest_owners(trust)
self.assertTrue(np.all(trust[oy,ox]))
self.assertEqual((int(oy[2,2]),int(ox[2,2])),(0,0))
self.assertEqual(int(distance[2,2]),4)
for y in range(5):
for x in range(5):self.assertEqual(int(distance[y,x]),min(x+y,8-x-y))
def test_verified_decoder_bytes_execute_after_temporary_path_replacement(self):
# Only a temporary decoder copy is replaced; no shared input is changed.
decoder_copy=self.root/'copied_decoder.py'
verified_bytes=DECODER.read_bytes()
decoder_copy.write_bytes(verified_bytes)
self.recipe['decoder_module']=binding(decoder_copy)
expected,_=tool.render_family(self.recipe,self.root)
checked_original=tool.checked
replaced=[]
malicious=b"raise RuntimeError('UNVERIFIED_TEMP_DECODER_MUST_NOT_EXECUTE')\n"
def verify_then_replace(binding_value,base):
path,data=checked_original(binding_value,base)
if path==decoder_copy.resolve():
self.assertEqual(data,verified_bytes)
self.assertEqual(tool.sha(data),tool.DECODER_SHA)
path.write_bytes(malicious)
replaced.append(path)
return path,data
with patch.object(tool,'checked',side_effect=verify_then_replace):
actual,report=tool.render_family(self.recipe,self.root)
self.assertEqual(replaced,[decoder_copy.resolve()])
self.assertEqual(decoder_copy.read_bytes(),malicious)
self.assertEqual(report['decoder_module']['sha256'],tool.DECODER_SHA)
for before,after in zip(expected,actual):
self.assertTrue(np.array_equal(before,after))
self.assertFalse(any(p.name.endswith('-derived.png')for p in self.root.iterdir()))
def test_deterministic_recipe_receipts_and_new_output_only(self):
rp=self.root/'recipe.json';rp.write_text(json.dumps(self.recipe,sort_keys=True))
before={p:tool.sha(p.read_bytes())for p in self.root.iterdir()if p.is_file()}
first=tool.run_recipe(rp,self.root/'first')
second=tool.run_recipe(rp,self.root/'second')
self.assertEqual(first,second)
self.assertEqual(set(first['execution']),{'status','method','recipe_sha256','tool_sha256','source_sha256','parent_raw_sha256','output_sha256'})
self.assertEqual(first['decoder_module']['sha256'],tool.DECODER_SHA)
self.assertIn('numpy_version',first)
for name in ['1030b-derived.png','1030c-derived.png','1030d-derived.png','derived-provenance.json']:
self.assertEqual((self.root/'first'/name).read_bytes(),(self.root/'second'/name).read_bytes())
with self.assertRaisesRegex(ValueError,'output exists'):tool.run_recipe(rp,self.root/'first')
for p,sha in before.items():self.assertEqual(tool.sha(p.read_bytes()),sha)
if __name__=='__main__':unittest.main()