Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 8 additions & 4 deletions testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,13 @@ def run_pyenv(
)


def ensure_python_distribution(version):
# type: (str) -> PyenvPythonDistribution
if version not in ALL_PY_VERSIONS:
def ensure_python_distribution(
version, # type: str
python_version=None, # type: Optional[str]
allow_adhoc_version=False, # type: bool
):
# type: (...) -> PyenvPythonDistribution
if not allow_adhoc_version and version not in ALL_PY_VERSIONS:
raise ValueError("Please constrain version to one of {}".format(ALL_PY_VERSIONS))

if WINDOWS and _ALL_PY_VERSIONS_TO_VERSION_INFO[version][:2] < (3, 8):
Expand All @@ -669,7 +673,7 @@ def ensure_python_distribution(version):
if WINDOWS:
python = os.path.join(interpreter_location, "python.exe")
else:
major, minor = version.split(".")[:2]
major, minor = (python_version or version).split(".")[:2]
python = os.path.join(
interpreter_location, "bin", "python{major}.{minor}".format(major=major, minor=minor)
)
Expand Down
135 changes: 135 additions & 0 deletions tests/integration/test_issue_3113.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright 2026 Pex project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import

import glob
import os.path
import shutil
import subprocess
from typing import Tuple

import pytest

from pex.common import safe_delete
from pex.util import CacheHelper
from testing import (
IS_LINUX_X86_64,
PyenvPythonDistribution,
ensure_python_distribution,
make_env,
run_pex_command,
)
from testing.pytest_utils.tmp import Tempdir


@pytest.fixture
def cowsay(tmpdir):
# type: (Tempdir) -> str

pex_root = tmpdir.join("pex-root")
cowsay = tmpdir.join("cowsay.pex")
run_pex_command(
args=[
"--pex-root",
pex_root,
"--runtime-pex-root",
pex_root,
"cowsay<6",
"-c",
"cowsay",
"-o",
cowsay,
]
).assert_success()
return cowsay


def pypy_dist(
pyenv_version, # type: str
python_version, # type: Tuple[int, int, int]
):
# type: (...) -> PyenvPythonDistribution
pypy_distribution = ensure_python_distribution(
pyenv_version,
python_version="{major}.{minor}".format(major=python_version[0], minor=python_version[1]),
allow_adhoc_version=True,
)
assert (
"{major}.{minor}.{patch}".format(
major=python_version[0], minor=python_version[1], patch=python_version[2]
)
== subprocess.check_output(
args=[
pypy_distribution.binary,
"-c",
"import sys; print('.'.join(map(str, sys.version_info[:3])))",
]
)
.decode("utf-8")
.strip()
)
return pypy_distribution


@pytest.fixture
def pypy3_11_11_dist():
# type: () -> PyenvPythonDistribution
return pypy_dist("pypy3.11-7.3.19", python_version=(3, 11, 11))


@pytest.fixture
def pypy3_11_13_dist():
# type: () -> PyenvPythonDistribution
return pypy_dist("pypy3.11-7.3.20", python_version=(3, 11, 13))


@pytest.mark.skipif(
not IS_LINUX_X86_64,
reason=(
"We only need to test this for one known-good pair of interpreters with matching binary "
"hash and differing patch versions. The pypy3.11-7.3.19 / pypy3.11-7.3.20 pair is known to "
"meet this criteria for x86_64 Linux."
),
)
def test_interpreter_upgrade_same_binary_hash(
tmpdir, # type: Tempdir
cowsay, # type: str
pypy3_11_11_dist, # type: PyenvPythonDistribution
pypy3_11_13_dist, # type: PyenvPythonDistribution
):
# type: (...) -> None

assert pypy3_11_11_dist.binary != pypy3_11_13_dist.binary
assert CacheHelper.hash(pypy3_11_11_dist.binary) == CacheHelper.hash(pypy3_11_13_dist.binary)

pypy_311_prefix = tmpdir.join("opt", "pypy")

def install_pypy_311_and_create_venv(
pypy_distribution, # type: PyenvPythonDistribution
venv_dir, # type: str
):
# type: (...) -> str

if os.path.exists(pypy_311_prefix):
shutil.move(pypy_311_prefix, pypy_311_prefix + ".sav")
shutil.copytree(pypy_distribution.interpreter.prefix, pypy_311_prefix)
for binary in glob.glob(os.path.join(pypy_311_prefix, "bin", "*")):
if os.path.basename(binary) not in ("libpypy3.11-c.so", "pypy3.11"):
safe_delete(binary)
pypy_binary = os.path.join(pypy_311_prefix, "bin", "pypy3.11")
venv_path = tmpdir.join(venv_dir)
subprocess.check_call(args=[pypy_binary, "-m", "venv", venv_path])
return os.path.join(venv_path, "bin", "python")

pypy3_11_11_venv_binary = install_pypy_311_and_create_venv(pypy3_11_11_dist, "pypy3_11_11.venv")
assert b"| I am 3.11.11 |" in subprocess.check_output(
args=[pypy3_11_11_venv_binary, cowsay, "I am 3.11.11"],
env=make_env(PEX_PYTHON_PATH=pypy3_11_11_venv_binary),
)

pypy3_11_13_venv_binary = install_pypy_311_and_create_venv(pypy3_11_13_dist, "pypy3_11_13.venv")
assert b"| I am 3.11.13 |" in subprocess.check_output(
args=[pypy3_11_13_venv_binary, cowsay, "I am 3.11.13"],
env=make_env(PEX_PYTHON_PATH=pypy3_11_13_venv_binary),
)
Comment on lines +132 to +135
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior to the fix this fails with:

:; uvrc test-integration -- --devpi -vvsk test_interpreter_upgrade_same_binary_hash -n0
...
tests/integration/test_issue_3113.py::test_interpreter_upgrade_same_binary_hash Traceback (most recent call last):
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/opt/pypy/lib/pypy3.11/runpy.py", line 201, in _run_module_as_main
    return _run_code(code, main_globals, N
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/opt/pypy/lib/pypy3.11/runpy.py", line 88, in _run_code
    exec(code, run_globals)
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pex-root/unzipped_pexes/3/8763eebecff32b475e219b8c4c1c2abc2c2a1be3/__main__.py", line 242, in <module>
    result, should_exit, is_globals = 
                                      ^^^^^
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pex-root/unzipped_pexes/3/8763eebecff32b475e219b8c4c1c2abc2c2a1be3/__main__.py", line 234, in boot
    result = 
             ^^^^^^^^^^^^^^
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pex-root/unzipped_pexes/3/8763eebecff32b475e219b8c4c1c2abc2c2a1be3/.bootstrap/pex/pex_bootstrapper.py", line 695, in bootstrap_pex
    maybe_reexec_pex(
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pex-root/unzipped_pexes/3/8763eebecff32b475e219b8c4c1c2abc2c2a1be3/.bootstrap/pex/pex_bootstrapper.py", line 420, in maybe_reexec_pex
    resolved = target.resolve_base_interpreter()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pex-root/unzipped_pexes/3/8763eebecff32b475e219b8c4c1c2abc2c2a1be3/.bootstrap/pex/interpreter.py", line 1530, in resolve_base_interpreter
    raise self.BaseInterpreterResolutionError("\n".join(message))
pex.interpreter.PythonInterpreter.BaseInterpreterResolutionError: Failed to resolve the base interpreter for the virtual environment at /tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pypy3_11_13.venv.
Search of base_prefix /tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/opt/pypy found no equivalent interpreter for /tmp/pytest-of-jsirois/test_interpreter_upgrade-0e5350/pypy3_11_13.venv/bin/pypy3.11
FAILED

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cburroughs this test was pretty titchy to get right (both a single re-used install path is required as well as a limited interpreter search path), but it faithfully reproduces your original test case (besides using pre-compiled PyPy interpreters) AFAICT.