DazedTL/tests/test_rpgmaker_images.py
DazedAnon d82895cc4d feat(rpgmaker): add scoped image patch preparation
- Patch highlighted editable images or all images when none are highlighted
- Skip unchanged images while preserving editable workspace copies
- Open the editable folder matching the current image or folder filter
2026-07-25 16:29:19 -05:00

330 lines
13 KiB
Python

import tempfile
import unittest
from io import BytesIO
from pathlib import Path
from PIL import Image
from util.rpgmaker_images import (
add_patch_exceptions,
clean_runtime_image_duplicates,
decrypt_assets,
decrypt_image_bytes,
editable_workspace_root,
encrypt_assets,
encrypt_image_bytes,
migrate_legacy_editable_workspace,
prepare_assets_for_patch,
read_encryption_key,
remove_editable_assets,
scan_image_assets,
)
KEY_HEX = "00112233445566778899aabbccddeeff"
KEY = bytes.fromhex(KEY_HEX)
def png_bytes(color: str) -> bytes:
output = BytesIO()
Image.new("RGBA", (12, 8), color).save(output, format="PNG")
return output.getvalue()
class RPGMakerImageTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name) / "Game"
self.content = self.root / "www"
(self.content / "data").mkdir(parents=True)
(self.content / "img" / "pictures").mkdir(parents=True)
(self.content / "data" / "System.json").write_text(
'{"encryptionKey":"' + KEY_HEX + '"}', encoding="utf-8"
)
def tearDown(self):
self.temp.cleanup()
def _encrypted_asset(self, name: str = "001.rpgmvp", color: str = "red") -> Path:
path = self.content / "img" / "pictures" / name
path.write_bytes(encrypt_image_bytes(png_bytes(color), KEY))
return path
def test_scan_and_decrypt_mv_image_without_touching_encrypted_original(self):
encrypted = self._encrypted_asset()
original = encrypted.read_bytes()
assets = scan_image_assets(self.root)
self.assertEqual([asset.asset_id for asset in assets], ["img/pictures/001.png"])
self.assertFalse(assets[0].has_plain)
self.assertEqual(
assets[0].plain_path,
self.root / ".dazedtl" / "images" / "www" / "img" / "pictures" / "001.png",
)
result = decrypt_assets(
assets, read_encryption_key(self.root), game_root=self.root
)
self.assertEqual(result.completed, 1)
self.assertEqual(result.errors, [])
self.assertEqual(assets[0].plain_path.read_bytes(), png_bytes("red"))
self.assertEqual(encrypted.read_bytes(), original)
ignore = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertIn("/.dazedtl/", ignore)
def test_existing_editable_png_is_not_overwritten_by_decrypt_all(self):
self._encrypted_asset()
asset = scan_image_assets(self.root)[0]
plain = asset.plain_path
plain.parent.mkdir(parents=True)
translated = png_bytes("blue")
plain.write_bytes(translated)
result = decrypt_assets(
scan_image_assets(self.root), KEY, game_root=self.root
)
self.assertEqual(result.completed, 0)
self.assertEqual(result.skipped, 1)
self.assertEqual(plain.read_bytes(), translated)
def test_remove_editable_image_deletes_only_workspace_copy(self):
self._encrypted_asset()
asset = scan_image_assets(self.root)[0]
decrypt_assets([asset], KEY, game_root=self.root)
edited = png_bytes("blue")
asset.plain_path.write_bytes(edited)
result = remove_editable_assets(self.root, [asset])
self.assertEqual(result.completed, 1)
self.assertEqual(result.errors, [])
self.assertFalse(asset.plain_path.exists())
self.assertTrue(asset.encrypted_path.exists())
def test_prepare_reencrypts_selected_image_backs_up_original_and_updates_ignore(self):
encrypted = self._encrypted_asset()
original = encrypted.read_bytes()
asset = scan_image_assets(self.root)[0]
decrypt_assets([asset], KEY, game_root=self.root)
translated = png_bytes("blue")
asset.plain_path.write_bytes(translated)
self.root.joinpath(".gitignore").write_text("*.*\n", encoding="utf-8")
result = prepare_assets_for_patch(self.root, [asset], KEY)
self.assertEqual(result.completed, 1)
self.assertEqual(result.errors, [])
self.assertEqual(decrypt_image_bytes(encrypted.read_bytes(), KEY), translated)
self.assertEqual(asset.plain_path.read_bytes(), translated)
backup = self.root / ".dazedtl" / "image_backups" / "www/img/pictures/001.rpgmvp"
self.assertEqual(backup.read_bytes(), original)
ignore = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertIn("/.dazedtl/", ignore)
self.assertIn("!/www/img/pictures/001.rpgmvp", ignore)
self.assertNotIn("!/www/img/pictures/001.png", ignore)
second = prepare_assets_for_patch(self.root, [asset], KEY)
again = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertEqual(second.completed, 0)
self.assertEqual(second.skipped, 1)
self.assertEqual(again.count("!/www/img/pictures/001.rpgmvp"), 1)
self.assertEqual(backup.read_bytes(), original)
def test_prepare_skips_unchanged_decrypted_image(self):
encrypted = self._encrypted_asset()
original = encrypted.read_bytes()
asset = scan_image_assets(self.root)[0]
decrypt_assets([asset], KEY, game_root=self.root)
result = prepare_assets_for_patch(self.root, [asset], KEY)
self.assertEqual(result.completed, 0)
self.assertEqual(result.skipped, 1)
self.assertEqual(result.patch_files, [])
self.assertEqual(encrypted.read_bytes(), original)
self.assertFalse(
(self.root / ".dazedtl" / "image_backups" / "www/img/pictures/001.rpgmvp").exists()
)
ignore = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertNotIn("!/www/img/pictures/001.rpgmvp", ignore)
def test_prepare_skips_unchanged_unencrypted_workspace_copy(self):
runtime = self.content / "img" / "pictures" / "menu.png"
original = png_bytes("yellow")
runtime.write_bytes(original)
asset = scan_image_assets(self.root)[0]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(original)
result = prepare_assets_for_patch(self.root, [asset], None)
self.assertEqual(result.completed, 0)
self.assertEqual(result.skipped, 1)
self.assertEqual(result.patch_files, [])
self.assertEqual(runtime.read_bytes(), original)
self.assertFalse((self.root / ".dazedtl" / "image_backups").exists())
def test_mz_png_uses_same_crypto_and_logical_png_name(self):
encrypted = self._encrypted_asset("title.png_", "green")
asset = scan_image_assets(self.root)[0]
self.assertEqual(asset.asset_id, "img/pictures/title.png")
self.assertEqual(asset.encrypted_path, encrypted)
result = decrypt_assets([asset], KEY, game_root=self.root)
self.assertEqual(result.completed, 1)
self.assertEqual(asset.plain_path.read_bytes(), png_bytes("green"))
def test_encrypt_only_rebuilds_runtime_file_without_changing_gitignore(self):
encrypted = self._encrypted_asset()
asset = scan_image_assets(self.root)[0]
decrypt_assets([asset], KEY, game_root=self.root)
translated = png_bytes("black")
asset.plain_path.write_bytes(translated)
ignore_before = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
result = encrypt_assets(self.root, [asset], KEY)
self.assertEqual(result.completed, 1)
self.assertEqual(result.errors, [])
self.assertEqual(decrypt_image_bytes(encrypted.read_bytes(), KEY), translated)
self.assertEqual(
self.root.joinpath(".gitignore").read_text(encoding="utf-8"),
ignore_before,
)
def test_nested_gitignore_receives_relative_exact_exception(self):
target = self.content / "img" / "pictures" / "menu image.png"
target.write_bytes(png_bytes("purple"))
nested = self.content / "img" / ".gitignore"
nested.write_text("*\n", encoding="utf-8")
changed = add_patch_exceptions(self.root, [target])
self.assertIn(self.root / ".gitignore", changed)
self.assertIn(nested, changed)
nested_text = nested.read_text(encoding="utf-8")
self.assertIn("!/pictures/menu\\ image.png", nested_text)
root_text = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertIn("!/www/img/pictures/menu\\ image.png", root_text)
def test_plain_selected_image_is_added_without_requiring_key(self):
plain = self.content / "img" / "pictures" / "menu.png"
plain.write_bytes(png_bytes("yellow"))
asset = scan_image_assets(self.root)[0]
result = prepare_assets_for_patch(self.root, [asset], None)
self.assertEqual(result.completed, 1)
self.assertEqual(result.errors, [])
ignore = self.root.joinpath(".gitignore").read_text(encoding="utf-8")
self.assertIn("!/www/img/pictures/menu.png", ignore)
def test_workspace_edit_of_plain_game_image_is_published_and_backed_up(self):
runtime = self.content / "img" / "pictures" / "menu.png"
original = png_bytes("yellow")
translated = png_bytes("blue")
runtime.write_bytes(original)
asset = scan_image_assets(self.root)[0]
asset.plain_path.parent.mkdir(parents=True)
asset.plain_path.write_bytes(translated)
result = prepare_assets_for_patch(self.root, [asset], None)
self.assertEqual(result.completed, 1)
self.assertEqual(runtime.read_bytes(), translated)
backup = self.root / ".dazedtl" / "image_backups" / "www/img/pictures/menu.png"
self.assertEqual(backup.read_bytes(), original)
self.assertEqual(result.patch_files, [runtime])
def test_legacy_side_by_side_editable_png_is_migrated_to_workspace(self):
self._encrypted_asset()
legacy = self.content / "img" / "pictures" / "001.png"
translated = png_bytes("purple")
legacy.write_bytes(translated)
asset = scan_image_assets(self.root)[0]
result = decrypt_assets([asset], KEY, game_root=self.root)
self.assertEqual(result.completed, 1)
self.assertEqual(asset.plain_path.read_bytes(), translated)
self.assertFalse(legacy.exists())
def test_conflicting_runtime_duplicate_is_preserved_outside_game_images(self):
self._encrypted_asset()
runtime_duplicate = self.content / "img" / "pictures" / "001.png"
runtime_duplicate.write_bytes(png_bytes("blue"))
asset = scan_image_assets(self.root)[0]
asset.plain_path.parent.mkdir(parents=True)
workspace_edit = png_bytes("purple")
asset.plain_path.write_bytes(workspace_edit)
cleaned = clean_runtime_image_duplicates(self.root)
self.assertEqual(cleaned, 1)
self.assertFalse(runtime_duplicate.exists())
self.assertEqual(asset.plain_path.read_bytes(), workspace_edit)
conflict = (
self.root
/ ".dazedtl"
/ "runtime_plain_conflicts"
/ "www"
/ "img"
/ "pictures"
/ "001.png"
)
self.assertEqual(conflict.read_bytes(), png_bytes("blue"))
def test_mz_workspace_preserves_runtime_hierarchy(self):
mz_root = Path(self.temp.name) / "MZGame"
image_dir = mz_root / "img" / "pictures"
data_dir = mz_root / "data"
image_dir.mkdir(parents=True)
data_dir.mkdir(parents=True)
data_dir.joinpath("System.json").write_text(
'{"encryptionKey":"' + KEY_HEX + '"}', encoding="utf-8"
)
encrypted = image_dir / "title.png_"
encrypted.write_bytes(encrypt_image_bytes(png_bytes("green"), KEY))
asset = scan_image_assets(mz_root)[0]
result = decrypt_assets([asset], KEY, game_root=mz_root)
self.assertEqual(result.completed, 1)
self.assertEqual(
asset.plain_path,
editable_workspace_root(mz_root) / "img" / "pictures" / "title.png",
)
def test_legacy_editable_folder_is_moved_under_dazedtl(self):
legacy = self.root / "DazedTL_Images" / "www" / "img" / "pictures"
legacy.mkdir(parents=True)
translated = png_bytes("purple")
legacy.joinpath("001.png").write_bytes(translated)
moved = migrate_legacy_editable_workspace(self.root)
destination = (
self.root / ".dazedtl" / "images" / "www" / "img" / "pictures" / "001.png"
)
self.assertEqual(moved, 1)
self.assertEqual(destination.read_bytes(), translated)
self.assertFalse((self.root / "DazedTL_Images").exists())
def test_scan_uses_editable_workspace_as_the_edit_list(self):
self._encrypted_asset("001.rpgmvp")
self._encrypted_asset("002.rpgmvp", "blue")
assets = scan_image_assets(self.root)
assets[1].plain_path.parent.mkdir(parents=True, exist_ok=True)
assets[1].plain_path.write_bytes(png_bytes("purple"))
rescanned = scan_image_assets(self.root)
self.assertEqual(
{asset.asset_id for asset in rescanned if asset.has_plain},
{assets[1].asset_id},
)
if __name__ == "__main__":
unittest.main()