diff --git a/scripts/verify_python_distribution.py b/scripts/verify_python_distribution.py index b2b1d0512..caa89d339 100644 --- a/scripts/verify_python_distribution.py +++ b/scripts/verify_python_distribution.py @@ -8,6 +8,7 @@ import os import re import subprocess +import sys import tarfile import tempfile import venv @@ -22,6 +23,20 @@ from packaging.version import InvalidVersion, Version REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + +# ``python scripts/verify_python_distribution.py`` puts ``scripts/`` on +# ``sys.path``, so the repository root is added explicitly before importing the +# shared subprocess policy. Duplicating that policy here is how the verifier and +# the runtime it checks would drift apart. +if str(REPOSITORY_ROOT) not in sys.path: + sys.path.insert(0, str(REPOSITORY_ROOT)) + +from core.platform_compat import ( + configure_utf8_stdio, + subprocess_env, + subprocess_text_kwargs, +) + PACKAGE_NAME = "deepcode-hku" MINIMUM_MCP_VERSION = Version("1.29") UNSUPPORTED_MCP_VERSION = Version("2") @@ -294,11 +309,12 @@ def _run( stdin=None, timeout: int = 180, ) -> None: - environment = { - **os.environ, - "PIP_DISABLE_PIP_VERSION_CHECK": "1", - "PYTHONNOUSERSITE": "1", - } + environment = subprocess_env( + { + "PIP_DISABLE_PIP_VERSION_CHECK": "1", + "PYTHONNOUSERSITE": "1", + } + ) try: subprocess.run( command, @@ -307,7 +323,7 @@ def _run( stdin=stdin, check=True, capture_output=True, - text=True, + **subprocess_text_kwargs(), timeout=timeout, ) except subprocess.CalledProcessError as exc: @@ -381,6 +397,7 @@ def smoke_installed_wheel(wheel: Path) -> None: def main(argv: list[str] | None = None) -> int: + configure_utf8_stdio() parser = argparse.ArgumentParser( description="Verify DeepCode Python artifacts and their installed runtime." ) diff --git a/tests/test_python_distribution_release.py b/tests/test_python_distribution_release.py index c5845de15..9dc452ded 100644 --- a/tests/test_python_distribution_release.py +++ b/tests/test_python_distribution_release.py @@ -4,6 +4,7 @@ import importlib.util import json +import subprocess import sys from pathlib import Path @@ -111,3 +112,50 @@ def test_packaged_web_manifest_requires_its_entry_assets(): del files["web/assets/app.js"] with pytest.raises(release.DistributionVerificationError, match="resource missing"): release.verify_web_assets(files.__getitem__, list(files), "web/", "2.2.0") + + +def test_smoke_failure_reports_output_that_the_reader_cannot_decode(tmp_path): + """A rejected byte used to wipe out the whole diagnostic. + + Reading without ``encoding`` decoded the child with the locale codec, so a + rejected byte raised inside subprocess's reader thread; the thread died, the + process result stayed clean, and ``_run`` reported the command with no + output at all. ``0x91`` is invalid UTF-8 and cp936, so the byte is rejected + on every runner. + """ + + code = ( + "import sys; " + "sys.stdout.buffer.write(b'partial \\x91 output\\n'); " + "sys.stdout.buffer.flush(); " + "sys.exit(3)" + ) + with pytest.raises(release.DistributionVerificationError) as failure: + release._run([sys.executable, "-c", code], cwd=tmp_path) + + message = str(failure.value) + assert "partial" in message + assert "output" in message + assert "\ufffd" in message + + +def test_smoke_children_are_told_to_write_utf8(monkeypatch, tmp_path): + """The read side declares UTF-8, so the child has to speak it too. + + Otherwise CJK output from pip or the CLI degrades into replacement + characters that the release report cannot be read from. + """ + + observed = {} + + def fake_run(command, **kwargs): + observed.update(kwargs) + raise subprocess.CalledProcessError(1, command, output="out", stderr="err") + + monkeypatch.setattr(release.subprocess, "run", fake_run) + with pytest.raises(release.DistributionVerificationError, match="boom"): + release._run(["boom"], cwd=tmp_path) + + assert observed["encoding"] == "utf-8" + assert observed["errors"] == "replace" + assert observed["env"]["PYTHONIOENCODING"] == "utf-8"