diff --git a/docs/changelog/3530.bugfix.rst b/docs/changelog/3530.bugfix.rst new file mode 100644 index 000000000..676d09410 --- /dev/null +++ b/docs/changelog/3530.bugfix.rst @@ -0,0 +1,3 @@ +Prevent tox from hanging upon exit due to orphaned build threads and +subprocesses when the ``--installpkg`` option is used with *sdist*. +- by :user:`vytas7` diff --git a/pyproject.toml b/pyproject.toml index 9cd667865..5689a5187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ test = [ "flaky>=3.8.1", "hatch-vcs>=0.4", "hatchling>=1.27", + "pdm-backend", "psutil>=6.1.1", "pytest>=8.3.4", "pytest-cov>=5", diff --git a/src/tox/tox_env/python/virtual_env/package/pyproject.py b/src/tox/tox_env/python/virtual_env/package/pyproject.py index 4b4be011f..b1a287f84 100644 --- a/src/tox/tox_env/python/virtual_env/package/pyproject.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -118,8 +118,15 @@ def root(self) -> Path: @root.setter def root(self, value: Path) -> None: - self._root = value - self._frontend_ = None # force recreating the frontend with new root + # NOTE(vytas): Recreating the frontend with a new root will orphan the + # current frontend.backend_executor, if any, making tox hang upon + # exit waiting for its threads and subprocesses (#3512). + # + # Here, we partially work around the issue by only resetting the root + # when it has actually changed: + if self._root != value: + self._root = value + self._frontend_ = None # force recreating the frontend with new root @staticmethod def id() -> str: diff --git a/tests/tox_env/python/virtual_env/package/conftest.py b/tests/tox_env/python/virtual_env/package/conftest.py index b34d44c81..1f19e6fa5 100644 --- a/tests/tox_env/python/virtual_env/package/conftest.py +++ b/tests/tox_env/python/virtual_env/package/conftest.py @@ -2,7 +2,7 @@ import sys from textwrap import dedent -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable import pytest @@ -40,3 +40,41 @@ def pkg_with_extras_project(tmp_path_factory: pytest.TempPathFactory) -> Path: toml = '[build-system]\nrequires=["setuptools", "wheel"]\nbuild-backend = "setuptools.build_meta"' (tmp_path / "pyproject.toml").write_text(toml) return tmp_path + + +@pytest.fixture(scope="session") +def pkg_with_pdm_backend( + tmp_path_factory: pytest.TempPathFactory, + pkg_builder: Callable[[Path, Path, list[str], bool], Path], +) -> Path: + tmp_path = tmp_path_factory.mktemp("skeleton") + + pyproject_toml = """ + [build-system] + requires = ["pdm-backend"] + build-backend = "pdm.backend" + + [project] + name = "skeleton" + description = "Just a skeleton for reproducing #3512." + version = "0.1.1337" + dependencies = [ + "requests", + ] + + [tool.pdm.build] + includes = [ + "skeleton/", + ] + source-includes = [ + "tox.ini", + ] + """ + (tmp_path / "pyproject.toml").write_text(dedent(pyproject_toml)) + (tmp_path / "skeleton").mkdir(exist_ok=True) + (tmp_path / "skeleton" / "__init__.py").touch() + + dist = tmp_path / "dist" + pkg_builder(dist, tmp_path, ["sdist"], False) + + return tmp_path diff --git a/tests/tox_env/python/virtual_env/package/test_package_pyproject.py b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py index 0ae8d2c3b..852a61b92 100644 --- a/tests/tox_env/python/virtual_env/package/test_package_pyproject.py +++ b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py @@ -458,3 +458,27 @@ def test_pyproject_config_settings_editable_legacy( "get_requires_for_build_wheel": {"C": "3"}, "prepare_metadata_for_build_wheel": {"D": "4"}, } + + +@pytest.mark.usefixtures("enable_pip_pypi_access") +def test_aaa_pyproject_installpkg_pep517_envs( + tox_project: ToxProjectCreator, + pkg_with_pdm_backend: Path, +) -> None: + # Regression test for #3512 + tox_ini = """ + [tox] + envlist = dummy1,dummy2 + + [testenv:dummy1] + commands = + python -c print(1) + + [testenv:dummy2] + commands = + python -c print(42) + """ + sdist = pkg_with_pdm_backend / "dist" / "skeleton-0.1.1337.tar.gz" + proj = tox_project({"tox.ini": tox_ini}, base=pkg_with_pdm_backend) + result = proj.run("--installpkg", str(sdist), "--exit-and-dump-after", "10") + result.assert_success()