Files
RustRaptor/tools/test_hd_boss_cleanup.py
T

122 lines
6.4 KiB
Python

"""Small synthetic geometry/lineage checks; no game or generated artwork."""
import copy
import hashlib
import json
from pathlib import Path
import struct
import tempfile
import unittest
import numpy as np
from PIL import Image
import hd_boss_cleanup as cleanup
class BossCleanup(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.cells = [[256, 10, 11, 256], [12, 13, 10, 12],
[256, 10, 256, 256], [10, 256, 256, 11]]
source = bytearray(struct.pack("<5i", 0, 0, 0, 4, 4))
for y, row in enumerate(self.cells):
for x, value in enumerate(row):
if value < 256:
source += struct.pack("<4i", x, y, y*4+x, 1) + bytes([value])
source += struct.pack("<4i", 0, 0, -1, 0)
(self.root / "Source").write_bytes(source)
palette = bytearray(768)
for i in range(10, 14):
palette[i*3:i*3+3] = bytes([25, 25, 25])
(self.root / "palette").write_bytes(palette)
self.raw = np.zeros((16, 16, 4), dtype=np.uint8)
for y in range(16):
for x in range(16):
self.raw[y, x] = [170 + ((x+y) % 3)*20, 30 + (x % 2)*30, 45 + (y % 3)*20, 255]
self.save_raw()
bind = lambda name: dict(path=name, sha256=cleanup.sha((self.root/name).read_bytes()))
self.recipe = dict(format_version=1, asset="200a2", label="synthetic only",
source=bind("Source"), palette=bind("palette"), parent_raw=bind("raw.png"),
logical_canvas=[4, 4], origin=[0, 0], output_scale=4,
alignment=dict(raw_painted_bounds=[0, 0, 16, 16], source_painted_bounds=[0, 0, 4, 4]),
patches=[], gap_fill=dict(generated_donor=[0, 0, 5, 5], detail_gain=0.6))
def save_raw(self):
Image.fromarray(self.raw).save(self.root / "raw.png")
def test_exact_source_outline_feet_and_generated_body_rgb(self):
pixels, report = cleanup.render(self.recipe, self.root)
expected = np.repeat(np.repeat(np.array(self.cells) < 256, 4, axis=0), 4, axis=1)
self.assertTrue(np.array_equal(pixels[:, :, 3] == 255, expected))
self.assertTrue(np.array_equal(pixels[expected, :3], self.raw[expected, :3]))
self.assertTrue(np.all(pixels[~expected] == 0))
self.assertEqual(np.nonzero(pixels[-1, :, 3])[0].tolist(), list(range(4)) + list(range(12, 16)))
self.assertEqual(report["direct_generated_fraction_of_painted"], 1)
self.assertGreater(report["internally_nonconstant_pixels"], 0)
self.assertFalse(np.all(pixels[expected, 0] == 101), "must preserve generated body, not Source NN")
def test_source_gap_is_opaque_and_explicit_generated_texture_patch_is_local(self):
self.raw[4:8, 4:8, 3] = 0
self.save_raw()
self.recipe["parent_raw"]["sha256"] = cleanup.sha((self.root/"raw.png").read_bytes())
self.recipe["patches"] = [dict(name="steel support", target=[1, 0, 2, 1],
generated_donor=[0, 0, 5, 5], detail_gain=0.5)]
pixels, report = cleanup.render(self.recipe, self.root)
self.assertEqual(report["gap_filled_pixels"], 16)
self.assertIn([1, 1], report["gap_filled_cells"])
self.assertTrue(np.all(pixels[4:8, 4:8, 3] == 255))
self.assertLess(float(abs(pixels[4:8, 4:8, :3].mean() - 101)), 5)
self.assertEqual(report["patches"][0]["cells"], [[1, 0]])
self.assertTrue(np.array_equal(pixels[4:8, 8:12, :3], self.raw[4:8, 8:12, :3]))
self.assertTrue(np.all(pixels[:4, :4] == 0), "Source-empty neighbor stays empty")
def test_input_identity_geometry_and_existing_output_fail_closed(self):
for mutation in [lambda r: r["source"].update(sha256="0"*64),
lambda r: r.update(format_version=True),
lambda r: r.update(logical_canvas=[5, 4]),
lambda r: r.update(origin=[False, 0]),
lambda r: r.update(origin=[1, 0]),
lambda r: r["alignment"].update(source_painted_bounds=[0, 1, 4, 4]),
lambda r: r["gap_fill"].update(generated_donor=[0, 0, 17, 4])]:
with self.subTest(mutation=mutation):
bad = copy.deepcopy(self.recipe)
mutation(bad)
with self.assertRaises(ValueError):
cleanup.render(bad, self.root)
recipe = self.root/"recipe.json"
recipe.write_text(json.dumps(self.recipe))
with self.assertRaises(ValueError):
cleanup.run_recipe(recipe, self.root)
def test_missing_generated_body_is_rejected_instead_of_source_fill_relabel(self):
self.raw[:, :, 3] = 0
self.raw[0:4, 4:8, 3] = 255
self.save_raw()
self.recipe["parent_raw"]["sha256"] = cleanup.sha((self.root/"raw.png").read_bytes())
with self.assertRaisesRegex(ValueError, "most generated body is missing"):
cleanup.render(self.recipe, self.root)
def test_repeatable_derived_output_and_complete_execution_binding(self):
recipe = self.root/"recipe.json"
recipe.write_text(json.dumps(self.recipe, sort_keys=True))
before = {n: (self.root/n).read_bytes() for n in ["Source", "palette", "raw.png"]}
reports = [cleanup.run_recipe(recipe, self.root/f"derived-{i}") for i in range(2)]
images = [(self.root/f"derived-{i}/200a2-derived.png").read_bytes() for i in range(2)]
self.assertEqual(images[0], images[1])
self.assertEqual(reports[0], reports[1])
execution = reports[0]["execution"]
self.assertEqual(execution["status"], "success")
self.assertEqual(execution["method"], cleanup.METHOD)
self.assertEqual(execution["recipe_sha256"], hashlib.sha256(recipe.read_bytes()).hexdigest())
self.assertEqual(execution["tool_sha256"], hashlib.sha256(Path(cleanup.__file__).read_bytes()).hexdigest())
self.assertEqual(execution["source_sha256"], {"200a2": self.recipe["source"]["sha256"]})
self.assertEqual(execution["parent_raw_sha256"], [self.recipe["parent_raw"]["sha256"]])
self.assertEqual(execution["output_sha256"], {"200a2": hashlib.sha256(images[0]).hexdigest()})
self.assertEqual(before, {n: (self.root/n).read_bytes() for n in before})
if __name__ == "__main__":
unittest.main()