diff --git a/Libraries/PyKotor/tests/cli/test_resource_json_progress.py b/Libraries/PyKotor/tests/cli/test_resource_json_progress.py new file mode 100644 index 000000000..ece82ae81 --- /dev/null +++ b/Libraries/PyKotor/tests/cli/test_resource_json_progress.py @@ -0,0 +1,63 @@ +"""Regression tests for JSON export progress reporting in CI/automation.""" + +from __future__ import annotations + +import io +import logging + +import pytest + +from pykotor.tools.resource_json import ( + _ExportProgressReporter, + _format_progress_bar, + _supports_live_progress, +) + + +class _FakeTty(io.StringIO): + def isatty(self) -> bool: + return True + + +def test_format_progress_bar_clamps_percent() -> None: + assert _format_progress_bar(-5.0) == "-" * len(_format_progress_bar(50.0)) + assert _format_progress_bar(150.0) == "#" * len(_format_progress_bar(50.0)) + + +@pytest.mark.parametrize("env_name", ["CI", "GITHUB_ACTIONS"]) +@pytest.mark.parametrize("env_value", ["true", "1", "yes", " TRUE "]) +def test_supports_live_progress_disabled_in_automation_env( + monkeypatch: pytest.MonkeyPatch, + env_name: str, + env_value: str, +) -> None: + monkeypatch.delenv("CI", raising=False) + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.setenv(env_name, env_value) + assert _supports_live_progress(_FakeTty()) is False + + +def test_supports_live_progress_enabled_for_tty_outside_ci( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("CI", raising=False) + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + assert _supports_live_progress(_FakeTty()) is True + + +def test_export_progress_reporter_logs_instead_of_live_updates_in_ci( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + monkeypatch.setenv("CI", "true") + caplog.set_level(logging.INFO) + reporter = _ExportProgressReporter( + logger=logging.getLogger("test_resource_json_progress"), + total_resources=10, + stream=_FakeTty(), + ) + + assert reporter.live_updates is False + reporter.update(1, "texture.tpc") + assert caplog.records + assert "texture.tpc" in caplog.records[0].message diff --git a/Libraries/PyKotor/tests/diff_tool/test_cli_utils.py b/Libraries/PyKotor/tests/diff_tool/test_cli_utils.py new file mode 100644 index 000000000..c65e2c635 --- /dev/null +++ b/Libraries/PyKotor/tests/diff_tool/test_cli_utils.py @@ -0,0 +1,89 @@ +"""Regression tests for diff_tool CLI path helpers.""" + +from __future__ import annotations + +import pathlib +import sys +import tempfile +import unittest +from pathlib import Path + +THIS_SCRIPT_PATH = pathlib.Path(__file__).resolve() +PYKOTOR_PATH = THIS_SCRIPT_PATH.parents[2].joinpath("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) + +from pykotor.diff_tool.cli_utils import ( # noqa: E402 # pyright: ignore[reportMissingImports] + is_kotor_install_dir, + normalize_path_arg, +) +from pykotor.tools.patching import is_kotor_install_dir as patching_is_kotor_install_dir # noqa: E402 +from pykotor.tslpatcher.diff.engine import ( # noqa: E402 + is_kotor_install_dir as engine_is_kotor_install_dir, +) + + +class TestNormalizePathArg(unittest.TestCase): + def test_none_and_blank_inputs(self) -> None: + self.assertIsNone(normalize_path_arg(None)) + self.assertIsNone(normalize_path_arg("")) + self.assertIsNone(normalize_path_arg(" ")) + + def test_strips_surrounding_quotes(self) -> None: + self.assertEqual(normalize_path_arg('"C:\\KOTOR"'), "C:\\KOTOR") + self.assertEqual(normalize_path_arg("'/opt/kotor'"), "/opt/kotor") + + def test_fixes_powershell_quote_escape_mangling(self) -> None: + mangled = 'C:\\Program Files\\KOTOR" C:\\other\\path' + self.assertEqual(normalize_path_arg(mangled), "C:\\Program Files\\KOTOR") + + def test_strips_trailing_slashes_and_embedded_quotes(self) -> None: + self.assertEqual(normalize_path_arg('C:\\path\\'), "C:\\path") + self.assertEqual(normalize_path_arg('C:\\path"'), "C:\\path") + + +class TestIsKotorInstallDir(unittest.TestCase): + def test_detects_install_with_exact_chitin_key_name(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "Kotor" + root.mkdir() + (root / "chitin.key").write_bytes(b"") + + self.assertTrue(is_kotor_install_dir(root)) + self.assertTrue(patching_is_kotor_install_dir(root)) + self.assertTrue(engine_is_kotor_install_dir(root)) + + def test_rejects_directory_without_chitin_key(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + + self.assertFalse(is_kotor_install_dir(root)) + self.assertFalse(patching_is_kotor_install_dir(root)) + self.assertFalse(engine_is_kotor_install_dir(root)) + + @unittest.skipIf( + sys.platform == "win32", + "Case-mismatch path semantics differ on Windows filesystems.", + ) + def test_detects_install_with_case_mismatched_paths(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "GameDir" + root.mkdir() + (root / "CHITIN.KEY").write_bytes(b"") + + mismatched = root.parent / "gamedir" + self.assertTrue(is_kotor_install_dir(mismatched)) + self.assertTrue(patching_is_kotor_install_dir(mismatched)) + self.assertTrue(engine_is_kotor_install_dir(mismatched)) + + +if __name__ == "__main__": + unittest.main()