From 6afb0e298538c2513a8717c92aefa142f1585fc7 Mon Sep 17 00:00:00 2001 From: foursecondfivefour <291715999+foursecondfivefour@users.noreply.github.com> Date: Fri, 13 Nov 2026 09:12:15 +0000 Subject: [PATCH] Add Python and C++ differential compatibility tests Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/workflows/cpp.yml | 12 +-- cpp/CMakeLists.txt | 1 + cpp/README.md | 4 + cpp/scripts/upload_alpha.py | 16 --- cpp/tests/test_compatibility.py | 186 ++++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 25 deletions(-) delete mode 100644 cpp/scripts/upload_alpha.py create mode 100644 cpp/tests/test_compatibility.py diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 16d99dd..9efc7cc 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -35,10 +35,10 @@ jobs: run: python cpp/scripts/report_test_failures.py - name: Package Linux x86-64 if: matrix.gui == 'ON' && runner.os == 'Linux' - run: python cpp/scripts/package.py --build cpp/build --platform linux --output packages/fear-arch00-tool-0.1.0-alpha-linux-x86_64.zip + run: python cpp/scripts/package.py --build cpp/build --platform linux --output packages/fear-arch00-tool-ci-linux-x86_64.zip - name: Package Windows x86-64 if: matrix.gui == 'ON' && runner.os == 'Windows' - run: python cpp/scripts/package.py --build cpp/build --platform windows --output packages/fear-arch00-tool-0.1.0-alpha-windows-x86_64.zip + run: python cpp/scripts/package.py --build cpp/build --platform windows --output packages/fear-arch00-tool-ci-windows-x86_64.zip - name: Upload tested package if: matrix.gui == 'ON' uses: actions/upload-artifact@v4 @@ -47,9 +47,3 @@ jobs: path: packages/*.zip if-no-files-found: error retention-days: 7 - # Only the explicitly requested alpha release commit uploads assets. - - name: Upload alpha asset to draft release - if: matrix.gui == 'ON' && github.event_name == 'push' && github.ref == 'refs/heads/arena/2682f006-arch00tool-fear-1-arch00-ltar' && startsWith(github.event.head_commit.message, '[alpha-release]') - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: python cpp/scripts/upload_alpha.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 016324b..a2a050b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -29,6 +29,7 @@ if(BUILD_TESTING) add_test(NAME benchmark-smoke COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/benchmarks/benchmark.py" --cpp "$" --files 4 --size-kib 16 --repeat 1 --jobs 2) add_test(NAME parallel COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_parallel.py" "$") add_test(NAME unicode-paths COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_unicode_paths.py" "$") + add_test(NAME compatibility COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_compatibility.py" "$") add_test(NAME parser COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_parser.py" "$") add_test(NAME cli COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cli.py" "$") add_test(NAME repack COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_repack.py" "$") diff --git a/cpp/README.md b/cpp/README.md index 88db93b..27baf7a 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -56,3 +56,7 @@ Measurements include process startup and are influenced by OS caching, CPU, file ## Continuous integration and Unicode paths `.github/workflows/cpp.yml` configures, builds, and runs the C++ tests with and without the optional GUI on Linux and Windows GitHub runners. The Windows CLI receives command-line arguments as UTF-16 and converts them to UTF-8 for the archive core; names printed by the CLI are UTF-8. Automated Unicode-path fixtures cover input, output, patterns, replacements, and batch operations. Windows console font and terminal encoding settings may still affect display. CI does not establish compatibility with actual game archives or test the GUI in an interactive browser. + +## Python compatibility checks + +The `compatibility` CTest suite generates synthetic LTAR v3 archives and compares Python and C++ validation, extraction, selected patterns, and repacking, including raw/zlib modes, CP1252 names, empty entries, replacement files, and several corrupted streams. Repack outputs are also compared byte-for-byte for the tested fixtures with one and four workers. These are differential tests, not proof of complete behavioral equivalence or game compatibility. Original game archives and unusual path, compression, metadata, and platform cases still need independent validation. diff --git a/cpp/scripts/upload_alpha.py b/cpp/scripts/upload_alpha.py deleted file mode 100644 index 900e67d..0000000 --- a/cpp/scripts/upload_alpha.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Upload a tested package from CI to the explicitly created alpha draft.""" -import subprocess -import sys -import time -from pathlib import Path - -release = 'v0.1.0-alpha' -package = next(Path('packages').glob('*.zip')) -for _ in range(60): - result = subprocess.run(['gh', 'release', 'view', release], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if result.returncode == 0: - break - time.sleep(5) -else: - sys.exit('Alpha draft release was not found within five minutes') -subprocess.run(['gh', 'release', 'upload', release, str(package)], check=True) diff --git a/cpp/tests/test_compatibility.py b/cpp/tests/test_compatibility.py new file mode 100644 index 0000000..57fa87f --- /dev/null +++ b/cpp/tests/test_compatibility.py @@ -0,0 +1,186 @@ +"""Differential tests against arch00tool.py on generated LTAR v3 archives.""" +import importlib.util +import os +from pathlib import Path +import random +import struct +import subprocess +import sys +import tempfile +import zlib + +CPP = Path(sys.argv[1]) +PY_TOOL = Path(__file__).resolve().parents[2] / 'arch00tool.py' +sys.dont_write_bytecode = True +SPEC = importlib.util.spec_from_file_location('arch00tool_reference', PY_TOOL) +REFERENCE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = REFERENCE +SPEC.loader.exec_module(REFERENCE) + + +def run(cmd, *args): + env = dict(os.environ, PYTHONUTF8='1') + return subprocess.run([*map(str, cmd), *map(str, args)], capture_output=True, + text=True, encoding='utf-8', errors='replace', env=env) + + +def make_archive(path, entries): + # File records stay in logical order while payloads are in reverse physical + # order, with gaps and an unreferenced trailer, to exercise repacking. + folder = b'assets' + names = bytearray(folder + b'\0') + name_offsets = [] + for name, *_ in entries: + name_offsets.append(len(names)) + names.extend(name + b'\0') + metadata_size = 48 + len(names) + 32 * len(entries) + 16 + offsets = [0] * len(entries) + body = bytearray() + for index in reversed(range(len(entries))): + body.extend(b'gap') + offsets[index] = metadata_size + len(body) + body.extend(entries[index][1]) + records = b''.join(struct.pack('