summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2021-08-15 10:07:45 +0100
committerGitHub <noreply@github.com>2021-08-15 10:07:45 +0100
commit78176651933ef954111a7e392b6914df938b7c8b (patch)
tree7967e04d15d948a71c9fe7bc90cfd5a073259548
parent6bb9491311b9bfa0602d56e68b1448b94f71b4a1 (diff)
downloadtox-git-78176651933ef954111a7e392b6914df938b7c8b.tar.gz
Add support for execute interrupt timeouts (#2158)
-rw-r--r--docs/changelog/2124.bugfix.rst2
-rw-r--r--docs/config.rst29
-rw-r--r--src/tox/execute/api.py64
-rw-r--r--src/tox/execute/local_sub_process/__init__.py101
-rw-r--r--src/tox/execute/pep517_backend.py17
-rw-r--r--src/tox/pytest.py28
-rw-r--r--src/tox/tox_env/api.py6
-rw-r--r--tests/execute/local_subprocess/local_subprocess_sigint.py6
-rw-r--r--tests/execute/local_subprocess/test_local_subprocess.py22
-rw-r--r--tests/session/cmd/test_show_config.py15
-rw-r--r--whitelist.txt70
11 files changed, 222 insertions, 138 deletions
diff --git a/docs/changelog/2124.bugfix.rst b/docs/changelog/2124.bugfix.rst
new file mode 100644
index 00000000..534f52be
--- /dev/null
+++ b/docs/changelog/2124.bugfix.rst
@@ -0,0 +1,2 @@
+Add support for setting :ref:`suicide_timeout`, :ref:`interrupt_timeout` and :ref:`terminate_timeout` - by
+:user:`gaborbernat`.
diff --git a/docs/config.rst b/docs/config.rst
index 9a89d8bf..18914932 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -293,6 +293,35 @@ Base
``allowlist_externals=make`` or ``allowlist_externals=/usr/bin/make``. If you want to allow all external commands
you can use ``allowlist_externals=*`` which will match all commands (not recommended).
+Execute
+~~~~~~~
+
+.. conf::
+ :keys: suicide_timeout
+ :default: 0.0
+ :version_added: 3.15.2
+
+ When an interrupt is sent via Ctrl+C or the tox process is killed with a SIGTERM, a SIGINT is sent to all foreground
+ processes. The :ref:`suicide_timeout` gives the running process time to cleanup and exit before receiving (in some
+ cases, a duplicate) SIGINT from tox.
+
+.. conf::
+ :keys: interrupt_timeout
+ :default: 0.3
+ :version_added: 3.15
+
+ When tox is interrupted, it propagates the signal to the child process after :ref:`suicide_timeout` seconds. If the
+ process still hasn't exited after :ref:`interrupt_timeout` seconds, its sends a SIGTERM.
+
+.. conf::
+ :keys: terminate_timeout
+ :default: 0.2
+ :version_added: 3.15
+
+ When tox is interrupted, after waiting :ref:`interrupt_timeout` seconds, it propagates the signal to the child
+ process, waits :ref:`interrupt_timeout` seconds, sends it a SIGTERM, waits :ref:`terminate_timeout` seconds, and
+ sends it a SIGKILL if it hasn't exited.
+
Run
~~~
diff --git a/src/tox/execute/api.py b/src/tox/execute/api.py
index 4aa880fd..f757eb60 100644
--- a/src/tox/execute/api.py
+++ b/src/tox/execute/api.py
@@ -7,7 +7,7 @@ import time
from abc import ABC, abstractmethod
from contextlib import contextmanager
from types import TracebackType
-from typing import Any, Callable, Dict, Iterator, NoReturn, Optional, Sequence, Tuple, Type
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, NoReturn, Optional, Sequence, Tuple, Type, cast
from colorama import Fore
@@ -16,14 +16,56 @@ from tox.report import OutErr
from .request import ExecuteRequest, StdinSource
from .stream import SyncWrite
+if TYPE_CHECKING:
+ from tox.tox_env.api import ToxEnv
+
ContentHandler = Callable[[bytes], None]
Executor = Callable[[ExecuteRequest, ContentHandler, ContentHandler], int]
LOGGER = logging.getLogger(__name__)
+class ExecuteOptions:
+ def __init__(self, env: "ToxEnv") -> None:
+ self._env = env
+
+ @classmethod
+ def register_conf(cls, env: "ToxEnv") -> None: # noqa
+ env.conf.add_config(
+ keys=["suicide_timeout"],
+ desc="timeout to allow process to exit before sending SIGINT",
+ of_type=float,
+ default=0.0,
+ )
+ env.conf.add_config(
+ keys=["interrupt_timeout"],
+ desc="timeout before sending SIGTERM after SIGINT",
+ of_type=float,
+ default=0.3,
+ )
+ env.conf.add_config(
+ keys=["terminate_timeout"],
+ desc="timeout before sending SIGKILL after SIGTERM",
+ of_type=float,
+ default=0.2,
+ )
+
+ @property
+ def suicide_timeout(self) -> float:
+ return cast(float, self._env.conf["suicide_timeout"])
+
+ @property
+ def interrupt_timeout(self) -> float:
+ return cast(float, self._env.conf["interrupt_timeout"])
+
+ @property
+ def terminate_timeout(self) -> float:
+ return cast(float, self._env.conf["terminate_timeout"])
+
+
class ExecuteStatus(ABC):
- def __init__(self, out: SyncWrite, err: SyncWrite) -> None:
+ def __init__(self, options: ExecuteOptions, out: SyncWrite, err: SyncWrite) -> None:
self.outcome: Optional[Outcome] = None
+ self.options = options
self._out = out
self._err = err
@@ -33,7 +75,7 @@ class ExecuteStatus(ABC):
raise NotImplementedError
@abstractmethod
- def wait(self, timeout: Optional[float] = None) -> None: # noqa: U100
+ def wait(self, timeout: Optional[float] = None) -> Optional[int]: # noqa: U100
raise NotImplementedError
@abstractmethod
@@ -65,11 +107,13 @@ class ExecuteStatus(ABC):
class Execute(ABC):
"""Abstract API for execution of a tox environment"""
+ _option_class: Type[ExecuteOptions] = ExecuteOptions
+
def __init__(self, colored: bool) -> None:
self._colored = colored
@contextmanager
- def call(self, request: ExecuteRequest, show: bool, out_err: OutErr) -> Iterator[ExecuteStatus]:
+ def call(self, request: ExecuteRequest, show: bool, out_err: OutErr, env: "ToxEnv") -> Iterator[ExecuteStatus]:
start = time.monotonic()
try:
# collector is what forwards the content from the file streams to the standard streams
@@ -77,7 +121,7 @@ class Execute(ABC):
out_sync = SyncWrite(out.name, out if show else None)
err_sync = SyncWrite(err.name, err if show else None, Fore.RED if self._colored else None)
with out_sync, err_sync:
- instance = self.build_instance(request, out_sync, err_sync)
+ instance = self.build_instance(request, self._option_class(env), out_sync, err_sync)
with instance as status:
yield status
exit_code = status.exit_code
@@ -89,16 +133,21 @@ class Execute(ABC):
@abstractmethod
def build_instance(
- self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite # noqa: U100
+ self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite # noqa: U100
) -> "ExecuteInstance":
raise NotImplementedError
+ @classmethod
+ def register_conf(cls, env: "ToxEnv") -> None:
+ cls._option_class.register_conf(env)
+
class ExecuteInstance(ABC):
"""An instance of a command execution"""
- def __init__(self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite) -> None:
+ def __init__(self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite) -> None:
self.request = request
+ self.options = options
self._out = out
self._err = err
@@ -234,6 +283,7 @@ __all__ = (
"Outcome",
"Execute",
"ExecuteInstance",
+ "ExecuteOptions",
"ExecuteStatus",
"StdinSource",
)
diff --git a/src/tox/execute/local_sub_process/__init__.py b/src/tox/execute/local_sub_process/__init__.py
index 7b8177b3..ffbeddfe 100644
--- a/src/tox/execute/local_sub_process/__init__.py
+++ b/src/tox/execute/local_sub_process/__init__.py
@@ -4,17 +4,15 @@ import logging
import os
import shutil
import sys
-import time
from subprocess import DEVNULL, PIPE, TimeoutExpired
from types import TracebackType
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Sequence, Tuple, Type
from tox.tox_env.errors import Fail
-from ..api import Execute, ExecuteInstance, ExecuteStatus
+from ..api import Execute, ExecuteInstance, ExecuteOptions, ExecuteStatus
from ..request import ExecuteRequest, StdinSource
from ..stream import SyncWrite
-from .read_via_thread import WAIT_GENERAL
if sys.platform == "win32": # explicit check for mypy # pragma: win32 cover
# needs stdin/stdout handlers backed by overlapped IO
@@ -23,6 +21,7 @@ if sys.platform == "win32": # explicit check for mypy # pragma: win32 cover
else:
from asyncio.windows_utils import Popen
from signal import CTRL_C_EVENT as SIG_INTERRUPT
+ from signal import SIGTERM
from subprocess import CREATE_NEW_PROCESS_GROUP
from .read_via_thread_windows import ReadViaThreadWindows as ReadViaThread
@@ -37,20 +36,21 @@ else: # pragma: win32 no cover
CREATION_FLAGS = 0
-WAIT_INTERRUPT = 0.3
-WAIT_TERMINATE = 0.2
+
IS_WIN = sys.platform == "win32"
class LocalSubProcessExecutor(Execute):
- def build_instance(self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite) -> ExecuteInstance:
- return LocalSubProcessExecuteInstance(request, out, err)
+ def build_instance(
+ self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite
+ ) -> ExecuteInstance:
+ return LocalSubProcessExecuteInstance(request, options, out, err)
class LocalSubprocessExecuteStatus(ExecuteStatus):
- def __init__(self, out: SyncWrite, err: SyncWrite, process: "Popen[bytes]"):
+ def __init__(self, options: ExecuteOptions, out: SyncWrite, err: SyncWrite, process: "Popen[bytes]"):
self._process: "Popen[bytes]" = process
- super().__init__(out, err)
+ super().__init__(options, out, err)
self._interrupted = False
@property
@@ -62,54 +62,30 @@ class LocalSubprocessExecuteStatus(ExecuteStatus):
if self._process is not None: # pragma: no branch
# A three level stop mechanism for children - INT -> TERM -> KILL
# communicate will wait for the app to stop, and then drain the standard streams and close them
- proc = self._process
- host_pid = os.getpid()
- to_pid = proc.pid
- logging.warning("requested interrupt of %d from %d", to_pid, host_pid)
- if proc.poll() is None: # still alive, first INT
- logging.warning(
- "send signal %s to %d from %d with timeout %.2f",
- f"SIGINT({SIG_INTERRUPT})",
- to_pid,
- host_pid,
- WAIT_INTERRUPT,
- )
- proc.send_signal(SIG_INTERRUPT)
- start = time.monotonic()
- while proc.poll() is None and (time.monotonic() - start) < WAIT_INTERRUPT:
- continue
- if proc.poll() is None: # pragma: no branch
- if sys.platform == "win32": # explicit check for mypy # pragma: no branch
- logging.warning("terminate %d from %d", to_pid, host_pid) # pragma: no cover
- else:
- logging.warning(
- "send signal %s to %d from %d with timeout %.2f",
- f"SIGTERM({SIGTERM})",
- to_pid,
- host_pid,
- WAIT_TERMINATE,
- )
- proc.terminate()
- start = time.monotonic()
- if sys.platform != "win32": # explicit check for mypy # pragma: no branch
- # Windows terminate is UNIX kill
- while proc.poll() is None and (time.monotonic() - start) < WAIT_TERMINATE:
- continue
- if proc.poll() is None: # pragma: no branch
- logging.warning("send signal %s to %d from %d", f"SIGKILL({SIGKILL})", to_pid, host_pid)
- proc.kill()
- while proc.poll() is None:
- continue # pragma: no cover
+ to_pid, host_pid = self._process.pid, os.getpid()
+ msg = "requested interrupt of %d from %d, activate in %.2f"
+ logging.warning(msg, to_pid, host_pid, self.options.suicide_timeout)
+ if self.wait(self.options.suicide_timeout) is None: # still alive -> INT
+ msg = "send signal %s to %d from %d with timeout %.2f"
+ logging.warning(msg, f"SIGINT({SIG_INTERRUPT})", to_pid, host_pid, self.options.interrupt_timeout)
+ self._process.send_signal(SIG_INTERRUPT)
+ if self.wait(self.options.interrupt_timeout) is None: # still alive -> TERM # pragma: no branch
+ logging.warning(msg, f"SIGTERM({SIGTERM})", to_pid, host_pid, self.options.terminate_timeout)
+ self._process.terminate()
+ if sys.platform != "win32": # Windows terminate is UNIX kill
+ if self.wait(self.options.terminate_timeout) is None: # still alive -> KILL
+ logging.warning(msg[:-18], f"SIGKILL({SIGKILL})", to_pid, host_pid)
+ self._process.kill()
+ self.wait() # unconditional wait as kill should soon bring down the process
+ logging.warning("interrupt finished with success")
else: # pragma: no cover # difficult to test, process must die just as it's being interrupted
- logging.warning("process already dead with %s within %s", proc.returncode, os.getpid())
- logging.warning("interrupt finished with success")
+ logging.warning("process already dead with %s within %s", self._process.returncode, host_pid)
- def wait(self, timeout: Optional[float] = None) -> None:
- # note poll in general might deadlock if output large, but we drain in background threads so not an issue here
- try:
- self._process.wait(timeout=WAIT_GENERAL if timeout is None else timeout)
+ def wait(self, timeout: Optional[float] = None) -> Optional[int]:
+ try: # note wait in general might deadlock if output large, but we drain in background threads so not an issue
+ return self._process.wait(timeout=timeout)
except TimeoutExpired:
- pass
+ return None
def write_stdin(self, content: str) -> None:
stdin = self._process.stdin
@@ -143,33 +119,34 @@ class LocalSubprocessExecuteStatus(ExecuteStatus):
class LocalSubprocessExecuteFailedStatus(ExecuteStatus):
- def __init__(self, out: SyncWrite, err: SyncWrite, exit_code: Optional[int]) -> None:
- super().__init__(out, err)
+ def __init__(self, options: ExecuteOptions, out: SyncWrite, err: SyncWrite, exit_code: Optional[int]) -> None:
+ super().__init__(options, out, err)
self._exit_code = exit_code
@property
def exit_code(self) -> Optional[int]:
return self._exit_code
- def wait(self, timeout: Optional[float] = None) -> None: # noqa: U100
- """already dead no need to wait"""
+ def wait(self, timeout: Optional[float] = None) -> Optional[int]: # noqa: U100
+ return self._exit_code # pragma: no cover
def write_stdin(self, content: str) -> None: # noqa: U100
"""cannot write"""
def interrupt(self) -> None:
- """Nothing running so nothing to interrupt"""
+ return None # pragma: no cover # nothing running so nothing to interrupt
class LocalSubProcessExecuteInstance(ExecuteInstance):
def __init__(
self,
request: ExecuteRequest,
+ options: ExecuteOptions,
out: SyncWrite,
err: SyncWrite,
on_exit_drain: bool = True,
) -> None:
- super().__init__(request, out, err)
+ super().__init__(request, options, out, err)
self.process: Optional[Popen[bytes]] = None
self._cmd: Optional[List[str]] = None
self._read_stderr: Optional[ReadViaThread] = None
@@ -218,9 +195,9 @@ class LocalSubProcessExecuteInstance(ExecuteInstance):
creationflags=CREATION_FLAGS,
)
except OSError as exception:
- return LocalSubprocessExecuteFailedStatus(self._out, self._err, exception.errno)
+ return LocalSubprocessExecuteFailedStatus(self.options, self._out, self._err, exception.errno)
- status = LocalSubprocessExecuteStatus(self._out, self._err, process)
+ status = LocalSubprocessExecuteStatus(self.options, self._out, self._err, process)
drain, pid = self._on_exit_drain, self.process.pid
self._read_stderr = ReadViaThread(stderr.send(process), self.err_handler, name=f"err-{pid}", drain=drain)
self._read_stderr.__enter__()
diff --git a/src/tox/execute/pep517_backend.py b/src/tox/execute/pep517_backend.py
index d865bfaf..4bd26b87 100644
--- a/src/tox/execute/pep517_backend.py
+++ b/src/tox/execute/pep517_backend.py
@@ -7,7 +7,7 @@ from types import TracebackType
from typing import Dict, Optional, Sequence, Tuple, Type
from tox.execute import ExecuteRequest
-from tox.execute.api import Execute, ExecuteInstance, ExecuteStatus
+from tox.execute.api import Execute, ExecuteInstance, ExecuteOptions, ExecuteStatus
from tox.execute.local_sub_process import LocalSubProcessExecuteInstance
from tox.execute.request import StdinSource
from tox.execute.stream import SyncWrite
@@ -26,19 +26,21 @@ class LocalSubProcessPep517Executor(Execute):
self._exc: Optional[Exception] = None
self.is_alive: bool = False
- def build_instance(self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite) -> ExecuteInstance:
- result = LocalSubProcessPep517ExecuteInstance(request, out, err, self.local_execute)
+ def build_instance(
+ self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite
+ ) -> ExecuteInstance:
+ result = LocalSubProcessPep517ExecuteInstance(request, options, out, err, self.local_execute(options))
return result
- @property
- def local_execute(self) -> Tuple[LocalSubProcessExecuteInstance, ExecuteStatus]:
+ def local_execute(self, options: ExecuteOptions) -> Tuple[LocalSubProcessExecuteInstance, ExecuteStatus]:
if self._exc is not None:
raise self._exc
if self._local_execute is None:
request = ExecuteRequest(cmd=self.cmd, cwd=self.cwd, env=self.env, stdin=StdinSource.API, run_id="pep517")
instance = LocalSubProcessExecuteInstance(
- request,
+ request=request,
+ options=options,
out=SyncWrite(name="pep517-out", target=None, color=None), # not enabled no need to enter/exit
err=SyncWrite(name="pep517-err", target=None, color=None), # not enabled no need to enter/exit
on_exit_drain=False,
@@ -89,11 +91,12 @@ class LocalSubProcessPep517ExecuteInstance(ExecuteInstance):
def __init__(
self,
request: ExecuteRequest,
+ options: ExecuteOptions,
out: SyncWrite,
err: SyncWrite,
instance_status: Tuple[LocalSubProcessExecuteInstance, ExecuteStatus],
):
- super().__init__(request, out, err)
+ super().__init__(request, options, out, err)
self._instance, self._status = instance_status
self._lock = Lock()
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index d50dc40e..afea6bee 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -34,7 +34,7 @@ from virtualenv.info import IS_WIN, fs_supports_symlink
import tox.run
from tox.config.sets import EnvConfigSet
-from tox.execute.api import Execute, ExecuteInstance, ExecuteStatus, Outcome
+from tox.execute.api import Execute, ExecuteInstance, ExecuteOptions, ExecuteStatus, Outcome
from tox.execute.request import ExecuteRequest, shell_cmd
from tox.execute.stream import SyncWrite
from tox.report import LOGGER, OutErr
@@ -160,34 +160,38 @@ class ToxProject:
self.exit_code = exit_code
super().__init__(colored)
- def build_instance(self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite) -> ExecuteInstance:
- return MockExecuteInstance(request, out, err, self.exit_code)
+ def build_instance(
+ self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite
+ ) -> ExecuteInstance:
+ return MockExecuteInstance(request, options, out, err, self.exit_code)
class MockExecuteStatus(ExecuteStatus):
- def __init__(self, out: SyncWrite, err: SyncWrite, exit_code: int) -> None:
- super().__init__(out, err)
+ def __init__(self, options: ExecuteOptions, out: SyncWrite, err: SyncWrite, exit_code: int) -> None:
+ super().__init__(options, out, err)
self._exit_code = exit_code
@property
def exit_code(self) -> Optional[int]:
return self._exit_code
- def wait(self, timeout: Optional[float] = None) -> None:
- """ """
+ def wait(self, timeout: Optional[float] = None) -> Optional[int]:
+ return self._exit_code
def write_stdin(self, content: str) -> None:
- """ """
+ return None # pragma: no cover
def interrupt(self) -> None:
- """ """
+ return None # pragma: no cover
class MockExecuteInstance(ExecuteInstance):
- def __init__(self, request: ExecuteRequest, out: SyncWrite, err: SyncWrite, exit_code: int) -> None:
- super().__init__(request, out, err)
+ def __init__(
+ self, request: ExecuteRequest, options: ExecuteOptions, out: SyncWrite, err: SyncWrite, exit_code: int
+ ) -> None:
+ super().__init__(request, options, out, err)
self.exit_code = exit_code
def __enter__(self) -> ExecuteStatus:
- return MockExecuteStatus(self._out, self._err, self.exit_code)
+ return MockExecuteStatus(self.options, self._out, self._err, self.exit_code)
def __exit__(
self,
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index 1ac32bdd..031acd77 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -106,6 +106,7 @@ class ToxEnv(ABC):
default=lambda conf, name: cast(Path, conf.core["work_dir"]) / self.name / "log",
desc="a folder for logging where tox will put logs of tool invocation",
)
+ self.executor.register_conf(self)
self.conf.default_set_env_loader = self._default_set_env
self.conf.add_config(
keys=["platform"],
@@ -337,8 +338,8 @@ class ToxEnv(ABC):
executor: Optional[Execute] = None,
) -> Outcome:
with self.execute_async(cmd, stdin, show, cwd, run_id, executor) as status:
- while status.exit_code is None:
- status.wait()
+ while status.wait() is None:
+ pass # pragma: no cover
if status.outcome is None: # pragma: no cover # this should not happen
raise RuntimeError # pragma: no cover
return status.outcome
@@ -427,6 +428,7 @@ class ToxEnv(ABC):
) -> Iterator[ExecuteStatus]:
with executor.call(
request=request,
+ env=self,
show=show,
out_err=out_err,
) as execute_status:
diff --git a/tests/execute/local_subprocess/local_subprocess_sigint.py b/tests/execute/local_subprocess/local_subprocess_sigint.py
index 848583a6..f43b9d02 100644
--- a/tests/execute/local_subprocess/local_subprocess_sigint.py
+++ b/tests/execute/local_subprocess/local_subprocess_sigint.py
@@ -6,6 +6,7 @@ from io import TextIOWrapper
from pathlib import Path
from types import FrameType
from typing import Optional
+from unittest.mock import MagicMock
from tox.execute import Outcome
from tox.execute.local_sub_process import LocalSubProcessExecutor
@@ -48,11 +49,12 @@ def handler(s: signal.Signals, f: FrameType) -> None:
interrupt_done = False
signal.signal(signal.SIGINT, handler)
logging.info("PID %d start %r", os.getpid(), request)
+tox_env = MagicMock(conf={"suicide_timeout": 0.01, "interrupt_timeout": 0.05, "terminate_timeout": 0.07})
try:
- with executor.call(request, show=False, out_err=out_err) as status:
+ with executor.call(request, show=False, out_err=out_err, env=tox_env) as status:
logging.info("wait on %r", status)
while status.exit_code is None:
- status.wait()
+ status.wait(timeout=0.01) # use wait here with timeout to not block the main thread
logging.info("wait over on %r", status)
show_outcome(status.outcome)
except Exception as exception: # pragma: no cover
diff --git a/tests/execute/local_subprocess/test_local_subprocess.py b/tests/execute/local_subprocess/test_local_subprocess.py
index d6a9cbba..2d66bd00 100644
--- a/tests/execute/local_subprocess/test_local_subprocess.py
+++ b/tests/execute/local_subprocess/test_local_subprocess.py
@@ -6,6 +6,7 @@ import sys
from io import TextIOWrapper
from pathlib import Path
from typing import Dict, List, Tuple
+from unittest.mock import MagicMock
import psutil
import pytest
@@ -48,7 +49,7 @@ def test_local_execute_basic_pass(
code = f"import sys; print({out!r}, end=''); print({err!r}, end='', file=sys.stderr)"
request = ExecuteRequest(cmd=[sys.executable, "-c", code], cwd=Path(), env=os_env, stdin=StdinSource.OFF, run_id="")
out_err = FakeOutErr()
- with executor.call(request, show=show, out_err=out_err.out_err) as status:
+ with executor.call(request, show=show, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None:
status.wait()
assert status.out == out.encode()
@@ -83,7 +84,7 @@ def test_local_execute_basic_pass_show_on_standard_newline_flush(caplog: LogCapt
run_id="",
)
out_err = FakeOutErr()
- with executor.call(request, show=True, out_err=out_err.out_err) as status:
+ with executor.call(request, show=True, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None:
status.wait()
outcome = status.outcome
@@ -121,7 +122,7 @@ def test_local_execute_write_a_lot(os_env: Dict[str, str]) -> None:
run_id="",
)
out_err = FakeOutErr()
- with executor.call(request, show=False, out_err=out_err.out_err) as status:
+ with executor.call(request, show=False, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None:
status.wait()
outcome = status.outcome
@@ -147,7 +148,7 @@ def test_local_execute_basic_fail(capsys: CaptureFixture, caplog: LogCaptureFixt
# run test
out_err = FakeOutErr()
- with executor.call(request, show=False, out_err=out_err.out_err) as status:
+ with executor.call(request, show=False, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None:
status.wait()
outcome = status.outcome
@@ -199,7 +200,7 @@ def test_command_does_not_exist(caplog: LogCaptureFixture, os_env: Dict[str, str
cmd=["sys-must-be-missing"], cwd=Path().absolute(), env=os_env, stdin=StdinSource.OFF, run_id=""
)
out_err = FakeOutErr()
- with executor.call(request, show=False, out_err=out_err.out_err) as status:
+ with executor.call(request, show=False, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None: # pragma: no branch
status.wait() # pragma: no cover
outcome = status.outcome
@@ -226,6 +227,7 @@ def test_command_keyboard_interrupt(tmp_path: Path, monkeypatch: MonkeyPatch, ca
child = next(iter(psutil.Process(pid=root).children())).pid
except AccessDenied as exc: # pragma: no cover # on termux for example
pytest.skip(str(exc)) # pragma: no cover
+ raise # pragma: no cover
print(f"test running in {os.getpid()} and sending CTRL+C to {process.pid}", file=sys.stderr)
process.send_signal(SIG_INTERRUPT)
@@ -236,9 +238,9 @@ def test_command_keyboard_interrupt(tmp_path: Path, monkeypatch: MonkeyPatch, ca
raise
out, err = capfd.readouterr()
- assert f"W requested interrupt of {child} from {root}" in err, err
- assert f"W send signal SIGINT(2) to {child} from {root} with timeout 0.30" in err, err
- assert f"W send signal SIGTERM(15) to {child} from {root} with timeout 0.20" in err, err
+ assert f"W requested interrupt of {child} from {root}, activate in 0.01" in err, err
+ assert f"W send signal SIGINT(2) to {child} from {root} with timeout 0.05" in err, err
+ assert f"W send signal SIGTERM(15) to {child} from {root} with timeout 0.07" in err, err
assert f"W send signal SIGKILL(9) to {child} from {root}" in err, err
outs = out.split("\n")
@@ -262,7 +264,7 @@ def test_local_subprocess_tty(monkeypatch: MonkeyPatch, mocker: MockerFixture, t
cmd: List[str] = [sys.executable, str(Path(__file__).parent / "tty_check.py")]
request = ExecuteRequest(cmd=cmd, stdin=StdinSource.API, cwd=Path.cwd(), env=dict(os.environ), run_id="")
out_err = FakeOutErr()
- with executor.call(request, show=False, out_err=out_err.out_err) as status:
+ with executor.call(request, show=False, out_err=out_err.out_err, env=MagicMock()) as status:
while status.exit_code is None:
status.wait()
outcome = status.outcome
@@ -293,6 +295,6 @@ def test_allow_list_external_ok(fake_exe_on_path: Path, mode: str) -> None:
run_id="run-id",
allow=[allow],
)
- inst = LocalSubProcessExecuteInstance(request, out=SyncWrite("out", None), err=SyncWrite("err", None))
+ inst = LocalSubProcessExecuteInstance(request, MagicMock(), out=SyncWrite("out", None), err=SyncWrite("err", None))
assert inst.cmd == [exe]
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index fb5efe3b..f9441349 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -188,3 +188,18 @@ def test_show_config_cli_flag(tox_project: ToxProjectCreator) -> None:
result = project.run("c", "-e", "py,.pkg", "-k", "package", "recreate", "--develop", "-r", "--no-recreate-pkg")
expected = "[testenv:py]\npackage = dev-legacy\nrecreate = True\n\n[testenv:.pkg]\nrecreate = False\n"
assert result.out == expected
+
+
+def test_show_config_timeout_default(tox_project: ToxProjectCreator) -> None:
+ project = tox_project({"tox.ini": "[testenv]\npakcage=skip"})
+ result = project.run("c", "-e", "py", "-k", "suicide_timeout", "interrupt_timeout", "terminate_timeout")
+ expected = "[testenv:py]\nsuicide_timeout = 0.0\ninterrupt_timeout = 0.3\nterminate_timeout = 0.2\n"
+ assert result.out == expected
+
+
+def test_show_config_timeout_custom(tox_project: ToxProjectCreator) -> None:
+ ini = "[testenv]\npakcage=skip\nsuicide_timeout = 1\ninterrupt_timeout = 2.222\nterminate_timeout = 3.0\n"
+ project = tox_project({"tox.ini": ini})
+ result = project.run("c", "-e", "py", "-k", "suicide_timeout", "interrupt_timeout", "terminate_timeout")
+ expected = "[testenv:py]\nsuicide_timeout = 1.0\ninterrupt_timeout = 2.222\nterminate_timeout = 3.0\n"
+ assert result.out == expected
diff --git a/whitelist.txt b/whitelist.txt
index c5fd2f25..476d0d5a 100644
--- a/whitelist.txt
+++ b/whitelist.txt
@@ -13,14 +13,14 @@ autoclass
autodoc
autosectionlabel
autouse
-BUFSIZE
+bufsize
byref
cachetools
canonicalize
capfd
caplog
capsys
-Cfg
+cfg
changelog
chardet
chdir
@@ -34,22 +34,22 @@ conftest
const
contnode
copytree
-COV
+cov
cpus
creationflags
creq
crypto
-CTRL
+ctrl
cygwin
dedent
deinit
delenv
dep
-Deps
+deps
desc
dest
devenv
-DEVNULL
+devnull
devpi
dirname
distlib
@@ -57,25 +57,24 @@ divmod
doc2path
docname
docutils
-DOTALL
+dotall
dpkg
-E3
-E4
-EBADF
+e3
+e4
+ebadf
editables
-EIO
+eio
entrypoints
envs
epilog
eq
-Eval
+eval
exc
exe
executables
expr
extlinks
extractall
-fallbacks
favicon
filelock
fixup
@@ -96,15 +95,15 @@ getsockname
globals
groupby
groupdict
-Hookimpl
-Hookspec
+hookimpl
+hookspec
hookspecs
htmlhelp
ident
-IGN
-IGNORECASE
+ign
+ignorecase
impl
-INET
+inet
insort
instream
intersphinx
@@ -112,14 +111,14 @@ isalpha
isatty
isspace
iterdir
-IWGRP
-IWOTH
-IWUSR
+iwgrp
+iwoth
+iwusr
kernel32
levelname
levelno
libs
-LIGHTRED
+lightred
lineno
linesep
list2cmdline
@@ -128,13 +127,13 @@ lvl
metavar
mktemp
modifyitems
-MULTILINE
+multiline
namelist
nitpicky
nok
nonlocal
-NotImplementedError
-NOTSET
+notimplementederror
+notset
nox
num
objtype
@@ -144,17 +143,16 @@ parsers
pathlib
pathname
pathsep
-Pep517
+pep517
platformdirs
pluggy
-Popen
+popen
pos
posargs
posix
prepend
prereleases
prj
-proc
prog
proj
psutil
@@ -174,7 +172,7 @@ refspec
reftitle
releaselevel
replacer
-Repo
+repo
req
reqs
retann
@@ -183,17 +181,17 @@ rpartition
rreq
rst
rtd
-Runtime
+runtime
sdist
setdefault
setenv
setitem
shlex
-SIG
-SIGINT
-SIGKILL
+sig
+sigint
+sigkill
signum
-SIGTERM
+sigterm
skipif
splitter
src
@@ -209,7 +207,7 @@ testenv
textwrap
tmp
tmpdir
-Toml
+toml
tomli
towncrier
tox
@@ -229,7 +227,7 @@ usedevelop
util
utils
v3
-VCS
+vcs
ver
virtualenv
whl