diff --git a/bsp_tool/branches/nexon/cso2.py b/bsp_tool/branches/nexon/cso2.py index 2ae84f67..b57bc682 100644 --- a/bsp_tool/branches/nexon/cso2.py +++ b/bsp_tool/branches/nexon/cso2.py @@ -1,8 +1,6 @@ """2013-2017 format""" # https://git.sr.ht/~leite/cso2-bsp-converter/tree/master/item/src/bsptypes.hpp import enum -import io -import zipfile from .. import base from ..valve import source @@ -100,22 +98,10 @@ class LumpHeader(base.MappedArray): # special lump classes, in alphabetical order: -class PakFile(zipfile.ZipFile): # WIP - """CSO2 PakFiles have a custom .zip format""" - # NOTE: it's not as simple as changing the FILE_MAGIC - # -- this appears to be a unique implementation of .zip - # b"CS" file magic & different header format? - def __init__(self, raw_zip: bytes): - # TODO: translate header to b"PK\x03\x04..." - raw_zip = b"".join([b"PK", raw_zip[2:]]) # not that easy - self._buffer = io.BytesIO(raw_zip) - super(PakFile, self).__init__(self._buffer) - - def as_bytes(self) -> bytes: - # TODO: translate header to b"CS\x03\x04..." - raw_zip = self._buffer.getvalue() - raw_zip = b"".join([b"CS", raw_zip[2:]]) # not that easy - return raw_zip +# TODO: PakFile +# -- struct magics use b"CS" instead of b"PK" +# -- changes may go deeper than his +# -- hopefully just a reskin of PK/3/4 ZIP_STORE # {"LUMP_NAME": {version: LumpClass}} diff --git a/bsp_tool/branches/respawn/titanfall.py b/bsp_tool/branches/respawn/titanfall.py index 62e78ea7..4e62e1a4 100644 --- a/bsp_tool/branches/respawn/titanfall.py +++ b/bsp_tool/branches/respawn/titanfall.py @@ -1068,7 +1068,7 @@ def as_bytes(self) -> bytes: "ENTITIES": {0: shared.Entities}, # NOTE: .ent files are handled directly by the RespawnBsp class "LEVEL_INFO": {0: LevelInfo}, - "PAKFILE": {0: shared.PakFile}, + "PAKFILE": {0: source.PakFile}, # "PHYSICS_COLLIDE": {0: physics.CollideLump}, # BROKEN .as_bytes() "TEXTURE_DATA_STRING_DATA": {0: shared.TextureDataStringData}} # TODO: LightProbeParentInfos/BspNodes/RefIds & StaticPropLightProbeIndices may all be Special diff --git a/bsp_tool/branches/shared.py b/bsp_tool/branches/shared.py index 528125da..5881087f 100644 --- a/bsp_tool/branches/shared.py +++ b/bsp_tool/branches/shared.py @@ -1,8 +1,6 @@ -import io import math import re -import zipfile -from typing import Any, Dict, List +from typing import Dict, List # Basic Lump Classes @@ -115,38 +113,6 @@ def from_bytes(cls, raw_lump: bytes): return cls(entities) -class PakFile(zipfile.ZipFile): - _buffer: io.BytesIO - - def __init__(self, file_: Any = None, mode: str = "a", **kwargs): - """always a read-only copy of the lump""" - if file_ is None: - empty_zip = [b"PK\x05\x06", b"\x00" * 16, b"\x20\x00XZP1\x20\x30", b"\x00" * 26] - self._buffer = io.BytesIO(b"".join(empty_zip)) - elif isinstance(file_, io.BytesIO): # BspClass will take this route via .from_bytes() - self._buffer = file_ - elif isinstance(file_, str): - self._buffer = io.BytesIO(open(file_, "rb").read()) - else: - raise TypeError(f"Cannot create {self.__class__.__name__} from type '{type(file_)}'") - super().__init__(self._buffer, mode=mode, **kwargs) - - def as_bytes(self) -> bytes: - # write ending records if edits were made (adapted from ZipFile.close) - if self.mode in "wxa" and self._didModify and self.fp is not None: - with self._lock: - if self._seekable: - self.fp.seek(self.start_dir) - self._write_end_record() - self._didModify = False # don't double up when .close() is called - # NOTE: .close() can get funky but it's OK because ._buffer isn't a real file - return self._buffer.getvalue() - - @classmethod - def from_bytes(cls, raw_lump: bytes): - return cls(io.BytesIO(raw_lump)) - - class TextureDataStringData(list): def __init__(self, iterable: List[str] = tuple()): super().__init__(iterable) diff --git a/bsp_tool/branches/valve/orange_box_x360.py b/bsp_tool/branches/valve/orange_box_x360.py index 2e20c5b3..594889aa 100644 --- a/bsp_tool/branches/valve/orange_box_x360.py +++ b/bsp_tool/branches/valve/orange_box_x360.py @@ -85,6 +85,8 @@ class MAX(enum.Enum): # special lump classes, in alphabetical order: +# TODO: PakFile_x360 +# TODO: PhysicsCollide_x360 # class PhysicsDisplacement_x360(physics.Displacement): # _int = UnsignedShort_x360 @@ -134,12 +136,8 @@ class GameLump_SPRPv10_x360(orange_box.GameLump_SPRPv10): # sprp GAME LUMP (LUM globals()[LumpClass_name] = LumpClass del LumpClass_name, LumpClass - SPECIAL_LUMP_CLASSES = {"ENTITIES": {0: shared.Entities}, - # "PAKFILE": {0: shared.PakFile_x360}, - # "PHYSICS_DISPLACEMENT": {0: PhysicsDisplacement_x360}, "TEXTURE_DATA_STRING_DATA": {0: shared.TextureDataStringData}} -# TODO: PhysicsCollide_x360 GAME_LUMP_HEADER = x360.make_big_endian(orange_box.GAME_LUMP_HEADER) diff --git a/bsp_tool/branches/valve/source.py b/bsp_tool/branches/valve/source.py index 79c98e1f..35d51227 100644 --- a/bsp_tool/branches/valve/source.py +++ b/bsp_tool/branches/valve/source.py @@ -6,7 +6,8 @@ import enum import io import struct -from typing import List +from typing import Any, List +import zipfile from ... import lumps from .. import base @@ -918,6 +919,42 @@ class GameLump_SPRPv7(GameLump_SPRPv6): # sprp GameLump (LUMP 35) StaticPropClass = StaticPropv7 +class PakFile(zipfile.ZipFile): # LUMP 40 + _buffer: io.BytesIO + + def __init__(self, file_: Any = None, mode: str = "a", **kwargs): + """always a read-only copy of the lump""" + if file_ is None: + empty_zip = [b"PK\x05\x06", b"\x00" * 16, b"\x20\x00XZP1 0", b"\x00" * 26] + self._buffer = io.BytesIO(b"".join(empty_zip)) + elif isinstance(file_, io.BytesIO): # BspClass will take this route via .from_bytes() + self._buffer = file_ + elif isinstance(file_, str): + self._buffer = io.BytesIO(open(file_, "rb").read()) + else: + raise TypeError(f"Cannot create {self.__class__.__name__} from type '{type(file_)}'") + super().__init__(self._buffer, mode=mode, **kwargs) + + def __repr__(self) -> str: + dev_branch_class = ".".join([*self.__module__.split(".")[-2:], self.__class__.__name__]) + return f"<{dev_branch_class} {len(self.namelist())} files mode='{self.mode}' @ 0x{id(self):016X}>" + + def as_bytes(self) -> bytes: + # write ending records if edits were made (adapted from ZipFile.close) + if self.mode in "wxa" and self._didModify and self.fp is not None: + with self._lock: + if self._seekable: + self.fp.seek(self.start_dir) + self._write_end_record() + self._didModify = False # don't double up when .close() is called + # NOTE: .close() can get funky but it's OK because ._buffer isn't a real file + return self._buffer.getvalue() + + @classmethod + def from_bytes(cls, raw_lump: bytes): + return cls(io.BytesIO(raw_lump)) + + # {"LUMP_NAME": {version: LumpClass}} BASIC_LUMP_CLASSES = {"DISPLACEMENT_TRIANGLES": {0: DisplacementTriangle}, "FACE_IDS": {0: shared.UnsignedShorts}, @@ -963,7 +1000,7 @@ class GameLump_SPRPv7(GameLump_SPRPv6): # sprp GameLump (LUMP 35) SPECIAL_LUMP_CLASSES = {"ENTITIES": {0: shared.Entities}, "TEXTURE_DATA_STRING_DATA": {0: shared.TextureDataStringData}, - "PAKFILE": {0: shared.PakFile}, + "PAKFILE": {0: PakFile}, # "PHYSICS_COLLIDE": {0: physics.CollideLump}, # BROKEN .as_bytes() "PHYSICS_DISPLACEMENT": {0: physics.Displacement}, "VISIBILITY": {0: quake2.Visibility}} diff --git a/bsp_tool/extensions/diff/__init__.py b/bsp_tool/extensions/diff/__init__.py index d1e9e924..337d5da7 100644 --- a/bsp_tool/extensions/diff/__init__.py +++ b/bsp_tool/extensions/diff/__init__.py @@ -3,6 +3,7 @@ from . import base from . import shared +from .valve import source from bsp_tool import branches from bsp_tool.base import Bsp @@ -10,6 +11,7 @@ def diff_lumps(old_lump: Any, new_lump: Any) -> base.Diff: + """lookup table & intialiser""" LumpClasses = set() for lump in (old_lump, new_lump): if issubclass(lump.__class__, BasicBspLump): @@ -24,8 +26,8 @@ def diff_lumps(old_lump: Any, new_lump: Any) -> base.Diff: raise NotImplementedError("Cannot diff lumps of differring LumpClass") if LumpClasses == {branches.shared.Entities}: DiffClass = shared.EntitiesDiff - elif LumpClasses == {branches.shared.PakFile}: - DiffClass = shared.PakFileDiff + elif LumpClasses == {branches.valve.source.PakFile}: + DiffClass = source.PakFileDiff elif RawBspLump in LumpClasses or ExternalRawBspLump in LumpClasses: # TODO: core.xxd diff raise NotImplementedError("Cannot diff raw lumps") diff --git a/bsp_tool/extensions/diff/shared.py b/bsp_tool/extensions/diff/shared.py index c0e3dfc6..a55c862f 100644 --- a/bsp_tool/extensions/diff/shared.py +++ b/bsp_tool/extensions/diff/shared.py @@ -1,10 +1,7 @@ import difflib -import io -import os -from typing import Dict, Generator, List, Tuple +from typing import Dict, Generator, List from . import base -from . import core class EntitiesDiff(base.Diff): @@ -64,57 +61,3 @@ def long_repr(entity: Dict[str, str]) -> List[str]: else: out.append(f'"{key}" "{value}"') return ["{", *out, "}"] - - -class PakFileDiff(base.Diff): - """Works on any ValveBsp based .bsp (except CS:O2)""" - - def short_stats(self) -> str: - """quick & dirty namelist check""" - old = set(self.old.namelist()) - new = set(self.new.namelist()) - 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()] - new_filelist = [f"{fn}\n" for fn in self.new.namelist()] - 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(" ").rstrip("\n") - old_file = self.old.read(filename) - new_file = self.new.read(filename) - if old_file == new_file: - yield meta_line - else: - 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/bsp_tool/extensions/diff/valve/source.py b/bsp_tool/extensions/diff/valve/source.py new file mode 100644 index 00000000..58b3edab --- /dev/null +++ b/bsp_tool/extensions/diff/valve/source.py @@ -0,0 +1,62 @@ +import difflib +import io +import os + +from typing import Generator, Tuple + +from .. import base +from .. import core + + +class PakFileDiff(base.Diff): + """Works on any ValveBsp based .bsp (except CS:O2)""" + + def short_stats(self) -> str: + """quick & dirty namelist check""" + old = set(self.old.namelist()) + new = set(self.new.namelist()) + 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()] + new_filelist = [f"{fn}\n" for fn in self.new.namelist()] + 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(" ").rstrip("\n") + old_file = self.old.read(filename) + new_file = self.new.read(filename) + if old_file == new_file: + yield meta_line + else: + 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/branches/test_shared.py b/tests/branches/test_shared.py index 3145a7cb..9c47ffee 100644 --- a/tests/branches/test_shared.py +++ b/tests/branches/test_shared.py @@ -1,81 +1,12 @@ -import os - -from bsp_tool.branches import shared - -import pytest +# from bsp_tool.branches import shared class TestEntities: - # TODO: test all parser edge cases - ... - - -class TestPakFile: - zips = {"empty.zip": b"".join([b"PK\x05\x06", b"\x00" * 16, - b"\x20\x00XZP1 0", b"\x00" * 26]), - "deflate.zip": b"".join([b"PK\x03\x04\x14", b"\x00" * 5, - b"\x92\x6E\xEF\x56\x99\xA0\xDC\x42", - b"\x07\x00\x00\x00" * 2, b"\x08\x00\x00\x00", - b"test.txthello~\n", - b"PK\x01\x02\x14\x03\x14", b"\x00" * 5, - b"\x92\x6E\xEF\x56\x99\xA0\xDC\x42", - b"\x07\x00\x00\x00" * 2, b"\x08\x00\x00\x00", - b"\x00" * 8, b"\x80\x01\x00\x00\x00\x00test.txt" - b"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00", - b"\x36\x00\x00\x00\x2D\x00\x00\x00", - b"\x20\x00XZP1 0", b"\x00" * 26])} - expected = {"empty.zip": dict(), - "deflate.zip": {"test.txt": b"hello~\n"}} - - def test_new(self): - """create & populate a PakFile from nothing""" - pk = shared.PakFile() - pk.writestr("test.txt", "hello~\n") - assert pk.namelist() == ["test.txt"] - assert pk.read("test.txt") == b"hello~\n" - - def setup_method(self, method): - """create test zipfiles""" - if method.__name__ in ("test_from_file", "test_bytes"): - for filename, data in self.zips.items(): - with open(filename, "wb") as zip_file: - zip_file.write(data) - - def teardown_method(self, method): - """delete test zipfiles""" - if method.__name__ in ("test_from_file", "test_bytes"): - for filename in self.zips: - os.remove(filename) - - @pytest.mark.parametrize("test_zip", zips) - def test_from_file(self, test_zip: str): - """open a .zip file""" - pk = shared.PakFile(test_zip) - assert set(pk.namelist()) == set(self.expected[test_zip]) - for filename in pk.namelist(): - assert pk.read(filename) == self.expected[test_zip][filename] - - @pytest.mark.parametrize("test_zip", zips) - def test_bytes(self, test_zip: str): - raw_zip = self.zips[test_zip] - pk = shared.PakFile.from_bytes(raw_zip) - assert pk.as_bytes() == raw_zip - - def test_save_changes(self): - pk = shared.PakFile() - pk.writestr("test.txt", "hello~\n") - raw_zip = pk.as_bytes() - # valid zip - pk2 = shared.PakFile.from_bytes(raw_zip) - assert pk.namelist() == pk2.namelist() - for filename in pk.namelist(): - assert pk.read(filename) == pk2.read(filename) - # continue editing - pk.writestr("test2.txt", "~world\n") - # pk.close() - raw_zip = pk.as_bytes() - # valid zip - pk2 = shared.PakFile.from_bytes(raw_zip) - assert pk.namelist() == pk2.namelist() - for filename in pk.namelist(): - assert pk.read(filename) == pk2.read(filename) + class TestFromBytes: + ... + # def test_basic_case(self): + # parsed = shared.Entities.from_bytes(...) + # assert parsed ... + + # def test_edge_case1(self): + # ... diff --git a/tests/branches/valve/test_source.py b/tests/branches/valve/test_source.py new file mode 100644 index 00000000..f0a5fe86 --- /dev/null +++ b/tests/branches/valve/test_source.py @@ -0,0 +1,76 @@ +import os + +from bsp_tool.branches.valve import source + +import pytest + + +class TestPakFile: + zips = {"empty.zip": b"".join([b"PK\x05\x06", b"\x00" * 16, + b"\x20\x00XZP1 0", b"\x00" * 26]), + "deflate.zip": b"".join([b"PK\x03\x04\x14", b"\x00" * 5, + b"\x92\x6E\xEF\x56\x99\xA0\xDC\x42", + b"\x07\x00\x00\x00" * 2, b"\x08\x00\x00\x00", + b"test.txthello~\n", + b"PK\x01\x02\x14\x03\x14", b"\x00" * 5, + b"\x92\x6E\xEF\x56\x99\xA0\xDC\x42", + b"\x07\x00\x00\x00" * 2, b"\x08\x00\x00\x00", + b"\x00" * 8, b"\x80\x01\x00\x00\x00\x00test.txt" + b"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00", + b"\x36\x00\x00\x00\x2D\x00\x00\x00", + b"\x20\x00XZP1 0", b"\x00" * 26])} + expected = {"empty.zip": dict(), + "deflate.zip": {"test.txt": b"hello~\n"}} + + def test_new(self): + """create & populate a PakFile from nothing""" + pk = source.PakFile() + pk.writestr("test.txt", "hello~\n") + assert pk.namelist() == ["test.txt"] + assert pk.read("test.txt") == b"hello~\n" + + def setup_method(self, method): + """create test zipfiles""" + if method.__name__ in ("test_from_file", "test_bytes"): + for filename, data in self.zips.items(): + with open(filename, "wb") as zip_file: + zip_file.write(data) + + def teardown_method(self, method): + """delete test zipfiles""" + if method.__name__ in ("test_from_file", "test_bytes"): + for filename in self.zips: + os.remove(filename) + + @pytest.mark.parametrize("test_zip", zips) + def test_from_file(self, test_zip: str): + """open a .zip file""" + pk = source.PakFile(test_zip) + assert set(pk.namelist()) == set(self.expected[test_zip]) + for filename in pk.namelist(): + assert pk.read(filename) == self.expected[test_zip][filename] + + @pytest.mark.parametrize("test_zip", zips) + def test_bytes(self, test_zip: str): + raw_zip = self.zips[test_zip] + pk = source.PakFile.from_bytes(raw_zip) + assert pk.as_bytes() == raw_zip + + def test_save_changes(self): + pk = source.PakFile() + pk.writestr("test.txt", "hello~\n") + raw_zip = pk.as_bytes() + # valid zip + pk2 = source.PakFile.from_bytes(raw_zip) + assert pk.namelist() == pk2.namelist() + for filename in pk.namelist(): + assert pk.read(filename) == pk2.read(filename) + # continue editing + pk.writestr("test2.txt", "~world\n") + # pk.close() + raw_zip = pk.as_bytes() + # valid zip + pk2 = source.PakFile.from_bytes(raw_zip) + assert pk.namelist() == pk2.namelist() + for filename in pk.namelist(): + assert pk.read(filename) == pk2.read(filename) diff --git a/tests/extensions/diff/test_BspDiff.py b/tests/extensions/diff/test_BspDiff.py index 9b9ec07b..28f44990 100644 --- a/tests/extensions/diff/test_BspDiff.py +++ b/tests/extensions/diff/test_BspDiff.py @@ -35,7 +35,7 @@ def test_lump_removed(self): # TODO: TestNoneDiff (short_stats only) # TODO: TestDiffLumps (assigning diff class) # -- branches.shared.Entities -> diff.shared.EntitiesDiff -# -- branches.shared.PakFile -> diff.shared.PakFileDiff +# -- branches.valve.source.PakFile -> diff.valve.source.PakFileDiff # -- branches.base.* -> diff.base.Diff # -- RawBspLump -> NotImplementedError # -- * -> diff.base.Diff diff --git a/tests/extensions/diff/test_shared.py b/tests/extensions/diff/test_shared.py index 65250ba0..c3ec3ed0 100644 --- a/tests/extensions/diff/test_shared.py +++ b/tests/extensions/diff/test_shared.py @@ -1,8 +1,6 @@ import itertools -import zipfile from bsp_tool.extensions import diff -from bsp_tool.branches.shared import PakFile class TestEntitiesDiff: @@ -26,35 +24,3 @@ def test_unified_diff(self): 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: - 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 diff --git a/tests/extensions/diff/valve/__init__.py b/tests/extensions/diff/valve/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/extensions/diff/valve/test_source.py b/tests/extensions/diff/valve/test_source.py new file mode 100644 index 00000000..c2907afd --- /dev/null +++ b/tests/extensions/diff/valve/test_source.py @@ -0,0 +1,39 @@ +import itertools +import zipfile + +from bsp_tool import branches +from bsp_tool.extensions import diff + + +class TestPakfileDiff: + def setup_method(self, method): + # TODO: parametrize PakFile baseclass (source.PakFile, cso2.PakFile) + old = branches.valve.source.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 = branches.valve.source.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") + # create a PakFile diff to interrogate in tests: + self.sample = diff.valve.source.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