Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow configurable child shutdown timeouts #565

Merged
merged 12 commits into from
Feb 22, 2022
15 changes: 10 additions & 5 deletions nox/popen.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@
from typing import IO, Mapping, Sequence


def shutdown_process(proc: subprocess.Popen) -> tuple[bytes, bytes]:
def shutdown_process(
proc: subprocess.Popen,
interrupt_timeout: float,
terminate_timeout: float,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's allow waiting without timeout as well.

Suggested change
interrupt_timeout: float,
terminate_timeout: float,
interrupt_timeout: float | None,
terminate_timeout: float | None,

) -> tuple[bytes, bytes]:
"""Gracefully shutdown a child process."""

with contextlib.suppress(subprocess.TimeoutExpired):
return proc.communicate(timeout=0.3)
return proc.communicate(timeout=interrupt_timeout)

proc.terminate()

with contextlib.suppress(subprocess.TimeoutExpired):
return proc.communicate(timeout=0.2)
return proc.communicate(timeout=terminate_timeout)

proc.kill()

Expand Down Expand Up @@ -60,6 +63,8 @@ def popen(
silent: bool = False,
stdout: int | IO | None = None,
stderr: int | IO = subprocess.STDOUT,
interrupt_timeout: float = 0.3,
terminate_timeout: float = 0.2,
Copy link
Collaborator

Choose a reason for hiding this comment

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

See above.

Suggested change
interrupt_timeout: float = 0.3,
terminate_timeout: float = 0.2,
interrupt_timeout: float | None = 0.3,
terminate_timeout: float | None = 0.2,

) -> tuple[int, str]:
if silent and stdout is not None:
raise ValueError(
Expand All @@ -77,7 +82,7 @@ def popen(
sys.stdout.flush()

except KeyboardInterrupt:
out, err = shutdown_process(proc)
out, err = shutdown_process(proc, interrupt_timeout, terminate_timeout)
if proc.returncode != 0:
raise

Expand Down
54 changes: 53 additions & 1 deletion tests/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,10 @@ def test_run_external_not_a_virtualenv(self):
session.run(sys.executable, "--version")

run.assert_called_once_with(
(sys.executable, "--version"), external=True, env=mock.ANY, paths=None
(sys.executable, "--version"),
external=True,
env=mock.ANY,
paths=None,
)

def test_run_external_condaenv(self):
Expand Down Expand Up @@ -344,6 +347,55 @@ def test_run_always_install_only(self, caplog):

assert session.run_always(operator.add, 23, 19) == 42

@pytest.mark.parametrize(
(
"interrupt_timeout_setting",
"terminate_timeout_setting",
"interrupt_timeout_expected",
"terminate_timeout_expected",
),
[
(None, None, 0.3, 0.2),
Copy link
Collaborator

Choose a reason for hiding this comment

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

If None is passed, None is the expected value (see above).

For the defaults, pass a different sentinel instead of None and check for that further down. For example, the string "default" would work.

(0, 0, 0, 0),
(1, 2, 1, 2),
],
)
def test_run_shutdown_process_timeouts(
self,
interrupt_timeout_setting,
terminate_timeout_setting,
interrupt_timeout_expected,
terminate_timeout_expected,
):
session, runner = self.make_session_and_runner()

runner.venv = nox.virtualenv.ProcessEnv()

subp_popen_instance = mock.Mock()
subp_popen_instance.communicate.side_effect = KeyboardInterrupt()
with mock.patch(
"nox.popen.shutdown_process", autospec=True
) as shutdown_process, mock.patch(
"nox.popen.subprocess.Popen",
new=mock.Mock(return_value=subp_popen_instance),
):
shutdown_process.return_value = ("", "")

timeout_kwargs = {}
if interrupt_timeout_setting is not None:
timeout_kwargs["interrupt_timeout"] = interrupt_timeout_setting
if terminate_timeout_setting is not None:
timeout_kwargs["terminate_timeout"] = terminate_timeout_setting

with pytest.raises(KeyboardInterrupt):
session.run(sys.executable, "--version", **timeout_kwargs)

shutdown_process.assert_called_once_with(
proc=mock.ANY,
interrupt_timeout=interrupt_timeout_expected,
terminate_timeout=terminate_timeout_expected,
)

@pytest.mark.parametrize(
("no_install", "reused", "run_called"),
[
Expand Down