-
-
Notifications
You must be signed in to change notification settings - Fork 157
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
Changes from 6 commits
00593be
7b52be6
f535fd4
33b2eb9
2c4b4a4
0f7a03f
b24aa55
9a6fc6c
5b44439
fcb9a30
9b22f7c
ab35c13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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, | ||||||||||
) -> 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() | ||||||||||
|
||||||||||
|
@@ -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, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above.
Suggested change
|
||||||||||
) -> tuple[int, str]: | ||||||||||
if silent and stdout is not None: | ||||||||||
raise ValueError( | ||||||||||
|
@@ -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 | ||||||||||
|
||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
|
@@ -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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"), | ||
[ | ||
|
There was a problem hiding this comment.
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.