diff --git a/src/koru/queue/ticket.py b/src/koru/queue/ticket.py index 03a44eb7..9a5b7319 100644 --- a/src/koru/queue/ticket.py +++ b/src/koru/queue/ticket.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import Any +from koru.queue.runners import _decode_subprocess_output from koru.queue.types import CommandResult @@ -207,7 +208,7 @@ def _python_has_planfile_cli(python: str) -> bool: ), ], capture_output=True, - text=True, + text=False, timeout=5, check=False, ) @@ -247,12 +248,15 @@ def _planfile_supports_structured_queue_json(executable: str) -> bool: [executable, "--version"], check=False, capture_output=True, - text=True, + text=False, timeout=2, ) except (OSError, subprocess.SubprocessError): return True - version = _parse_version_tuple(f"{result.stdout}\n{result.stderr}") + version = _parse_version_tuple( + f"{_decode_subprocess_output(result.stdout)}\n" + f"{_decode_subprocess_output(result.stderr)}" + ) if version is None: return True return version >= _MIN_STRUCTURED_QUEUE_PLANFILE_VERSION @@ -330,7 +334,7 @@ def _configured_planfile_cmd_usable(configured: str) -> bool: proc = subprocess.run( [*parts, "--version"], capture_output=True, - text=True, + text=False, timeout=5, check=False, ) @@ -338,7 +342,10 @@ def _configured_planfile_cmd_usable(configured: str) -> bool: return True if proc.returncode == 0: return True - text = f"{proc.stdout}\n{proc.stderr}".lower() + text = ( + f"{_decode_subprocess_output(proc.stdout)}\n" + f"{_decode_subprocess_output(proc.stderr)}" + ).lower() return not any(marker in text for marker in _PLANFILE_MODULE_MISSING_MARKERS) diff --git a/tests/test_planfile_queue.py b/tests/test_planfile_queue.py index 089d2b1e..22f5df74 100644 --- a/tests/test_planfile_queue.py +++ b/tests/test_planfile_queue.py @@ -13,7 +13,12 @@ from koru.cqrs.event_store import JsonlEventStore from koru.queue import run_next_planfile_task -from koru.queue.ticket import parse_next_ticket, planfile_command +from koru.queue.ticket import ( + _configured_planfile_cmd_usable, + _planfile_supports_structured_queue_json, + parse_next_ticket, + planfile_command, +) from tests import _repolab @@ -27,6 +32,52 @@ def _ticket_args(command: list[str]) -> list[str]: class TestPlanfileCommand(unittest.TestCase): + def test_structured_queue_probe_handles_non_utf8_bytes(self) -> None: + _planfile_supports_structured_queue_json.cache_clear() + with patch( + "koru.queue.ticket.subprocess.run", + return_value=SimpleNamespace( + returncode=0, + stdout=b"planfile wersja \xf3 0.1.101", + stderr=b"", + ), + ) as run: + self.assertTrue(_planfile_supports_structured_queue_json("/tmp/planfile")) + self.assertFalse(run.call_args.kwargs["text"]) + _planfile_supports_structured_queue_json.cache_clear() + with patch( + "koru.queue.ticket.subprocess.run", + return_value=SimpleNamespace( + returncode=0, + stdout=b"planfile wersja \xf3 0.1.99", + stderr=b"", + ), + ): + self.assertFalse(_planfile_supports_structured_queue_json("/tmp/planfile-old")) + + def test_configured_command_probe_handles_non_utf8_module_missing_marker(self) -> None: + _configured_planfile_cmd_usable.cache_clear() + with patch( + "koru.queue.ticket.subprocess.run", + return_value=SimpleNamespace( + returncode=1, + stdout=b"", + stderr=b"\xf3 no module named 'planfile.cli'", + ), + ) as run: + self.assertFalse(_configured_planfile_cmd_usable("/tmp/venv/bin/python -m planfile.cli")) + self.assertFalse(run.call_args.kwargs["text"]) + _configured_planfile_cmd_usable.cache_clear() + with patch( + "koru.queue.ticket.subprocess.run", + return_value=SimpleNamespace( + returncode=0, + stdout=b"\xf3 planfile 0.1.101", + stderr=b"", + ), + ): + self.assertTrue(_configured_planfile_cmd_usable("/tmp/venv/bin/python -m planfile.cli")) + def test_prefers_local_planfile_before_importable_module_from_active_env(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir)