From f9a8e160eabd450710906e797fd73f2a41f8bba3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 10:14:34 +0000 Subject: [PATCH 1/3] test(common): add case-aware is_kotor_install_dir regression coverage Co-authored-by: PuritanWizard --- .../tests/common/test_is_kotor_install_dir.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Libraries/PyKotor/tests/common/test_is_kotor_install_dir.py diff --git a/Libraries/PyKotor/tests/common/test_is_kotor_install_dir.py b/Libraries/PyKotor/tests/common/test_is_kotor_install_dir.py new file mode 100644 index 000000000..c143ffb2c --- /dev/null +++ b/Libraries/PyKotor/tests/common/test_is_kotor_install_dir.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import pathlib +import sys +import tempfile +import unittest + +THIS_SCRIPT_PATH = pathlib.Path(__file__).resolve() +PYKOTOR_PATH = THIS_SCRIPT_PATH.parents[3].joinpath("src") +UTILITY_PATH = THIS_SCRIPT_PATH.parents[5].joinpath("Libraries", "Utility", "src") + + +def add_sys_path(p: pathlib.Path) -> None: + working_dir = str(p) + if working_dir not in sys.path: + sys.path.append(working_dir) + + +if PYKOTOR_PATH.joinpath("pykotor").exists(): + add_sys_path(PYKOTOR_PATH) +if UTILITY_PATH.joinpath("utility").exists(): + add_sys_path(UTILITY_PATH) + +from pykotor.diff_tool.cli_utils import is_kotor_install_dir as cli_is_kotor_install_dir +from pykotor.tools.patching import is_kotor_install_dir as patching_is_kotor_install_dir +from pykotor.tslpatcher.diff.engine import is_kotor_install_dir as engine_is_kotor_install_dir + + +class TestIsKotorInstallDir(unittest.TestCase): + _IMPLEMENTATIONS = ( + ("cli_utils", cli_is_kotor_install_dir), + ("patching", patching_is_kotor_install_dir), + ("diff_engine", engine_is_kotor_install_dir), + ) + + def test_detects_install_with_exact_case(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "KotorInstall" + root.mkdir() + (root / "chitin.key").write_bytes(b"key") + + for name, is_install in self._IMPLEMENTATIONS: + with self.subTest(implementation=name): + self.assertTrue(is_install(root)) + + def test_rejects_directory_without_chitin_key(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "NotInstall" + root.mkdir() + + for name, is_install in self._IMPLEMENTATIONS: + with self.subTest(implementation=name): + self.assertFalse(is_install(root)) + + def test_rejects_file_path(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + file_path = pathlib.Path(tmp) / "not_a_dir" + file_path.write_bytes(b"x") + + for name, is_install in self._IMPLEMENTATIONS: + with self.subTest(implementation=name): + self.assertFalse(is_install(file_path)) + + @unittest.skipIf( + sys.platform == "win32", + "Case mismatch semantics differ on Windows filesystems.", + ) + def test_detects_install_with_case_mismatched_chitin_key(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "KotorInstall" + root.mkdir() + (root / "CHITIN.KEY").write_bytes(b"key") + + for name, is_install in self._IMPLEMENTATIONS: + with self.subTest(implementation=name): + self.assertTrue(is_install(root)) + + @unittest.skipIf( + sys.platform == "win32", + "Case mismatch semantics differ on Windows filesystems.", + ) + def test_detects_install_with_case_mismatched_root_path(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) / "KotorInstall" + root.mkdir() + (root / "chitin.key").write_bytes(b"key") + + mismatched_root = root.parent / "kotorinstall" + + for name, is_install in self._IMPLEMENTATIONS: + with self.subTest(implementation=name): + self.assertTrue(is_install(mismatched_root)) + + +if __name__ == "__main__": + unittest.main() From a1e8129d4ec28dba28c5acfc5422bfd9f0123e1c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 10:14:39 +0000 Subject: [PATCH 2/3] test(cli): cover ci env guard for json export live progress Co-authored-by: PuritanWizard --- .../PyKotor/tests/cli/test_json_commands.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Libraries/PyKotor/tests/cli/test_json_commands.py b/Libraries/PyKotor/tests/cli/test_json_commands.py index 67a02752f..ac60c48be 100644 --- a/Libraries/PyKotor/tests/cli/test_json_commands.py +++ b/Libraries/PyKotor/tests/cli/test_json_commands.py @@ -1,11 +1,13 @@ from __future__ import annotations import base64 +import io import json import logging from argparse import Namespace from pathlib import Path +from unittest.mock import patch import pytest from loggerplus import RobustLogger @@ -28,6 +30,7 @@ from pykotor.resource.type import ResourceType from pykotor.tools.resource_json import ( _serialize_mdl_face, + _supports_live_progress, export_installation_to_json_tree, iter_installation_resource_documents, serialize_file_resource_document, @@ -922,3 +925,27 @@ def fake_main(argv: list[str]) -> int: assert "--merge-module" in captured_argv assert captured_argv.count("--merge-path") == 2 assert "--merge-conflict-policy" in captured_argv + + +@pytest.mark.parametrize("env_name", ["CI", "GITHUB_ACTIONS"]) +@pytest.mark.parametrize("env_value", ["true", "1", "yes", "TRUE"]) +def test_supports_live_progress_disabled_in_ci_env( + env_name: str, env_value: str, monkeypatch: pytest.MonkeyPatch +) -> None: + tty_stream = io.StringIO() + monkeypatch.setenv(env_name, env_value) + with patch.object(tty_stream, "isatty", return_value=True): + assert _supports_live_progress(tty_stream) is False + + +def test_supports_live_progress_follows_tty_when_not_in_ci( + monkeypatch: pytest.MonkeyPatch, +) -> None: + tty_stream = io.StringIO() + non_tty_stream = io.StringIO() + monkeypatch.delenv("CI", raising=False) + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + with patch.object(tty_stream, "isatty", return_value=True): + assert _supports_live_progress(tty_stream) is True + with patch.object(non_tty_stream, "isatty", return_value=False): + assert _supports_live_progress(non_tty_stream) is False From 5510ccf2e4c31491f7925ce023bc97763c37964e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 10:14:39 +0000 Subject: [PATCH 3/3] test(extract,tslpatcher): cover modules casing and case-aware ini paths Co-authored-by: PuritanWizard --- .../tests/extract/test_installation.py | 9 +++++++++ .../PyKotor/tests/tslpatcher/test_reader.py | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Libraries/PyKotor/tests/extract/test_installation.py b/Libraries/PyKotor/tests/extract/test_installation.py index 6b0371cc3..76d8e9fd5 100644 --- a/Libraries/PyKotor/tests/extract/test_installation.py +++ b/Libraries/PyKotor/tests/extract/test_installation.py @@ -43,6 +43,15 @@ def add_sys_path(p: pathlib.Path): class TestInstallation(TestCase): + def test_create_installation_uses_modules_directory_casing(self): + with tempfile.TemporaryDirectory() as tmp: + install_path = Path(tmp) / "k1_install" + create_installation(install_path, Game.K1) + + self.assertTrue((install_path / "Modules").is_dir()) + if sys.platform != "win32": + self.assertFalse((install_path / "modules").exists()) + @classmethod def setUpClass(cls): # Create temporary directory for installation diff --git a/Libraries/PyKotor/tests/tslpatcher/test_reader.py b/Libraries/PyKotor/tests/tslpatcher/test_reader.py index 613bb544c..bfe8fa055 100644 --- a/Libraries/PyKotor/tests/tslpatcher/test_reader.py +++ b/Libraries/PyKotor/tests/tslpatcher/test_reader.py @@ -1850,5 +1850,25 @@ def _setupIniAndConfig(self, ini_text: str) -> PatcherConfig: # endregion +class TestConfigReaderFromFilepath(unittest.TestCase): + @unittest.skipIf( + sys.platform == "win32", + "Case mismatch semantics differ on Windows filesystems.", + ) + def test_from_filepath_resolves_case_mismatched_mod_directory(self): + with tempfile.TemporaryDirectory() as tmp: + mod_root = Path(tmp) / "MyMod" + mod_root.mkdir() + ini_path = mod_root / "changes.ini" + ini_path.write_text( + "[Settings]\nLookupGameFolder=0\nLookupGameNumber=1\n", + encoding="utf-8", + ) + + reader = ConfigReader.from_filepath(mod_root.parent / "mymod" / "CHANGES.INI") + self.assertTrue(reader.mod_path.is_dir()) + self.assertEqual(reader.mod_path.name, "MyMod") + + if __name__ == "__main__": unittest.main()