Codebase list python-procrunner / 45248bc
Introduce new parameter raise_timeout_exceptions to prepare for the introduction of raising exceptions in timeout conditions by default in a future release. Forward compatible code sets this parameter to True. Markus Gerstel 3 years ago
3 changed file(s) with 99 addition(s) and 5 deletion(s). Raw diff Collapse all Expand all
77 import sys
88 import time
99 import timeit
10 import warnings
1011 from multiprocessing import Pipe
1112 from threading import Thread
1213
345346 environment_override=None,
346347 win32resolve=True,
347348 working_directory=None,
349 raise_timeout_exception=False,
348350 ):
349351 """
350352 Run an external process.
371373 extension.
372374 :param string working_directory: If specified, run the executable from
373375 within this working directory.
376 :param boolean raise_timeout_exception: Forward compatibility flag. If set
377 then a subprocess.TimeoutExpired exception is raised
378 instead of returning an object that can be checked
379 for a timeout condition. Defaults to False, will be
380 changed to True in a future release.
374381 :return: A ReturnObject() containing the executed command, stdout and stderr
375382 (both as bytestrings), and the exitcode. Further values such as
376383 process runtime can be accessed as dictionary values.
388395 start_time = timeit.default_timer()
389396 if timeout is not None:
390397 max_time = start_time + timeout
398 if not raise_timeout_exception:
399 warnings.warn(
400 "Using procrunner with timeout and without raise_timeout_exception set is deprecated",
401 DeprecationWarning,
402 stacklevel=2,
403 )
391404
392405 if environment is not None:
393406 env = {key: _path_resolve(environment[key]) for key in environment}
518531
519532 stdout = stdout.get_output()
520533 stderr = stderr.get_output()
534
535 if timeout_encountered and raise_timeout_exception:
536 raise subprocess.TimeoutExpired(
537 cmd=command, timeout=timeout, output=stdout, stderr=stderr
538 )
539
521540 time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
522
523541 result = ReturnObject(
524542 {
525543 "exitcode": p.returncode,
33 import procrunner
44 import pytest
55 import sys
6
7
8 @mock.patch("procrunner._NonBlockingStreamReader")
9 @mock.patch("procrunner.time")
10 @mock.patch("procrunner.subprocess")
11 @mock.patch("procrunner.Pipe")
12 def test_run_command_aborts_after_timeout_legacy(
13 mock_pipe, mock_subprocess, mock_time, mock_streamreader
14 ):
15 mock_pipe.return_value = mock.Mock(), mock.Mock()
16 mock_process = mock.Mock()
17 mock_process.returncode = None
18 mock_subprocess.Popen.return_value = mock_process
19 task = ["___"]
20
21 with pytest.raises(RuntimeError):
22 with pytest.warns(DeprecationWarning, match="timeout"):
23 procrunner.run(task, -1, False)
24
25 assert mock_subprocess.Popen.called
26 assert mock_process.terminate.called
27 assert mock_process.kill.called
628
729
830 @mock.patch("procrunner._NonBlockingStreamReader")
1941 task = ["___"]
2042
2143 with pytest.raises(RuntimeError):
22 procrunner.run(task, -1, False)
44 procrunner.run(task, -1, False, raise_timeout_exception=True)
2345
2446 assert mock_subprocess.Popen.called
2547 assert mock_process.terminate.called
6587 callback_stdout=mock.sentinel.callback_stdout,
6688 callback_stderr=mock.sentinel.callback_stderr,
6789 working_directory=mock.sentinel.cwd,
90 raise_timeout_exception=True,
6891 )
6992
7093 assert mock_subprocess.Popen.called
103126 def test_default_process_environment_is_parent_environment(mock_subprocess):
104127 mock_subprocess.Popen.side_effect = NotImplementedError() # cut calls short
105128 with pytest.raises(NotImplementedError):
106 procrunner.run([mock.Mock()], -1, False)
129 procrunner.run([mock.Mock()], -1, False, raise_timeout_exception=True)
107130 assert mock_subprocess.Popen.call_args[1]["env"] == os.environ
108131
109132
113136 mock_env = {"key": mock.sentinel.key}
114137 # Pass an environment dictionary
115138 with pytest.raises(NotImplementedError):
116 procrunner.run([mock.Mock()], -1, False, environment=copy.copy(mock_env))
139 procrunner.run(
140 [mock.Mock()],
141 -1,
142 False,
143 environment=copy.copy(mock_env),
144 raise_timeout_exception=True,
145 )
117146 assert mock_subprocess.Popen.call_args[1]["env"] == mock_env
118147
119148
130159 False,
131160 environment=copy.copy(mock_env1),
132161 environment_override=copy.copy(mock_env2),
162 raise_timeout_exception=True,
133163 )
134164 mock_env_sum = copy.copy(mock_env1)
135165 mock_env_sum.update({key: str(mock_env2[key]) for key in mock_env2})
142172 mock_env2 = {"keyB": str(mock.sentinel.keyB)}
143173 with pytest.raises(NotImplementedError):
144174 procrunner.run(
145 [mock.Mock()], -1, False, environment_override=copy.copy(mock_env2)
175 [mock.Mock()],
176 -1,
177 False,
178 environment_override=copy.copy(mock_env2),
179 raise_timeout_exception=True,
146180 )
147181 random_environment_variable = list(os.environ)[0]
148182 if random_environment_variable == list(mock_env2)[0]:
173207 environment_override={
174208 random_environment_variable: "X" + random_environment_value
175209 },
210 raise_timeout_exception=True,
176211 )
177212 assert (
178213 mock_subprocess.Popen.call_args[1]["env"][random_environment_variable]
00 import os
1 import subprocess
12 import sys
3 import timeit
24
35 import procrunner
46 import pytest
6769 assert (
6870 "LEAK_DETECTOR" not in os.environ
6971 ), "overridden environment variable leaked into parent process"
72
73
74 def test_timeout_behaviour_legacy(tmp_path):
75 start = timeit.default_timer()
76 with pytest.warns(DeprecationWarning, match="timeout"):
77 result = procrunner.run(
78 [sys.executable, "-c", "import time; time.sleep(5)"],
79 timeout=0.1,
80 working_directory=tmp_path,
81 raise_timeout_exception=False,
82 )
83 runtime = timeit.default_timer() - start
84 if hasattr(result, "timeout"):
85 with pytest.warns(DeprecationWarning, match="\\.timeout"):
86 assert result.timeout
87 else:
88 assert result["timeout"]
89 assert runtime < 3
90 assert not result.stdout
91 assert not result.stderr
92 assert result.returncode
93
94
95 def test_timeout_behaviour(tmp_path):
96 command = (sys.executable, "-c", "import time; time.sleep(5)")
97 start = timeit.default_timer()
98 with pytest.raises(subprocess.TimeoutExpired) as te:
99 procrunner.run(
100 command,
101 timeout=0.1,
102 working_directory=tmp_path,
103 raise_timeout_exception=True,
104 )
105 runtime = timeit.default_timer() - start
106 assert runtime < 3
107 assert te.value.stdout == b""
108 assert te.value.stderr == b""
109 assert te.value.timeout == 0.1
110 assert te.value.cmd == command