"""Synthetic inputs only: no game masters or real cleanup execution.""" import copy,io,json,os,struct,tempfile,unittest,subprocess,sys from unittest import mock from types import SimpleNamespace from pathlib import Path import numpy as np from PIL import Image import hd_grating_gray_cleanup as m class GrayCastTests(unittest.TestCase): def setUp(self): self.temp=tempfile.TemporaryDirectory();self.root=Path(self.temp.name) pal=np.zeros((256,3),np.uint8);pal[11]=[18,18,18];pal[30]=[4,4,4] (self.root/'palette.bin').write_bytes(pal.tobytes()) y,x=np.indices((128,256));rgb=np.stack([50+x%5+y%7,35+x%5+y%7,28+x%5+y%7],axis=2).astype('uint8') im=Image.fromarray(rgb);im.save(self.root/'parent.png') self.source=np.full((32,32),199,np.uint8);self.source[2:31,2:31]=11 self.recipe=dict(format_version=1,method=m.METHOD,palette=self.bind('palette.bin'),parent_raw=self.bind('parent.png'),frames=[]) for asset in ['203c6','203c7']: name=asset+'.bin';(self.root/name).write_bytes(struct.pack('<5i',1,1,0,32,32)+self.source.tobytes()) self.recipe['frames'].append(dict(asset=asset,Source=self.bind(name),selected_cells_sha256=m.selected_digest(m.semantic_mask(asset,self.source)))) def tearDown(self):self.temp.cleanup() def bind(self,n):return dict(path=n,sha256=m.sha((self.root/n).read_bytes())) def prepared(self):return m.prepare_inputs(self.recipe,self.root) def test_cast_neutral_preserves_generated_luminance(self): result=m.derive(*self.prepared());self.assertEqual(len(result),2) for a,out,metrics in result: self.assertEqual(out.shape,(128,128,4));self.assertTrue(np.all(out[:,:,3]==255)) self.assertTrue(metrics['outside_resized_parent_exact']);self.assertEqual(metrics['exact_Original_pixel_copies'],0) self.assertLessEqual(metrics['max_luma_structure_rounding_error'],.5000001) self.assertGreater(metrics['generated_luma_variance'],1);self.assertGreater(metrics['output_luma_variance'],1) self.assertLess(abs(metrics['output_luma_variance']-metrics['generated_luma_variance']),2) def test_repeatability(self): a=m.derive(*self.prepared());b=m.derive(*self.prepared()) self.assertTrue(all(np.array_equal(x[1],y[1])and x[2]==y[2]for x,y in zip(a,b))) def test_all_semantic_cells_not_error_picker(self): for asset in m.REGIONS: mask=m.semantic_mask(asset,self.source);x0,y0,x1,y1=m.REGIONS[asset] self.assertEqual(int(mask.sum()),int(np.count_nonzero(self.source[y0:y1,x0:x1]==11))) def test_missing_or_unknown_fields(self): for key in ['method','palette','frames']: r=copy.deepcopy(self.recipe);del r[key] with self.assertRaises(ValueError):m.validate_recipe(r) r=copy.deepcopy(self.recipe);r['gain']=1.2 with self.assertRaises(ValueError):m.validate_recipe(r) def test_bool_version_rejected(self): r=copy.deepcopy(self.recipe);r['format_version']=True with self.assertRaises(ValueError):m.validate_recipe(r) def test_order_and_selector_are_fixed(self): r=copy.deepcopy(self.recipe);r['frames'].reverse() with self.assertRaises(ValueError):m.validate_recipe(r) p,raw,frames=self.prepared();frames[0][0]['selected_cells_sha256']='0'*64 with self.assertRaises(ValueError):m.derive(p,raw,frames) def test_wrong_hash_and_escaping_paths(self): for ref in [dict(path='parent.png',sha256='0'*64),dict(path='../parent.png',sha256='0'*64),dict(path='/parent.png',sha256='0'*64),dict(path='a/../parent.png',sha256='0'*64)]: with self.assertRaises((ValueError,OSError)):m.read_bound(self.root,ref) def test_symlink_file_and_parent_rejected(self): (self.root/'alias.png').symlink_to(self.root/'parent.png') with self.assertRaises(OSError):m.read_bound(self.root,dict(path='alias.png',sha256=self.bind('parent.png')['sha256'])) (self.root/'folder').symlink_to(self.root,target_is_directory=True) with self.assertRaises(OSError):m.read_bound(self.root,dict(path='folder/parent.png',sha256=self.bind('parent.png')['sha256'])) def test_bytes_are_not_reopened(self): prepared=self.prepared();first=m.derive(*prepared) (self.root/'parent.png').write_bytes(b'corrupted after authenticated read') for a,b in zip(first,m.derive(*prepared)):self.assertTrue(np.array_equal(a[1],b[1])) def test_flat_parent_rejected(self): p,_,f=self.prepared();stream=io.BytesIO();Image.new('RGB',(256,128),(50,35,28)).save(stream,format='PNG') with self.assertRaisesRegex(ValueError,'witness'):m.derive(p,stream.getvalue(),f) def test_transparency_size_and_shift_caps(self): p,_,f=self.prepared() for image in [Image.new('RGBA',(256,128),(50,40,30,0)),Image.new('RGB',(128,128)),Image.new('RGB',(256,64))]: stream=io.BytesIO();image.save(stream,format='PNG') with self.assertRaises(ValueError):m.derive(p,stream.getvalue(),f) p[11]=[240,240,240] with self.assertRaisesRegex(ValueError,'fixed32'):m.derive(p,self.prepared()[1],f) def test_source_and_palette_validation(self): for b in [b'',struct.pack('<5i',2,1,0,32,32)+self.source.tobytes(),struct.pack('<5i',1,1,0,32,32)+bytes([254])*1024]: with self.assertRaises(ValueError):m.source_indices(b) for b in [bytes(767),bytes([64])*768]: with self.assertRaises(ValueError):m.palette_rgb(b) def test_fresh_publication_receipt_and_no_overwrite(self): (self.root/'recipe.json').write_text(json.dumps(self.recipe));out=self.root/'new-output' result=m.run('recipe.json',self.root,out) self.assertEqual(set(result['execution']),{'status','method','recipe_sha256','tool_sha256','source_sha256','parent_raw_sha256','output_sha256'}) self.assertEqual(set(result['execution']['output_sha256']),{'203c6','203c7'}) before=sorted(p.name for p in out.iterdir()) with self.assertRaises(ValueError):m.run('recipe.json',self.root,out) self.assertEqual(before,sorted(p.name for p in out.iterdir())) def test_existing_empty_directory_no_replace(self): stage=self.root/'stage';target=self.root/'target';stage.mkdir();target.mkdir() with self.assertRaises(OSError):m.publish_no_replace(stage,target) self.assertTrue(stage.is_dir());self.assertTrue(target.is_dir()) def test_outside_and_alpha_exact_against_independent_baseline(self): palette,raw,frames=self.prepared();result=m.derive(palette,raw,frames) with Image.open(io.BytesIO(raw))as im:parent=np.asarray(im.convert('RGBA')) for col,((asset,out,_),(_,src))in enumerate(zip(result,frames)): baseline=np.asarray(Image.fromarray(parent[:,col*128:(col+1)*128]).resize((128,128),Image.Resampling.LANCZOS)) area=np.repeat(np.repeat(m.semantic_mask(asset,src),4,0),4,1) np.testing.assert_array_equal(out[~area],baseline[~area]) np.testing.assert_array_equal(out[:,:,3],baseline[:,:,3]) ys,xs=np.nonzero(area);a=(ys[0],xs[0]);different=np.abs((baseline[:,:,:3]@m.LUMA)-(baseline[a][:3]@m.LUMA))>2 by,bx=np.nonzero(area&different);b=(by[0],bx[0]) original_difference=float((baseline[a][:3].astype(float)-baseline[b][:3])@m.LUMA) output_difference=float((out[a][:3].astype(float)-out[b][:3])@m.LUMA) self.assertLessEqual(abs(original_difference-output_difference),1.0000002) def test_gray_index_boundaries_and_exclusive_region_edges(self): for asset,(x0,y0,x1,y1)in m.REGIONS.items(): source=np.full((32,32),11,np.uint8);source[y0,x0:x0+4]=[10,11,30,31] mask=m.semantic_mask(asset,source) self.assertEqual(mask[y0,x0:x0+4].tolist(),[False,True,True,False]) self.assertTrue(mask[y1-1,x1-1]);self.assertFalse(mask[y1,x1-1]);self.assertFalse(mask[y0,x1]) self.assertFalse(mask[y0,x0-1]) if y0:self.assertFalse(mask[y0-1,x0]) def test_clipping_is_rejected_without_luminance_compression(self): palette,raw,frames=self.prepared() with Image.open(io.BytesIO(raw))as im:a=np.asarray(im.convert('RGB')).copy() a[30,90]=255;stream=io.BytesIO();Image.fromarray(a).save(stream,format='PNG') with self.assertRaisesRegex(ValueError,'clipping'):m.derive(palette,stream.getvalue(),frames) def test_nonneutral_source_anchor_rejected(self): palette,raw,frames=self.prepared();palette[11]=[75,50,30] with self.assertRaisesRegex(ValueError,'near-neutral'):m.derive(palette,raw,frames) def test_non_rgb_parent_and_malformed_frames_rejected(self): p,raw,f=self.prepared() for mode in ['L','P']: stream=io.BytesIO();Image.new(mode,(256,128)).save(stream,format='PNG') with self.assertRaises(ValueError):m.derive(p,stream.getvalue(),f) for bad in [None,7,'203c6',[]]: r=copy.deepcopy(self.recipe);r['frames'][0]=bad with self.assertRaises(ValueError):m.validate_recipe(r) for bad in [None,7,[]]: with self.assertRaises(ValueError):m.read_regular(self.root,bad) def test_fifo_is_nonblocking_and_regular_rejected(self): os.mkfifo(self.root/'pipe') code="import hd_grating_gray_cleanup as m; m.read_regular(__import__('sys').argv[1], 'pipe')" env=dict(os.environ,PYTHONPATH=str(Path(m.__file__).parent)) child=subprocess.run([sys.executable,'-c',code,str(self.root)],capture_output=True,timeout=3,env=env) self.assertNotEqual(child.returncode,0);self.assertIn(b'bounded regular input required',child.stderr) def test_oversize_short_and_changed_reads_rejected(self): with self.assertRaisesRegex(ValueError,'bounded regular'):m.read_regular(self.root,'parent.png',limit=2) with mock.patch.object(m.os,'fdopen',return_value=io.BytesIO(b'x')): with self.assertRaisesRegex(ValueError,'changed'):m.read_regular(self.root,'parent.png') real=m.os.fstat;calls=[0] def changed(fd): st=real(fd);calls[0]+=1 values={k:getattr(st,k)for k in ['st_mode','st_dev','st_ino','st_size','st_mtime_ns','st_ctime_ns']} if calls[0]%2==0:values['st_mtime_ns']+=1 return SimpleNamespace(**values) with mock.patch.object(m.os,'fstat',side_effect=changed): with self.assertRaisesRegex(ValueError,'changed'):m.read_regular(self.root,'parent.png') if __name__=='__main__':unittest.main()