diff --git a/bsp_tool/extensions/diff/shared.py b/bsp_tool/extensions/diff/shared.py index 787e26a0..c0e3dfc6 100644 --- a/bsp_tool/extensions/diff/shared.py +++ b/bsp_tool/extensions/diff/shared.py @@ -1,5 +1,7 @@ import difflib -from typing import Dict, Generator, List +import io +import os +from typing import Dict, Generator, List, Tuple from . import base from . import core @@ -64,14 +66,16 @@ def long_repr(entity: Dict[str, str]) -> List[str]: return ["{", *out, "}"] -class PakFileDiff: +class PakFileDiff(base.Diff): """Works on any ValveBsp based .bsp (except CS:O2)""" - def shortstat(self) -> str: + def short_stats(self) -> str: """quick & dirty namelist check""" old = set(self.old.namelist()) new = set(self.new.namelist()) - return f"{new.difference(old)} insertions(+) {old.difference(new)} deletions(-)" + added = len(new.difference(old)) + removed = len(old.difference(new)) + return f"{added} insertions(+) {removed} deletions(-)" def unified_diff(self) -> Generator[str, None, None]: old_filelist = [f"{fn}\n" for fn in self.old.namelist()] @@ -79,16 +83,38 @@ def unified_diff(self) -> Generator[str, None, None]: meta_diff = difflib.ndiff(old_filelist, new_filelist) for meta_line in meta_diff: if meta_line.startswith(" "): # maybe a match - filename = meta_line.lstrip() + filename = meta_line.lstrip(" ").rstrip("\n") old_file = self.old.read(filename) new_file = self.new.read(filename) if old_file == new_file: yield meta_line else: - yield f"# BEGIN {filename}" - # NOTE: we could grab date_time from ZipInfo & format it, but lazy - for line in difflib.unified_diff(core.xxd(old_file), core.xxd(new_file), [filename] * 2): - yield line - yield f"# END {filename}" + for line in self.diff_file(filename): + yield f" {line}" else: yield meta_line + + def diff_file(self, filename: str) -> Generator[str, None, None]: + old_file = self.old.read(filename) + new_file = self.new.read(filename) + _, ext = os.path.splitext(filename) + # TODO: check MegaTest .bsps for other common plaintext file formats + if ext in (".log", ".vmt", ".txt"): + try: + old = io.StringIO(old_file.decode()).readlines() + new = io.StringIO(new_file.decode()).readlines() + except UnicodeDecodeError: # not pure utf-8 + old = list(core.xxd(old_file)) + new = list(core.xxd(new_file)) + else: # binary diff + old = list(core.xxd(old_file)) + new = list(core.xxd(new_file)) + old_time = self.formatted_date_time(self.old.getinfo(filename).date_time) + new_time = self.formatted_date_time(self.new.getinfo(filename).date_time) + for line in difflib.unified_diff(old, new, filename, filename, old_time, new_time): + yield line + + @staticmethod + def formatted_date_time(zipinfo_date_time: Tuple[int]) -> str: + year, month, day, hour, minute, second = zipinfo_date_time + return f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}" diff --git a/tests/extensions/diff/test_shared.py b/tests/extensions/diff/test_shared.py index 55c7c724..65250ba0 100644 --- a/tests/extensions/diff/test_shared.py +++ b/tests/extensions/diff/test_shared.py @@ -1,30 +1,60 @@ import itertools +import zipfile from bsp_tool.extensions import diff - - -old = [dict(classname="worldspawn"), - dict(classname="light", origin="0 0 0")] -new = [dict(classname="worldspawn"), - dict(classname="info_player_start", origin="0 0 0"), - dict(classname="light", origin="0 0 64")] -sample = diff.shared.EntitiesDiff(old, new) +from bsp_tool.branches.shared import PakFile class TestEntitiesDiff: + def setup_method(self, method): + old = [dict(classname="worldspawn"), + dict(classname="light", origin="0 0 0")] + new = [dict(classname="worldspawn"), + dict(classname="info_player_start", origin="0 0 0"), + dict(classname="light", origin="0 0 64")] + self.sample = diff.shared.EntitiesDiff(old, new) + def test_short_stats(self): - stats = sample.short_stats() + stats = self.sample.short_stats() assert stats == "2 insertions(+) 1 deletions(-)" def test_unified_diff(self): - # TODO: test a multiline diff - expected = ["- \n", - "+ \n", - "+ \n"] - for line, expected_line in itertools.zip_longest(sample.unified_diff(), expected): - assert line == expected_line + # TODO: test a multiline repr diff + expected_lines = ["- \n", + "+ \n", + "+ \n"] + diff_lines = self.sample.unified_diff() + for diff_line, expected_line in itertools.zip_longest(diff_lines, expected_lines): + assert diff_line == expected_line class TestPakfileDiff: - # TODO: generate dummy pakfiles for comparison - ... + def setup_method(self, method): + old = PakFile() + old.writestr("same.txt", "same hat!\n") + old.writestr(".secret", "!!! DO NOT SHIP !!!\n") + old.writestr("mispelt", "oops, typo\n") + compile_log = zipfile.ZipInfo(filename="compile.log", date_time=(1980, 1, 1, 0, 0, 0)) + old.writestr(compile_log, "REVISION: 01\n") + new = PakFile() + new.writestr("same.txt", "same hat!\n") + new.writestr("mispelled", "oops, typo\n") + compile_log = zipfile.ZipInfo(filename="compile.log", date_time=(1980, 1, 2, 0, 0, 0)) + new.writestr(compile_log, "REVISION: 02\n") + self.sample = diff.shared.PakFileDiff(old, new) + + def test_short_stats(self): + stats = self.sample.short_stats() + assert stats == "1 insertions(+) 2 deletions(-)" + + def test_unified_diff(self): + expected_lines = [" same.txt\n", + "- .secret\n", + "- mispelt\n", + "? ^\n", + "+ mispelled\n", + "? ^^^\n", + *[f" {line}" for line in self.sample.diff_file("compile.log")]] + diff_lines = self.sample.unified_diff() + for diff_line, expected_line in itertools.zip_longest(diff_lines, expected_lines): + assert diff_line == expected_line