diff options
-rw-r--r-- | .pre-commit-config.yaml | 2 | ||||
-rw-r--r-- | docs/changelog/1843.bugfix.rst | 2 | ||||
-rw-r--r-- | docs/changelog/1843.feature.rst | 1 | ||||
-rw-r--r-- | src/tox/session/cmd/run/single.py | 68 | ||||
-rw-r--r-- | src/tox/tox_env/runner.py | 8 | ||||
-rw-r--r-- | tests/demo_pkg_inline/build.py | 13 | ||||
-rw-r--r-- | tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py | 2 | ||||
-rw-r--r-- | tests/session/cmd/test_sequential.py | 66 | ||||
-rw-r--r-- | tox.ini | 2 |
9 files changed, 124 insertions, 40 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d388dd29..326682d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: - --min-py3-version - "3.6" - repo: https://github.com/asottile/blacken-docs - rev: v1.9.1 + rev: v1.9.2 hooks: - id: blacken-docs additional_dependencies: diff --git a/docs/changelog/1843.bugfix.rst b/docs/changelog/1843.bugfix.rst new file mode 100644 index 00000000..e5bd4507 --- /dev/null +++ b/docs/changelog/1843.bugfix.rst @@ -0,0 +1,2 @@ +Fix handling of ``commands_pre``/``commands``/``commands_post`` to be in line with tox 3 (returned incorrect exit codes +and post was not always executed) - by :user:`gaborbernat`. diff --git a/docs/changelog/1843.feature.rst b/docs/changelog/1843.feature.rst new file mode 100644 index 00000000..e2850004 --- /dev/null +++ b/docs/changelog/1843.feature.rst @@ -0,0 +1 @@ +Add support for the ``ignore_errors`` settings in tox test environments - by :user:`gaborbernat`. diff --git a/src/tox/session/cmd/run/single.py b/src/tox/session/cmd/run/single.py index 8f0881e2..4b89b7f7 100644 --- a/src/tox/session/cmd/run/single.py +++ b/src/tox/session/cmd/run/single.py @@ -3,11 +3,11 @@ Defines how to run a single tox environment. """ import logging import time -from typing import List, NamedTuple, Tuple, cast +from pathlib import Path +from typing import List, NamedTuple, Tuple from tox.config.types import Command from tox.execute.api import Outcome, StdinSource -from tox.tox_env.api import ToxEnv from tox.tox_env.errors import Fail, Skip from tox.tox_env.python.virtual_env.package.api import ToxBackendFailed from tox.tox_env.runner import RunToxEnv @@ -32,7 +32,7 @@ def run_one(tox_env: RunToxEnv, recreate: bool, no_test: bool, suspend_display: return ToxEnvRunResult(name, skipped, code, outcomes, duration) -def _evaluate(tox_env: ToxEnv, recreate: bool, no_test: bool) -> Tuple[bool, int, List[Outcome]]: +def _evaluate(tox_env: RunToxEnv, recreate: bool, no_test: bool) -> Tuple[bool, int, List[Outcome]]: skipped = False code: int = 0 outcomes: List[Outcome] = [] @@ -59,28 +59,48 @@ def _evaluate(tox_env: ToxEnv, recreate: bool, no_test: bool) -> Tuple[bool, int return skipped, code, outcomes -def run_commands(tox_env: ToxEnv, no_test: bool) -> Tuple[int, List[Outcome]]: - status = Outcome.OK # assume all good +def run_commands(tox_env: RunToxEnv, no_test: bool) -> Tuple[int, List[Outcome]]: outcomes: List[Outcome] = [] - if no_test is False: - keys = ("commands_pre", "commands", "commands_post") - for key in keys: - for at, cmd in enumerate(cast(List[Command], tox_env.conf[key])): - current_outcome = tox_env.execute( - cmd.args, - cwd=tox_env.conf["change_dir"], - stdin=StdinSource.user_only(), - show=True, - run_id=f"{key}[{at}]", - ) - outcomes.append(current_outcome) - if cmd.ignore_exit_code: - continue - try: - current_outcome.assert_success() - except SystemExit as exception: - return exception.code, outcomes - return status, outcomes + if no_test: + status_pre, status_main, status_post = Outcome.OK, Outcome.OK, Outcome.OK + else: + chdir: Path = tox_env.conf["change_dir"] + ignore_errors: bool = tox_env.conf["ignore_errors"] + try: + status_pre = run_command_set(tox_env, "commands_pre", chdir, ignore_errors, outcomes) + if status_pre == Outcome.OK or ignore_errors: + status_main = run_command_set(tox_env, "commands", chdir, ignore_errors, outcomes) + else: + status_main = Outcome.OK + finally: + status_post = run_command_set(tox_env, "commands_post", chdir, ignore_errors, outcomes) + exit_code = status_pre or status_main or status_post # first non-success + return exit_code, outcomes + + +def run_command_set(tox_env: RunToxEnv, key: str, cwd: Path, ignore_errors: bool, outcomes: List[Outcome]) -> int: + exit_code = Outcome.OK + command_set: List[Command] = tox_env.conf[key] + for at, cmd in enumerate(command_set): + current_outcome = tox_env.execute( + cmd.args, + cwd=cwd, + stdin=StdinSource.user_only(), + show=True, + run_id=f"{key}[{at}]", + ) + outcomes.append(current_outcome) + try: + current_outcome.assert_success() + except SystemExit as exception: + if cmd.ignore_exit_code: + continue + if ignore_errors: + if exit_code == Outcome.OK: + exit_code = exception.code # ignore errors continues ahead but saves the exit code + continue + return exception.code + return exit_code __all__ = ( diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py index 1a44d7c0..146e395f 100644 --- a/src/tox/tox_env/runner.py +++ b/src/tox/tox_env/runner.py @@ -59,7 +59,13 @@ class RunToxEnv(ToxEnv, ABC): keys=["change_dir", "changedir"], of_type=Path, default=lambda conf, name: cast(Path, conf.core["tox_root"]), - desc="Change to this working directory when executing the test command.", + desc="change to this working directory when executing the test command", + ) + self.conf.add_config( + keys=["ignore_errors"], + of_type=bool, + default=False, + desc="when executing the commands keep going even if a sub-command exits with non-zero exit code", ) self.has_package = self.add_package_conf() diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py index 70663632..7476d88f 100644 --- a/tests/demo_pkg_inline/build.py +++ b/tests/demo_pkg_inline/build.py @@ -2,6 +2,7 @@ import sys import tarfile from pathlib import Path from textwrap import dedent +from typing import Dict, List, Optional from zipfile import ZipFile name = "demo_pkg_inline" @@ -42,7 +43,11 @@ content = { } -def build_wheel(wheel_directory, metadata_directory=None, config_settings=None): +def build_wheel( + wheel_directory: str, + metadata_directory: Optional[str] = None, + config_settings: Optional[Dict[str, str]] = None, +) -> str: base_name = f"{name}-{version}-py{sys.version_info.major}-none-any.whl" path = Path(wheel_directory) / base_name with ZipFile(str(path), "w") as zip_file_handler: @@ -52,11 +57,11 @@ def build_wheel(wheel_directory, metadata_directory=None, config_settings=None): return base_name -def get_requires_for_build_wheel(config_settings): +def get_requires_for_build_wheel(config_settings: Optional[Dict[str, str]] = None) -> List[str]: return [] # pragma: no cover # only executed in non-host pythons -def build_sdist(sdist_directory, config_settings=None): +def build_sdist(sdist_directory: str, config_settings: Optional[Dict[str, str]] = None) -> str: result = f"{name}-{version}.tar.gz" with tarfile.open(str(Path(sdist_directory) / result), "w:gz") as tar: root = Path(__file__).parent @@ -65,5 +70,5 @@ def build_sdist(sdist_directory, config_settings=None): return result -def get_requires_for_build_sdist(config_settings): +def get_requires_for_build_sdist(config_settings: Optional[Dict[str, str]] = None) -> List[str]: return [] # pragma: no cover # only executed in non-host pythons diff --git a/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py b/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py index 694740b1..3d596bf8 100644 --- a/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py +++ b/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py @@ -1,2 +1,2 @@ -def do(): +def do(): # type: () -> None print("greetings from demo_pkg_setuptools") diff --git a/tests/session/cmd/test_sequential.py b/tests/session/cmd/test_sequential.py index a14e2ff3..e9b73a71 100644 --- a/tests/session/cmd/test_sequential.py +++ b/tests/session/cmd/test_sequential.py @@ -161,8 +161,8 @@ def test_recreate_package(tox_project: ToxProjectCreator, demo_pkg_inline: Path) def test_package_deps_change(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: toml = (demo_pkg_inline / "pyproject.toml").read_text() build = (demo_pkg_inline / "build.py").read_text() - ini = "[testenv]\npackage=wheel\ncommands=python -c 'from demo_pkg_inline import do; do()'" - proj = tox_project({"tox.ini": ini, "pyproject.toml": toml, "build.py": build}) + proj = tox_project({"tox.ini": "[testenv]\npackage=wheel", "pyproject.toml": toml, "build.py": build}) + proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) result_first = proj.run("r") result_first.assert_success() @@ -170,12 +170,7 @@ def test_package_deps_change(tox_project: ToxProjectCreator, demo_pkg_inline: Pa # new deps are picked up (proj.path / "pyproject.toml").write_text(toml.replace("requires = []", 'requires = ["wheel"]')) - (proj.path / "build.py").write_text( - build.replace( - "def get_requires_for_build_wheel(config_settings):\n return []", - "def get_requires_for_build_wheel(config_settings):\n return ['setuptools']", - ) - ) + (proj.path / "build.py").write_text(build.replace("return []", "return ['setuptools']")) result_rerun = proj.run("r") result_rerun.assert_success() @@ -389,3 +384,58 @@ def test_skip_develop_mode(tox_project: ToxProjectCreator, demo_pkg_setuptools: (".pkg", "_exit"), ] assert calls == expected + + +def _c(code: int) -> str: + return f"python -c 'raise SystemExit({code})'" + + +def test_commands_pre_fail_post_runs(tox_project: ToxProjectCreator) -> None: + ini = f"[testenv]\npackage=skip\ncommands_pre={_c(8)}\ncommands={_c(0)}\ncommands_post={_c(9)}" + proj = tox_project({"tox.ini": ini}) + result = proj.run() + result.assert_failed(code=8) + assert "commands_pre[0]" in result.out + assert "commands[0]" not in result.out + assert "commands_post[0]" in result.out + + +def test_commands_pre_pass_post_runs_main_fails(tox_project: ToxProjectCreator) -> None: + ini = f"[testenv]\npackage=skip\ncommands_pre={_c(0)}\ncommands={_c(8)}\ncommands_post={_c(9)}" + proj = tox_project({"tox.ini": ini}) + result = proj.run() + result.assert_failed(code=8) + assert "commands_pre[0]" in result.out + assert "commands[0]" in result.out + assert "commands_post[0]" in result.out + + +def test_commands_post_fails_exit_code(tox_project: ToxProjectCreator) -> None: + ini = f"[testenv]\npackage=skip\ncommands_pre={_c(0)}\ncommands={_c(0)}\ncommands_post={_c(9)}" + proj = tox_project({"tox.ini": ini}) + result = proj.run() + result.assert_failed(code=9) + assert "commands_pre[0]" in result.out + assert "commands[0]" in result.out + assert "commands_post[0]" in result.out + + +@pytest.mark.parametrize( + ["pre", "main", "post", "outcome"], + [ + (0, 8, 0, 8), + (0, 0, 8, 8), + (8, 0, 0, 8), + ], +) +def test_commands_ignore_errors(tox_project: ToxProjectCreator, pre: int, main: int, post: int, outcome: int) -> None: + def _s(key: str, code: int) -> str: + return f"\ncommands{key}=\n {_c(code)}\n {'' if code == 0 else _c(code+1)}" + + ini = f"[testenv]\npackage=skip\nignore_errors=True{_s('_pre', pre)}{_s('', main)}{_s('_post', post)}" + proj = tox_project({"tox.ini": ini}) + result = proj.run() + result.assert_failed(code=outcome) + assert "commands_pre[0]" in result.out + assert "commands[0]" in result.out + assert "commands_post[0]" in result.out @@ -51,7 +51,7 @@ commands = description = run type check on code base setenv = {tty:MYPY_FORCE_COLOR = 1} deps = - mypy==0.790 + mypy==0.800 commands = mypy src/tox mypy tests |