diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 79a3a2561..1820f90de 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -44,6 +44,6 @@ jobs: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: attestations: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0cc0e049..f6630b01a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: validate-pyproject - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.12.10" + rev: "v0.12.11" hooks: - id: ruff-check args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] diff --git a/docs/changelog.rst b/docs/changelog.rst index 9c6798176..952f15fb7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,43 @@ Release History .. towncrier release notes start +v4.30.2 (2025-09-04) +-------------------- + +Bugfixes - 4.30.2 +~~~~~~~~~~~~~~~~~ +- Previously, when tox ran in an automatically provisioned environment, it could hang waiting for a PEP 517 build backend + if used in conjunction with the ``--installpkg`` option. This has been fixed by properly tearing down the automatically + provisioned environment after the tests. + - by :user:`vytas7` (:issue:`3600`) + +v4.30.1 (2025-09-03) +-------------------- + +Bugfixes - 4.30.1 +~~~~~~~~~~~~~~~~~ +- Prevent tox from hanging upon exit due to orphaned build threads and subprocesses when the ``--installpkg`` option is + used with *sdist*. + - by :user:`vytas7` (:issue:`3530`) + +v4.30.0 (2025-09-03) +-------------------- + +Features - 4.30.0 +~~~~~~~~~~~~~~~~~ +- Add ``__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI``, which passes through the ``CI`` variable if present. This is intended for use by other libraries to detect if tox is running under CI. (:issue:`3442`) + +Bugfixes - 4.30.0 +~~~~~~~~~~~~~~~~~ +- Makes the error message more clear when pyproject.toml file cannot be loaded + or is missing expected keys. (:issue:`3578`) +- The :func:`tox_extend_envs() hook ` + recently added in :pull:`3591` turned out to not work well with + ``tox run``. It was fixed internally, not to exhaust the underlying + iterator on the first use. + + -- by :user:`webknjaz` (:issue:`3598`) + v4.29.0 (2025-08-29) -------------------- diff --git a/docs/config.rst b/docs/config.rst index 19e702d5d..b50c14e22 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -561,6 +561,8 @@ Base options - ✅ - ❌ + If the environment variable ``CI`` is present, ``__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI`` will be set to the value of ``CI``. The ``CI`` variable itself will not be passed through. + More environment variable-related information can be found in :ref:`environment variable substitutions`. .. conf:: diff --git a/pyproject.toml b/pyproject.toml index ca4c72038..a7323bf83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ test = [ "flaky>=3.8.1", "hatch-vcs>=0.5", "hatchling>=1.27", + "pdm-backend", "psutil>=7", "pytest>=8.4.1", "pytest-cov>=6.2.1", diff --git a/src/tox/config/main.py b/src/tox/config/main.py index ff264ea21..9fbd9f4bc 100644 --- a/src/tox/config/main.py +++ b/src/tox/config/main.py @@ -2,7 +2,7 @@ import os from collections import OrderedDict, defaultdict -from itertools import chain +from itertools import chain, tee from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Iterator, Sequence, TypeVar @@ -81,7 +81,11 @@ def src_path(self) -> Path: def __iter__(self) -> Iterator[str]: """:return: an iterator that goes through existing environments""" - return chain(self._src.envs(self.core), self._extra_envs) + # NOTE: `tee(self._extra_envs)[1]` is necessary for compatibility with + # NOTE: Python 3.11 and older versions. Once Python 3.12 is the lowest + # NOTE: supported version, it can be changed to + # NOTE: `chain.from_iterable(tee(self._extra_envs, 1))`. + return chain(self._src.envs(self.core), tee(self._extra_envs)[1]) def sections(self) -> Iterator[Section]: yield from self._src.sections() diff --git a/src/tox/config/source/discover.py b/src/tox/config/source/discover.py index f04b9a7a0..988c4976d 100644 --- a/src/tox/config/source/discover.py +++ b/src/tox/config/source/discover.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from tox.config.types import MissingRequiredConfigKeyError from tox.report import HandledError from .legacy_toml import LegacyToml @@ -59,21 +60,32 @@ def _locate_source() -> Source | None: for base in chain([folder], folder.parents): for src_type in SOURCE_TYPES: candidate: Path = base / src_type.FILENAME - try: - return src_type(candidate) - except ValueError: - pass + if candidate.exists(): + try: + return src_type(candidate) + except MissingRequiredConfigKeyError as exc: + msg = f"{src_type.__name__} skipped loading {candidate.resolve()} due to {exc}" + logging.info(msg) + except ValueError as exc: + msg = f"{src_type.__name__} failed loading {candidate.resolve()} due to {exc}" + raise HandledError(msg) from exc return None def _load_exact_source(config_file: Path) -> Source: # if the filename matches to the letter some config file name do not fallback to other source types + if not config_file.exists(): + msg = f"config file {config_file} does not exist" + raise HandledError(msg) exact_match = [s for s in SOURCE_TYPES if config_file.name == s.FILENAME] # pragma: no cover for src_type in exact_match or SOURCE_TYPES: # pragma: no branch try: return src_type(config_file) - except ValueError: # noqa: PERF203 + except MissingRequiredConfigKeyError: # noqa: PERF203 pass + except ValueError as exc: + msg = f"{src_type.__name__} failed loading {config_file.resolve()} due to {exc}" + raise HandledError(msg) from exc msg = f"could not recognize config file {config_file}" raise HandledError(msg) @@ -88,7 +100,7 @@ def _create_default_source(root_dir: Path | None) -> Source: else: # if not set use where we find pyproject.toml in the tree or cwd empty = root_dir names = " or ".join({i.FILENAME: None for i in SOURCE_TYPES}) - logging.warning("No %s found, assuming empty tox.ini at %s", names, empty) + logging.warning("No loadable %s found, assuming empty tox.ini at %s", names, empty) return ToxIni(empty / "tox.ini", content="") diff --git a/src/tox/config/source/legacy_toml.py b/src/tox/config/source/legacy_toml.py index db6227cdd..6de39f11f 100644 --- a/src/tox/config/source/legacy_toml.py +++ b/src/tox/config/source/legacy_toml.py @@ -2,6 +2,8 @@ import sys +from tox.config.types import MissingRequiredConfigKeyError + if sys.version_info >= (3, 11): # pragma: no cover (py311+) import tomllib else: # pragma: no cover (py311+) @@ -27,7 +29,8 @@ def __init__(self, path: Path) -> None: try: content = toml_content["tool"]["tox"]["legacy_tox_ini"] except KeyError as exc: - raise ValueError(path) from exc + msg = f"`tool.tox.legacy_tox_ini` missing from {path}" + raise MissingRequiredConfigKeyError(msg) from exc super().__init__(path, content=content) diff --git a/src/tox/config/source/setup_cfg.py b/src/tox/config/source/setup_cfg.py index d1d316eac..ffb994afd 100644 --- a/src/tox/config/source/setup_cfg.py +++ b/src/tox/config/source/setup_cfg.py @@ -18,7 +18,8 @@ class SetupCfg(IniSource): def __init__(self, path: Path) -> None: super().__init__(path) if not self._parser.has_section(self.CORE_SECTION.key): - raise ValueError + msg = f"section {self.CORE_SECTION.key} not found" + raise ValueError(msg) __all__ = ("SetupCfg",) diff --git a/src/tox/config/source/toml_pyproject.py b/src/tox/config/source/toml_pyproject.py index 4c03e41ac..b70b1404b 100644 --- a/src/tox/config/source/toml_pyproject.py +++ b/src/tox/config/source/toml_pyproject.py @@ -7,6 +7,7 @@ from tox.config.loader.section import Section from tox.config.loader.toml import TomlLoader +from tox.config.types import MissingRequiredConfigKeyError from tox.report import HandledError from .api import Source @@ -81,7 +82,7 @@ def __init__(self, path: Path) -> None: our_content = our_content[key] self._our_content = our_content except KeyError as exc: - raise ValueError(path) from exc + raise MissingRequiredConfigKeyError(path) from exc super().__init__(path) def get_core_section(self) -> Section: diff --git a/src/tox/config/types.py b/src/tox/config/types.py index 42c2c4814..0e9070ad4 100644 --- a/src/tox/config/types.py +++ b/src/tox/config/types.py @@ -10,6 +10,13 @@ class CircularChainError(ValueError): """circular chain in config""" +class MissingRequiredConfigKeyError(ValueError): + """missing required config key + + Used by the two toml loaders in order to identify if config keys are present. + """ + + class Command: # noqa: PLW1641 """A command to execute.""" diff --git a/src/tox/plugin/spec.py b/src/tox/plugin/spec.py index ba3eae96b..e69f206b5 100644 --- a/src/tox/plugin/spec.py +++ b/src/tox/plugin/spec.py @@ -33,6 +33,8 @@ def tox_register_tox_env(register: ToxEnvRegister) -> None: def tox_extend_envs() -> Iterable[str]: """Declare additional environment names. + .. versionadded:: 4.29.0 + This hook is called without any arguments early in the lifecycle. It is expected to return an iterable of strings with environment names for tox to consider. It can be used to facilitate dynamic creation of diff --git a/src/tox/provision.py b/src/tox/provision.py index c989c39dd..86fedf912 100644 --- a/src/tox/provision.py +++ b/src/tox/provision.py @@ -146,10 +146,14 @@ def run_provision(name: str, state: State) -> int: logging.info("will run in a automatically provisioned python environment under %s", env_python) try: tox_env.setup() + args: list[str] = [str(env_python), "-m", "tox"] + args.extend(state.args) + outcome = tox_env.execute( + cmd=args, stdin=StdinSource.user_only(), show=True, run_id="provision", cwd=Path.cwd() + ) + return cast("int", outcome.exit_code) except Skip as exception: msg = f"cannot provision tox environment {tox_env.conf['env_name']} because {exception}" raise HandledError(msg) from exception - args: list[str] = [str(env_python), "-m", "tox"] - args.extend(state.args) - outcome = tox_env.execute(cmd=args, stdin=StdinSource.user_only(), show=True, run_id="provision", cwd=Path.cwd()) - return cast("int", outcome.exit_code) + finally: + tox_env.teardown() diff --git a/src/tox/session/state.py b/src/tox/session/state.py index 6b63f39da..3f59ecd73 100644 --- a/src/tox/session/state.py +++ b/src/tox/session/state.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from itertools import chain +from itertools import chain, tee from typing import TYPE_CHECKING, Sequence from tox.config.main import Config @@ -20,7 +20,7 @@ class State: """Runtime state holder.""" def __init__(self, options: Options, args: Sequence[str]) -> None: - extended_envs = chain.from_iterable(MANAGER.tox_extend_envs()) + (extended_envs,) = tee(chain.from_iterable(MANAGER.tox_extend_envs()), 1) self.conf = Config.make(options.parsed, options.pos_args, options.source, extended_envs) self.conf.core.add_constant( keys=["on_platform"], diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index 5e50a5a32..d07177de3 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -379,6 +379,8 @@ def environment_variables(self) -> dict[str, str]: result["TOX_ENV_NAME"] = self.name result["TOX_WORK_DIR"] = str(self.core["work_dir"]) result["TOX_ENV_DIR"] = str(self.conf["env_dir"]) + if (ci := os.environ.get("CI")) is not None: + result["__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI"] = ci return result @staticmethod diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py index a870d3649..a097b5a67 100644 --- a/src/tox/tox_env/package.py +++ b/src/tox/tox_env/package.py @@ -97,7 +97,9 @@ def mark_active_run_env(self, run_env: RunToxEnv) -> None: self._envs.add(run_env.conf.name) def teardown_env(self, conf: EnvConfigSet) -> None: - self._envs.remove(conf.name) + if conf.name in self._envs: + # conf.name (".tox") may be missing in self._envs in the case of an automatically provisioned environment + self._envs.remove(conf.name) if len(self._envs) == 0: self._teardown() 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..37fb4625a 100644 --- a/src/tox/tox_env/python/virtual_env/package/pyproject.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -118,6 +118,12 @@ def root(self) -> Path: @root.setter def root(self, value: Path) -> None: + # Recreating the frontend with a new root would orphan the current frontend.backend_executor, if any, making tox + # hang upon exit waiting for its threads and subprocesses (#3512). + # Therefore, we make sure to close the existing back-end executor in the case of an existing PEP 517 frontend. + if self._frontend is not None: + self._frontend.backend_executor.close() + self._root = value self._frontend_ = None # force recreating the frontend with new root diff --git a/tests/config/source/test_discover.py b/tests/config/source/test_discover.py index 86370e5ce..d86fb750b 100644 --- a/tests/config/source/test_discover.py +++ b/tests/config/source/test_discover.py @@ -10,8 +10,8 @@ def out_no_src(path: Path) -> str: return ( - f"ROOT: No tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}\n" - f"default environments:\npy -> [no description]\n" + f"ROOT: No loadable tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}" + f"\ndefault environments:\npy -> [no description]\n" ) @@ -47,4 +47,4 @@ def test_bad_src_content(tox_project: ToxProjectCreator, tmp_path: Path) -> None outcome = project.run("l", "-c", str(tmp_path / "setup.cfg")) outcome.assert_failed() - assert outcome.out == f"ROOT: HandledError| could not recognize config file {tmp_path / 'setup.cfg'}\n" + assert outcome.out == f"ROOT: HandledError| config file {tmp_path / 'setup.cfg'} does not exist\n" diff --git a/tests/config/source/test_setup_cfg.py b/tests/config/source/test_setup_cfg.py index 3de474735..e5a84ec9d 100644 --- a/tests/config/source/test_setup_cfg.py +++ b/tests/config/source/test_setup_cfg.py @@ -19,4 +19,4 @@ def test_bad_conf_setup_cfg(tox_project: ToxProjectCreator) -> None: filename = str(project.path / "setup.cfg") outcome = project.run("l", "-c", filename) outcome.assert_failed() - assert outcome.out == f"ROOT: HandledError| could not recognize config file {filename}\n" + assert outcome.out == f"ROOT: HandledError| SetupCfg failed loading {filename} due to section tox:tox not found\n" diff --git a/tests/plugin/test_inline.py b/tests/plugin/test_inline.py index 4fae64b27..46af969a3 100644 --- a/tests/plugin/test_inline.py +++ b/tests/plugin/test_inline.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -43,6 +44,7 @@ def tox_extend_envs() -> tuple[str]: def tox_add_core_config(core_conf: ConfigSet, state: State) -> None: # noqa: ARG001 in_memory_config_loader = MemoryLoader( base=["sentinel-base"], + commands_pre=["sentinel-cmd"], description="sentinel-description", ) state.conf.memory_seed_loaders[env_name].append( @@ -59,3 +61,15 @@ def tox_add_core_config(core_conf: ConfigSet, state: State) -> None: # noqa: AR tox_config_result = project.run("config", "-e", "sentinel-env-name", "-qq") tox_config_result.assert_success() assert "base = sentinel-base" in tox_config_result.out + + tox_run_result = project.run("run", "-e", "sentinel-env-name", "-q") + tox_run_result.assert_failed() + underlying_expected_oserror_msg = ( + "[WinError 2] The system cannot find the file specified" + if sys.platform == "win32" + else "[Errno 2] No such file or directory: 'sentinel-cmd'" + ) + expected_cmd_lookup_error_txt = ( + f"sentinel-env-name: Exception running subprocess {underlying_expected_oserror_msg!s}\n" + ) + assert expected_cmd_lookup_error_txt in tox_run_result.out diff --git a/tests/session/cmd/test_legacy.py b/tests/session/cmd/test_legacy.py index 0d04dc6a1..08d65fe2d 100644 --- a/tests/session/cmd/test_legacy.py +++ b/tests/session/cmd/test_legacy.py @@ -66,7 +66,8 @@ def test_legacy_list_env_with_no_tox_file(tox_project: ToxProjectCreator) -> Non outcome = project.run("le", "-l") outcome.assert_success() out = ( - f"ROOT: No tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {project.path}\n" + "ROOT: No loadable tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at " + f"{project.path}\n" ) assert not outcome.err assert outcome.out == out diff --git a/tests/test_provision.py b/tests/test_provision.py index 0cf4de589..2cde4f68b 100644 --- a/tests/test_provision.py +++ b/tests/test_provision.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from pathlib import Path from subprocess import check_call -from typing import TYPE_CHECKING, Callable, Iterator +from typing import TYPE_CHECKING, Callable, Iterator, Sequence from unittest import mock from zipfile import ZipFile @@ -16,6 +16,7 @@ from packaging.requirements import Requirement if TYPE_CHECKING: + from build import DistributionType from devpi_process import Index, IndexServer from tox.pytest import MonkeyPatch, TempPathFactory, ToxProjectCreator @@ -102,22 +103,38 @@ def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> list[Path] @pytest.fixture(scope="session") -def pypi_index_self(pypi_server: IndexServer, tox_wheels: list[Path], demo_pkg_inline_wheel: Path) -> Index: - with elapsed("start devpi and create index"): # takes around 1s +def local_pypi_indexes( + pypi_server: IndexServer, tox_wheels: list[Path], demo_pkg_inline_wheel: Path +) -> tuple[Index, Index]: + with elapsed("start devpi and create indexes"): # takes around 1s + pypi_server.create_index("mirror", "type=mirror", "mirror_url=https://pypi.org/simple/") + mirrored_index = pypi_server.create_index("magic", f"bases={pypi_server.user}/mirror") self_index = pypi_server.create_index("self", "volatile=False") with elapsed("upload tox and its wheels to devpi"): # takes around 3.2s on build + mirrored_index.upload(*tox_wheels, demo_pkg_inline_wheel) self_index.upload(*tox_wheels, demo_pkg_inline_wheel) - return self_index + return mirrored_index, self_index -@pytest.fixture -def _pypi_index_self(pypi_index_self: Index, monkeypatch: MonkeyPatch) -> None: - pypi_index_self.use() - monkeypatch.setenv("PIP_INDEX_URL", pypi_index_self.url) +def _use_pypi_index(pypi_index: Index, monkeypatch: MonkeyPatch) -> None: + pypi_index.use() + monkeypatch.setenv("PIP_INDEX_URL", pypi_index.url) monkeypatch.setenv("PIP_RETRIES", str(2)) monkeypatch.setenv("PIP_TIMEOUT", str(5)) +@pytest.fixture +def _pypi_index_mirrored(local_pypi_indexes: tuple[Index, Index], monkeypatch: MonkeyPatch) -> None: + pypi_index_mirrored, _ = local_pypi_indexes + _use_pypi_index(pypi_index_mirrored, monkeypatch) + + +@pytest.fixture +def _pypi_index_self(local_pypi_indexes: tuple[Index, Index], monkeypatch: MonkeyPatch) -> None: + _, pypi_index_self = local_pypi_indexes + _use_pypi_index(pypi_index_self, monkeypatch) + + def test_provision_requires_nok(tox_project: ToxProjectCreator) -> None: ini = "[tox]\nrequires = pkg-does-not-exist\n setuptools==1\nskipsdist=true\n" outcome = tox_project({"tox.ini": ini}).run("c", "-e", "py") @@ -254,3 +271,33 @@ def test_provision_default_arguments_exists(tox_project: ToxProjectCreator, subc outcome = project.run(subcommand) for argument in ["result_json", "hash_seed", "discover", "list_dependencies"]: assert hasattr(outcome.state.conf.options, argument) + + +@pytest.mark.integration +@pytest.mark.usefixtures("_pypi_index_mirrored") +def test_provision_install_pkg_pep517( + tmp_path_factory: TempPathFactory, + tox_project: ToxProjectCreator, + pkg_builder: Callable[[Path, Path, Sequence[DistributionType], bool], Path], +) -> None: + example = tmp_path_factory.mktemp("example") + skeleton = """ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "skeleton" + version = "0.1.1337" + """ + (example / "pyproject.toml").write_text(skeleton) + sdist = pkg_builder(example / "dist", example, ["sdist"], False) + + tox_ini = r""" + [tox] + requires = demo-pkg-inline + [testenv] + commands = python -c "print(42)" + """ + project = tox_project({"tox.ini": tox_ini}, base=example) + result = project.run("r", "-e", "py", "--installpkg", str(sdist), "--notest") + result.assert_success() diff --git a/tests/tox_env/python/virtual_env/package/conftest.py b/tests/tox_env/python/virtual_env/package/conftest.py index ca0fd355c..e96910954 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 @@ -41,3 +41,41 @@ def pkg_with_extras_project(tmp_path_factory: pytest.TempPathFactory) -> Path: toml = '[build-system]\nrequires=["setuptools"]\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 b756b7621..88fb0c2fc 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 @@ -464,3 +464,24 @@ 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_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)) + result.assert_success() diff --git a/tests/tox_env/test_ci_passthrough.py b/tests/tox_env/test_ci_passthrough.py new file mode 100644 index 000000000..28c643816 --- /dev/null +++ b/tests/tox_env/test_ci_passthrough.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from tox.pytest import ToxProjectCreator + + +@pytest.mark.parametrize("value", ["1", "0", "", "arbitrary_value"]) +def test_ci_passthrough_present(value: str, tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("CI", value) + prj = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(0)'\n"}) + execute_calls = prj.patch_execute(lambda _r: 0) + result = prj.run("r", "-e", "py") + result.assert_success() + req = execute_calls.call_args[0][3] + assert req.env["__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI"] == value + + +def test_ci_passthrough_absent(tox_project: ToxProjectCreator, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("CI", raising=False) + prj = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(0)'\n"}) + execute_calls = prj.patch_execute(lambda _r: 0) + result = prj.run("r", "-e", "py") + result.assert_success() + req = execute_calls.call_args[0][3] + assert "__TOX_ENVIRONMENT_VARIABLE_ORIGINAL_CI" not in req.env diff --git a/tox.toml b/tox.toml index fee6aa48c..d0986bc70 100644 --- a/tox.toml +++ b/tox.toml @@ -85,7 +85,7 @@ commands = [ [ "python", "-c", - 'print(r"documentation available under file://{work_dir}{/}docs_out{/}index.html")', + 'print(r"documentation available under file://{work_dir}{/}docs_out{/}html{/}index.html")', ], ]