Merge pull request #4078 from altendky/4073-altendky-subprocessing_timeout
Add timeout for Testdir.runpytest_subprocess() and Testdir.run()
This commit is contained in:
commit
a6fb4c8268
1
AUTHORS
1
AUTHORS
|
@ -121,6 +121,7 @@ Katerina Koukiou
|
||||||
Kevin Cox
|
Kevin Cox
|
||||||
Kodi B. Arfer
|
Kodi B. Arfer
|
||||||
Kostis Anagnostopoulos
|
Kostis Anagnostopoulos
|
||||||
|
Kyle Altendorf
|
||||||
Lawrence Mitchell
|
Lawrence Mitchell
|
||||||
Lee Kamentsky
|
Lee Kamentsky
|
||||||
Lev Maximov
|
Lev Maximov
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``.
|
|
@ -61,6 +61,11 @@ def pytest_configure(config):
|
||||||
config.pluginmanager.register(checker)
|
config.pluginmanager.register(checker)
|
||||||
|
|
||||||
|
|
||||||
|
def raise_on_kwargs(kwargs):
|
||||||
|
if kwargs:
|
||||||
|
raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs))))
|
||||||
|
|
||||||
|
|
||||||
class LsofFdLeakChecker(object):
|
class LsofFdLeakChecker(object):
|
||||||
def get_open_files(self):
|
def get_open_files(self):
|
||||||
out = self._exec_lsof()
|
out = self._exec_lsof()
|
||||||
|
@ -482,6 +487,9 @@ class Testdir(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class TimeoutExpired(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
def __init__(self, request, tmpdir_factory):
|
def __init__(self, request, tmpdir_factory):
|
||||||
self.request = request
|
self.request = request
|
||||||
self._mod_collections = WeakKeyDictionary()
|
self._mod_collections = WeakKeyDictionary()
|
||||||
|
@ -1039,14 +1047,23 @@ class Testdir(object):
|
||||||
|
|
||||||
return popen
|
return popen
|
||||||
|
|
||||||
def run(self, *cmdargs):
|
def run(self, *cmdargs, **kwargs):
|
||||||
"""Run a command with arguments.
|
"""Run a command with arguments.
|
||||||
|
|
||||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||||
|
|
||||||
|
:param args: the sequence of arguments to pass to `subprocess.Popen()`
|
||||||
|
:param timeout: the period in seconds after which to timeout and raise
|
||||||
|
:py:class:`Testdir.TimeoutExpired`
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
Returns a :py:class:`RunResult`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
timeout = kwargs.pop("timeout", None)
|
||||||
|
raise_on_kwargs(kwargs)
|
||||||
|
|
||||||
cmdargs = [
|
cmdargs = [
|
||||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||||
]
|
]
|
||||||
|
@ -1061,7 +1078,40 @@ class Testdir(object):
|
||||||
popen = self.popen(
|
popen = self.popen(
|
||||||
cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32")
|
cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32")
|
||||||
)
|
)
|
||||||
ret = popen.wait()
|
|
||||||
|
def handle_timeout():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
timeout_message = (
|
||||||
|
"{seconds} second timeout expired running:"
|
||||||
|
" {command}".format(seconds=timeout, command=cmdargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
popen.kill()
|
||||||
|
popen.wait()
|
||||||
|
raise self.TimeoutExpired(timeout_message)
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
ret = popen.wait()
|
||||||
|
elif six.PY3:
|
||||||
|
try:
|
||||||
|
ret = popen.wait(timeout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
handle_timeout()
|
||||||
|
else:
|
||||||
|
end = time.time() + timeout
|
||||||
|
|
||||||
|
resolution = min(0.1, timeout / 10)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ret = popen.poll()
|
||||||
|
if ret is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if time.time() > end:
|
||||||
|
handle_timeout()
|
||||||
|
|
||||||
|
time.sleep(resolution)
|
||||||
finally:
|
finally:
|
||||||
f1.close()
|
f1.close()
|
||||||
f2.close()
|
f2.close()
|
||||||
|
@ -1108,9 +1158,15 @@ class Testdir(object):
|
||||||
with "runpytest-" so they do not conflict with the normal numbered
|
with "runpytest-" so they do not conflict with the normal numbered
|
||||||
pytest location for temporary files and directories.
|
pytest location for temporary files and directories.
|
||||||
|
|
||||||
|
:param args: the sequence of arguments to pass to the pytest subprocess
|
||||||
|
:param timeout: the period in seconds after which to timeout and raise
|
||||||
|
:py:class:`Testdir.TimeoutExpired`
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
Returns a :py:class:`RunResult`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
p = py.path.local.make_numbered_dir(
|
p = py.path.local.make_numbered_dir(
|
||||||
prefix="runpytest-", keep=None, rootdir=self.tmpdir
|
prefix="runpytest-", keep=None, rootdir=self.tmpdir
|
||||||
)
|
)
|
||||||
|
@ -1119,7 +1175,7 @@ class Testdir(object):
|
||||||
if plugins:
|
if plugins:
|
||||||
args = ("-p", plugins[0]) + args
|
args = ("-p", plugins[0]) + args
|
||||||
args = self._getpytestargs() + args
|
args = self._getpytestargs() + args
|
||||||
return self.run(*args)
|
return self.run(*args, timeout=kwargs.get("timeout"))
|
||||||
|
|
||||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||||
"""Run pytest using pexpect.
|
"""Run pytest using pexpect.
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import py.path
|
import py.path
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import _pytest.pytester as pytester
|
import _pytest.pytester as pytester
|
||||||
from _pytest.pytester import HookRecorder
|
from _pytest.pytester import HookRecorder
|
||||||
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
|
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
|
||||||
|
@ -401,3 +402,32 @@ def test_testdir_subprocess(testdir):
|
||||||
def test_unicode_args(testdir):
|
def test_unicode_args(testdir):
|
||||||
result = testdir.runpytest("-k", u"💩")
|
result = testdir.runpytest("-k", u"💩")
|
||||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdir_run_no_timeout(testdir):
|
||||||
|
testfile = testdir.makepyfile("def test_no_timeout(): pass")
|
||||||
|
assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdir_run_with_timeout(testdir):
|
||||||
|
testfile = testdir.makepyfile("def test_no_timeout(): pass")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
result = testdir.runpytest_subprocess(testfile, timeout=120)
|
||||||
|
end = time.time()
|
||||||
|
duration = end - start
|
||||||
|
|
||||||
|
assert result.ret == EXIT_OK
|
||||||
|
assert duration < 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdir_run_timeout_expires(testdir):
|
||||||
|
testfile = testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
def test_timeout():
|
||||||
|
time.sleep(10)"""
|
||||||
|
)
|
||||||
|
with pytest.raises(testdir.TimeoutExpired):
|
||||||
|
testdir.runpytest_subprocess(testfile, timeout=1)
|
||||||
|
|
Loading…
Reference in New Issue