143 lines
7.6 KiB
Python
143 lines
7.6 KiB
Python
"""Synthetic dispatcher tests; never execute cleanup pixel processing."""
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
spec = importlib.util.spec_from_file_location("batch", Path(__file__).with_name("hd_boss_cleanup_reproduce.py"))
|
|
batch = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(batch)
|
|
|
|
|
|
class BatchTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.root = Path(self.tmp.name)
|
|
self.project = self.root / "project"
|
|
(self.project / "tools").mkdir(parents=True)
|
|
self.base = self.root / "job"
|
|
self.base.mkdir()
|
|
for n in ["hd_boss_cleanup.py", "hd_frame_policy.py"]:
|
|
(self.project / "tools" / n).write_text("# synthetic bound implementation\n")
|
|
for n in ["source", "raw", "palette"]:
|
|
(self.base / n).write_bytes(n.encode())
|
|
def ref(p, base): return {"path": str(p.relative_to(base)), "sha256": batch.sha(p)}
|
|
self.doc = {"format_version": 1, "kind": "boss_cleanup_reproduce_batch", "method": batch.METHOD,
|
|
"cleanup_tool": ref(self.project / "tools/hd_boss_cleanup.py", self.project),
|
|
"decoder": ref(self.project / "tools/hd_frame_policy.py", self.project), "frames": []}
|
|
for asset in ["abc", "abd"]:
|
|
sub = {"asset": asset, "source": ref(self.base / "source", self.base),
|
|
"parent_raw": ref(self.base / "raw", self.base), "palette": ref(self.base / "palette", self.base)}
|
|
p = self.base / f"{asset}.json"
|
|
p.write_text(json.dumps(sub))
|
|
self.doc["frames"].append({"asset": asset, "recipe": ref(p, self.base),
|
|
"expected_output_sha256": __import__('hashlib').sha256(asset.encode()).hexdigest()})
|
|
self.recipe = self.base / "batch.json"
|
|
self.save()
|
|
|
|
def save(self): self.recipe.write_text(json.dumps(self.doc))
|
|
|
|
def test_preflight_rejects_bad_tool_before_any_output(self):
|
|
self.doc["cleanup_tool"]["sha256"] = "0" * 64
|
|
self.save()
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
self.assertFalse((self.base / "out").exists())
|
|
|
|
def test_duplicate_asset_rejected(self):
|
|
self.doc["frames"][1] = self.doc["frames"][0].copy()
|
|
self.save()
|
|
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
|
|
|
def test_escaped_recipe_rejected(self):
|
|
self.doc["frames"][0]["recipe"]["path"] = "../outside.json"
|
|
self.save()
|
|
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
|
|
|
def test_wrong_subrecipe_asset_rejected(self):
|
|
self.doc["frames"][0]["asset"] = "abe"
|
|
self.save()
|
|
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
|
|
|
def test_changed_source_rejected_before_output(self):
|
|
(self.base / "source").write_bytes(b"changed")
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
self.assertFalse((self.base / "out").exists())
|
|
|
|
def fake_cleanup(self, subpath, output):
|
|
sub = json.loads(subpath.read_text()); asset = sub["asset"]
|
|
output.mkdir()
|
|
(output / f"{asset}-derived.png").write_bytes(asset.encode())
|
|
result = {"execution": {"status": "success", "method": "derived-generated-boss-source-geometry-rgb-preserved-v1", "recipe_sha256": batch.sha(subpath),
|
|
"tool_sha256": self.doc["cleanup_tool"]["sha256"], "source_sha256": {asset: sub["source"]["sha256"]},
|
|
"parent_raw_sha256": [sub["parent_raw"]["sha256"]],
|
|
"output_sha256": {asset: batch.sha(output / f"{asset}-derived.png")}}}
|
|
(output / "derived-provenance.json").write_text(json.dumps(result))
|
|
return result
|
|
|
|
def fake_import(self, name):
|
|
return SimpleNamespace(__file__=str(self.project / "tools" / f"{name}.py"), run_recipe=self.fake_cleanup)
|
|
|
|
def test_actual_aggregate_uses_unique_parent_and_per_asset_outputs(self):
|
|
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import):
|
|
result = batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
e = result["execution"]
|
|
self.assertEqual(result["actual_cleanup_subruns"], 2)
|
|
self.assertEqual(e["recipe_sha256"], batch.sha(self.recipe))
|
|
self.assertEqual(e["tool_sha256"], batch.sha(batch.__file__))
|
|
self.assertEqual(e["parent_raw_sha256"], [batch.sha(self.base / "raw")])
|
|
self.assertEqual(set(e["source_sha256"]), {"abc", "abd"})
|
|
self.assertEqual(e["output_sha256"], {r["asset"]: r["expected_output_sha256"] for r in self.doc["frames"]})
|
|
|
|
def test_wrong_accepted_png_does_not_write_success_receipt(self):
|
|
self.doc["frames"][0]["expected_output_sha256"] = "0" * 64
|
|
self.save()
|
|
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import):
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
self.assertFalse((self.base / "out/batch-execution.json").exists())
|
|
|
|
def test_existing_output_is_never_overwritten(self):
|
|
output = self.base / "out"; output.mkdir(); sentinel = output / "old"; sentinel.write_text("old")
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, output, self.project)
|
|
self.assertEqual(sentinel.read_text(), "old")
|
|
|
|
def test_all_subrun_execution_fields_must_match_pinned_inputs(self):
|
|
original = self.fake_cleanup
|
|
fields = ["status", "method", "recipe_sha256", "tool_sha256", "source_sha256", "parent_raw_sha256", "output_sha256"]
|
|
for number, field in enumerate(fields):
|
|
with self.subTest(field=field):
|
|
def contradictory(path, output):
|
|
report = original(path, output)
|
|
report['execution'][field] = "contradiction"
|
|
return report
|
|
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=contradictory):
|
|
output = self.base / f"bad-{number}"
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, output, self.project)
|
|
self.assertFalse((output / 'batch-execution.json').exists())
|
|
|
|
def test_batch_recipe_edit_during_subrun_prevents_success(self):
|
|
original = self.fake_cleanup
|
|
def changed(path, output):
|
|
report = original(path, output)
|
|
self.recipe.write_text(self.recipe.read_text() + ' ')
|
|
return report
|
|
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=changed):
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
self.assertFalse((self.base / 'out/batch-execution.json').exists())
|
|
|
|
def test_subrecipe_edit_during_subrun_prevents_success(self):
|
|
original = self.fake_cleanup
|
|
def changed(path, output):
|
|
report = original(path, output)
|
|
path.write_text(path.read_text() + ' ')
|
|
return report
|
|
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=changed):
|
|
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
|
self.assertFalse((self.base / 'out/batch-execution.json').exists())
|
|
|
|
|
|
if __name__ == "__main__": unittest.main()
|