From 2c2a26b2f0e11520a4c8cac2732db7b068063ade Mon Sep 17 00:00:00 2001 From: Jared Ketterer <36507175+snake-biscuits@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:08:09 +1000 Subject: [PATCH] preparing to use `extensions.diff` in `tests.save` --- bsp_tool/extensions/diff/__init__.py | 53 +++++++++++++++----- bsp_tool/extensions/diff/shared.py | 2 +- tests/test_save.py | 72 +++++++++++++++------------- 3 files changed, 82 insertions(+), 45 deletions(-) diff --git a/bsp_tool/extensions/diff/__init__.py b/bsp_tool/extensions/diff/__init__.py index 337d5da7..9cd33b47 100644 --- a/bsp_tool/extensions/diff/__init__.py +++ b/bsp_tool/extensions/diff/__init__.py @@ -1,9 +1,11 @@ +"""determining the equivialency of bsps & reporting findings in detail""" import difflib from typing import Any, Dict, Generator, List from . import base from . import shared from .valve import source +# TODO: lightmap diffs from bsp_tool import branches from bsp_tool.base import Bsp @@ -44,6 +46,7 @@ def diff_lumps(old_lump: Any, new_lump: Any) -> base.Diff: class BspDiff: """deferred diffs of lumps & headers etc.""" + # NOTE: not a base.Diff subclass old: Bsp new: Bsp @@ -53,10 +56,10 @@ def __init__(self, old: Bsp, new: Bsp): self.old = old self.new = new self.headers = HeadersDiff(old.headers, new.headers) - # NOTE: a change in header offsets does not imply a change in lump data # TODO: other metadata (file magic, version, revision, signature etc.) def __getattr__(self, lump_name: str) -> Any: + """retrieve differ for given lump""" old_lump = getattr(self.old, lump_name, None) new_lump = getattr(self.new, lump_name, None) no_old_lump = old_lump is None @@ -70,17 +73,39 @@ def __getattr__(self, lump_name: str) -> Any: setattr(self, lump_name, diff) # cache return diff + def has_no_changes(self) -> bool: + try: + assert self.headers.has_no_changes() + # TODO: other metadata + for lump in self.old.headers: + old_header = self.old.headers[lump] + new_header = self.new.headers[lump] + if old_header.length != 0 or new_header.length != 0: + assert getattr(self, lump).has_no_changes() + except AssertionError: + return False + return True + + def what_changed(self) -> List[str]: + check = {"headers": self.headers.has_no_changes()} + # TODO: other metadata + for lump in self.old.headers: + old_header = self.old.headers[lump] + new_header = self.new.headers[lump] + if old_header.length != 0 or new_header.length != 0: + check[lump] = getattr(self, lump).has_no_changes() + return {attr for attr, unchanged in check.items() if not unchanged} + def save(self, base_filename: str, log_mode: base.LogMode = base.LogMode.VERBOSE): """generate & save .diff files""" - # for each lump (match by name) - # filename.lump.00.ENTITIES.diff: old_goldsrc.ENTITIES (0) -> new_blue_shift.ENTITIES (1) - # filename.lump.01.PLANES.diff: old_goldsrc.PLANES (1) -> new_blue_shift.PLANES (0) - # RespawnBsp - # -- filename.ENTITITES.fx.diff: filename_fx.ent - # -- filename.lump.00XX.LUMP_NAME.diff - # -- filename.lump.00XX.LUMP_NAME.bsp_lump.diff - # filename.bsp.diff: headers & Y/N lump matches raise NotImplementedError() + # self.lump.unified_diff() -> individual files + # -- .bsp -> filename.00.ENTITIES.diff + # RespawnBsp format: + # -- .bsp -> filename.00XX.LUMP_NAME.diff + # -- .bsp_lump -> filename.external.00XX.LUMP_NAME.diff + # -- .ent -> filename.external.ent.xxxxx.diff + # should also generate a general / meta diff for metadata etc. class NoneDiff(base.Diff): @@ -118,13 +143,19 @@ def __getitem__(self, lump_name: str) -> str: if diff is None: old = f"{lump_name} {self.old[lump_name]!r}\n" new = f"{lump_name} {self.new[lump_name]!r}\n" - diff = list(difflib.unified_diff([old], [new])) + diff = list(difflib.unified_diff([old], [new]))[3:] self._cache[lump_name] = diff return diff + # TODO: check headers in order + # -- sorted({(h.offset, h.length, i) for i, h in enumerate(self.old.headers.values()) if h.length > 0}) + # -- find knock-on changes + # -- trivial differences (e.g. offset=0, length=0) + def short_stats(self) -> str: - raise NotImplementedError() # TODO: how to summarise? + # change in any attr but "offset" + raise NotImplementedError() def unified_diff(self) -> Generator[str, None, None]: for lump_name in self.old: diff --git a/bsp_tool/extensions/diff/shared.py b/bsp_tool/extensions/diff/shared.py index a55c862f..f1b25d02 100644 --- a/bsp_tool/extensions/diff/shared.py +++ b/bsp_tool/extensions/diff/shared.py @@ -56,7 +56,7 @@ def short_repr(entity: Dict[str, str]) -> str: def long_repr(entity: Dict[str, str]) -> List[str]: out = list() for key, value in entity.items(): - if not isinstance(value, list): # duplicate key (Source Input/Output) + if isinstance(value, list): # duplicate key (Source Input/Output) out.extend([f'"{key}" "{v}"' for v in value]) else: out.append(f'"{key}" "{value}"') diff --git a/tests/test_save.py b/tests/test_save.py index 8441d8d3..77b832dd 100644 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -6,21 +6,24 @@ from types import ModuleType from typing import List -from bsp_tool import D3DBsp +# BspClasses +# from bsp_tool import D3DBsp from bsp_tool import IdTechBsp from bsp_tool import QuakeBsp from bsp_tool import ReMakeQuakeBsp from bsp_tool import RespawnBsp from bsp_tool import ValveBsp +# branches from bsp_tool.branches.id_software import quake from bsp_tool.branches.id_software import quake2 from bsp_tool.branches.id_software import quake3 from bsp_tool.branches.id_software import remake_quake -from bsp_tool.branches.infinity_ward import modern_warfare +# from bsp_tool.branches.infinity_ward import modern_warfare from bsp_tool.branches.respawn import titanfall2 from bsp_tool.branches.strata import strata from bsp_tool.branches.valve import orange_box -# TODO: from bsp_tool.extensions import diff +# extensions +from bsp_tool.extensions import diff import pytest @@ -91,12 +94,9 @@ def save_and_diff_backup(BspClass: object, branch_script: ModuleType, map_path: # get filename of pre-save backup filename, ext = os.path.splitext(filename_ext) # ext includes "." filename_bak_ext = f"{filename}.bak{ext}" - # TODO: use bsp_tool.extensions.diff - # -- this would also compare .bsp_lump & .ent - raise RuntimeError(f"didn't diff {os.path.basename(filename_bak_ext)}") # TOO SLOW! - # diff_lines = difflib.unified_diff(xxd(filename_bak_ext), xxd(filename_ext), - # os.path.basename(filename_bak_ext), os.path.basename(filename_ext)) - # return "".join(diff_lines) + old_bsp = BspClass(branch_script, filename_bak_ext) # original file + new_bsp = BspClass(branch_script, filename_ext) # saved copy + return diff.BspDiff(old_bsp, new_bsp) # tests @@ -112,62 +112,68 @@ def save_and_diff_backup(BspClass: object, branch_script: ModuleType, map_path: @pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet") @map_dirs_to_test("Call of Duty 4", "Call of Duty 4/mp", ext="*.d3dbsp") def test_D3DBsp_modern_warfare(map_path: str): - diff = save_and_diff_backup(D3DBsp, modern_warfare, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # TODO: diff.HeadersDiff isn't ready for modern_warfare + # bsp_diff = save_and_diff_backup(D3DBsp, modern_warfare, map_path) + assert False + ... @pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet") @map_dirs_to_test("Quake 2") def test_IdTechBsp_quake2(map_path: str): - diff = save_and_diff_backup(IdTechBsp, quake2, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(IdTechBsp, quake2, map_path) + assert False + ... @pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet") @map_dirs_to_test("Quake 3 Arena") def test_IdTechBsp_quake3(map_path: str): - diff = save_and_diff_backup(IdTechBsp, quake3, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(IdTechBsp, quake3, map_path) + assert False + ... @pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet") @map_dirs_to_test("ReMakeQuake") def test_ReMakeQuakeBsp_remake_quake(map_path: str): - diff = save_and_diff_backup(ReMakeQuakeBsp, remake_quake, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(ReMakeQuakeBsp, remake_quake, map_path) + assert False + ... @pytest.mark.xfail @map_dirs_to_test("Titanfall 2") def test_RespawnBsp_titanfall2(map_path: str): - diff = save_and_diff_backup(RespawnBsp, titanfall2, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(RespawnBsp, titanfall2, map_path) + assert False + ... + # manually observed: + # -- bsp_diff.old.ENTITIES env_fog_controller: colour values separated w/ "\xA0" + # --- MRVN-Radiant/remap bug? mapsrc/*.map is clean + # --- might also be a "plaintext" issue, iirc \xA0 get wierd when decoded / translated + # -- bsp_diff.new.signature: +4 bytes of padding @pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet") @map_dirs_to_test("Quake") def test_QuakeBsp_quake(map_path: str): - diff = save_and_diff_backup(QuakeBsp, quake, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(QuakeBsp, quake, map_path) + assert False + ... @pytest.mark.xfail @map_dirs_to_test("Team Fortress 2") def test_ValveBsp_orange_box(map_path: str): - diff = save_and_diff_backup(ValveBsp, orange_box, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(ValveBsp, orange_box, map_path) + assert False + ... @pytest.mark.xfail @map_dirs_to_test("Momentum Mod") def test_ValveBsp_strata(map_path: str): - diff = save_and_diff_backup(ValveBsp, strata, map_path) - print("".join(diff)) - assert len(diff) == 0, "not a perfect copy" + # bsp_diff = save_and_diff_backup(ValveBsp, strata, map_path) + assert False + ...