Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Libraries/PyKotor/tests/cli/test_json_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
from pykotor.resource.formats.tpc import TPC, TPCTextureFormat, bytes_tpc, read_tpc
from pykotor.resource.type import ResourceType
from pykotor.tools.resource_json import (
_ExportProgressReporter,
_serialize_mdl_face,
_supports_live_progress,
export_installation_to_json_tree,
iter_installation_resource_documents,
serialize_file_resource_document,
Expand Down Expand Up @@ -328,6 +330,90 @@ def test_export_installation_to_json_tree_logs_percentage_progress(
)


def test_supports_live_progress_false_when_ci_env_truthy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Automation must not rely on carriage-return stderr updates; log milestones instead."""
monkeypatch.setenv("CI", "true")

class FakeTTY:
def isatty(self) -> bool:
return True

assert _supports_live_progress(FakeTTY()) is False


def test_supports_live_progress_false_when_github_actions_truthy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("CI", raising=False)
monkeypatch.setenv("GITHUB_ACTIONS", "true")

class FakeTTY:
def isatty(self) -> bool:
return True

assert _supports_live_progress(FakeTTY()) is False


def test_export_progress_reporter_logs_to_logger_when_ci_disables_live_updates(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.setenv("CI", "true")

class FakeTTY:
def isatty(self) -> bool:
return True

def write(self, _s: str) -> None:
raise AssertionError("stderr live progress must not be used when CI is set")

def flush(self) -> None:
raise AssertionError("stderr live progress must not be used when CI is set")

log = logging.getLogger("pykotor.tests.resource_json_export_progress")
log.setLevel(logging.INFO)
with caplog.at_level(logging.INFO, logger=log.name):
progress = _ExportProgressReporter(log, total_resources=4, stream=FakeTTY())
assert progress.live_updates is False
progress.update(1, "dialog.tlk")
progress.finish()
messages = [record.getMessage() for record in caplog.records if record.name == log.name]
assert any("25.00% Writing dialog.tlk" in m for m in messages)


def test_export_progress_reporter_writes_carriage_returns_when_not_ci_and_stream_is_tty(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.delenv("CI", raising=False)
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)

writes: list[str] = []

class FakeTTY:
def isatty(self) -> bool:
return True

def write(self, s: str) -> None:
writes.append(s)

def flush(self) -> None:
pass

log = logging.getLogger("pykotor.tests.resource_json_export_progress_tty")
log.setLevel(logging.INFO)
with caplog.at_level(logging.INFO, logger=log.name):
progress = _ExportProgressReporter(log, total_resources=4, stream=FakeTTY())
assert progress.live_updates is True
progress.update(1, "dialog.tlk")
progress.finish()
assert any(w.startswith("\r") for w in writes)
assert not [r for r in caplog.records if r.name == log.name]
assert writes[-1].startswith("\r")


def test_iter_installation_resource_documents_reuses_installation_export_shape(
tmp_path: Path,
) -> None:
Expand Down
16 changes: 15 additions & 1 deletion Libraries/PyKotor/tests/common/test_case_aware_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def add_sys_path(p: pathlib.Path):

from typing import TYPE_CHECKING

from pykotor.tools.path import CaseAwarePath # noqa: E402 # pyright: ignore[reportMissingImports]
from pykotor.tools.path import CaseAwarePath, clear_cache # noqa: E402 # pyright: ignore[reportMissingImports]

if TYPE_CHECKING:
from collections.abc import Generator
Expand Down Expand Up @@ -137,6 +137,20 @@ def test_fix_path_formatting(self):
assert CaseAwarePath.str_norm("/path//to/dir/", slash="\\") == "\\path\\to\\dir"
assert CaseAwarePath.str_norm("/path//to/dir/", slash="/") == "/path/to/dir"

@unittest.skipIf(sys.platform == "win32", "POSIX-specific case resolution behavior")
def test_clear_cache_refreshes_case_insensitive_directory_lookup(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = pathlib.Path(tmpdir) / "GameRoot"
root.mkdir()
(root / "dialog.tlk").write_bytes(b"tlk")
wrong_case = CaseAwarePath(root.parent / "gameroot" / "DIALOG.TLK")
assert wrong_case.name == "dialog.tlk"
(root / "dialog.tlk").unlink()
(root / "streamsounds").mkdir()
clear_cache()
nested = CaseAwarePath(root.parent / "GameRoot" / "StreamSounds")
assert nested.is_dir()

@unittest.skipIf(sys.platform == "win32", "POSIX-specific case resolution behavior")
def test_virtual_archive_segment_does_not_scan_file_path(self):
with tempfile.TemporaryDirectory() as tmpdir:
Expand Down
Loading