From 96b2ae66549b74b4f838e7f4169ae376e9528b52 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 3 Oct 2018 23:50:42 -0400 Subject: [PATCH 01/22] Initial pass at timeout for subprocessing pytest pytest-dev/pytest#4073 --- setup.py | 1 + src/_pytest/pytester.py | 36 +++++++++++++++++++++++++++++++++--- testing/test_pytester.py | 18 ++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4c12fbfcc..e65659825 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ def main(): "attrs>=17.4.0", "more-itertools>=4.0.0", "atomicwrites>=1.0", + "monotonic", ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a50999172..da0ac1868 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import codecs import gc +import monotonic import os import platform import re @@ -482,6 +483,9 @@ class Testdir(object): """ + class TimeoutExpired(Exception): + pass + def __init__(self, request, tmpdir_factory): self.request = request self._mod_collections = WeakKeyDictionary() @@ -1039,7 +1043,7 @@ class Testdir(object): return popen - def run(self, *cmdargs): + def run(self, *cmdargs, **kwargs): """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. @@ -1061,7 +1065,27 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - ret = popen.wait() + timeout = kwargs.get('timeout') + if timeout is None: + ret = popen.wait() + elif six.PY3: + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + raise self.TimeoutExpired + else: + end = monotonic.monotonic() + timeout + + while True: + ret = popen.poll() + if ret is not None: + break + + remaining = end - monotonic.monotonic() + if remaining <= 0: + raise self.TimeoutExpired() + + time.sleep(remaining * 0.9) finally: f1.close() f2.close() @@ -1119,7 +1143,13 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args) + + if "timeout" in kwargs: + timeout = {"timeout": kwargs["timeout"]} + else: + timeout = {} + + return self.run(*args, **timeout) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. diff --git a/testing/test_pytester.py b/testing/test_pytester.py index c5a64b7bd..9ddbc1380 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import os import py.path import pytest +import subprocess import sys import _pytest.pytester as pytester from _pytest.pytester import HookRecorder @@ -401,3 +402,20 @@ def test_testdir_subprocess(testdir): def test_unicode_args(testdir): result = testdir.runpytest("-k", u"💩") 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_timeout_expires(testdir): + testfile = testdir.makepyfile( + """ + import time + + def test_timeout(): + time.sleep(10)""" + ) + with pytest.raises(testdir.TimeoutExpired): + testdir.runpytest_subprocess(testfile, timeout=0.5) From 870a93c37b136d80406ce117b27b5d0f9631e1ee Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 01:02:58 -0400 Subject: [PATCH 02/22] Actually construct TimeoutExpired --- src/_pytest/pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index da0ac1868..9591f3b52 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1072,7 +1072,7 @@ class Testdir(object): try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired + raise self.TimeoutExpired() else: end = monotonic.monotonic() + timeout From fe7050ba004527edce5dabfb983d7efcf4d7dfa1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Oct 2018 18:45:30 -0300 Subject: [PATCH 03/22] Fix lint --- src/_pytest/pytester.py | 2 +- testing/test_pytester.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9591f3b52..15638e42b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1065,7 +1065,7 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - timeout = kwargs.get('timeout') + timeout = kwargs.get("timeout") if timeout is None: ret = popen.wait() elif six.PY3: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 9ddbc1380..ebc699cc5 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import os import py.path import pytest -import subprocess import sys import _pytest.pytester as pytester from _pytest.pytester import HookRecorder @@ -413,7 +412,7 @@ def test_testdir_run_timeout_expires(testdir): testfile = testdir.makepyfile( """ import time - + def test_timeout(): time.sleep(10)""" ) From d2906950ce8afe2f19b56a5046a2274a2c7d65f6 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:26:08 -0400 Subject: [PATCH 04/22] monotonic.monotonic() -> time.time() --- setup.py | 1 - src/_pytest/pytester.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e65659825..4c12fbfcc 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,6 @@ def main(): "attrs>=17.4.0", "more-itertools>=4.0.0", "atomicwrites>=1.0", - "monotonic", ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 15638e42b..1b836e546 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import codecs import gc -import monotonic import os import platform import re @@ -1074,14 +1073,14 @@ class Testdir(object): except subprocess.TimeoutExpired: raise self.TimeoutExpired() else: - end = monotonic.monotonic() + timeout + end = time.time() + timeout while True: ret = popen.poll() if ret is not None: break - remaining = end - monotonic.monotonic() + remaining = end - time.time() if remaining <= 0: raise self.TimeoutExpired() From d5e5433553a52d93654b863669593f68a6a7556e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:43:41 -0400 Subject: [PATCH 05/22] Add descriptive message for timeout --- src/_pytest/pytester.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1b836e546..7037b41c7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1065,13 +1065,19 @@ class Testdir(object): cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) timeout = kwargs.get("timeout") + + timeout_message = ( + "{seconds} second timeout expired running:" + " {command}".format(seconds=timeout, command=cmdargs) + ) + if timeout is None: ret = popen.wait() elif six.PY3: try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired() + raise self.TimeoutExpired(timeout_message) else: end = time.time() + timeout @@ -1082,7 +1088,7 @@ class Testdir(object): remaining = end - time.time() if remaining <= 0: - raise self.TimeoutExpired() + raise self.TimeoutExpired(timeout_message) time.sleep(remaining * 0.9) finally: From 33f0338eeb14e4b154122447d73f161b536d70f1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 21:57:29 -0400 Subject: [PATCH 06/22] kill and wait for subprocess before raising TimeoutExpired --- src/_pytest/pytester.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 7037b41c7..d7ad6e3b0 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1066,10 +1066,15 @@ class Testdir(object): ) timeout = kwargs.get("timeout") - timeout_message = ( - "{seconds} second timeout expired running:" - " {command}".format(seconds=timeout, command=cmdargs) - ) + def handle_timeout(): + 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() @@ -1077,7 +1082,7 @@ class Testdir(object): try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: - raise self.TimeoutExpired(timeout_message) + handle_timeout() else: end = time.time() + timeout @@ -1088,7 +1093,7 @@ class Testdir(object): remaining = end - time.time() if remaining <= 0: - raise self.TimeoutExpired(timeout_message) + handle_timeout() time.sleep(remaining * 0.9) finally: From dcd635ba0cbba40efce1947071cc0cfb7830952e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 22:11:02 -0400 Subject: [PATCH 07/22] Correct timeout to check every so often --- src/_pytest/pytester.py | 4 +++- testing/test_pytester.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d7ad6e3b0..73265f52b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1086,6 +1086,8 @@ class Testdir(object): else: end = time.time() + timeout + resolution = min(0.1, timeout / 10) + while True: ret = popen.poll() if ret is not None: @@ -1095,7 +1097,7 @@ class Testdir(object): if remaining <= 0: handle_timeout() - time.sleep(remaining * 0.9) + time.sleep(resolution) finally: f1.close() f2.close() diff --git a/testing/test_pytester.py b/testing/test_pytester.py index ebc699cc5..de5145250 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -4,6 +4,7 @@ import os import py.path import pytest import sys +import time import _pytest.pytester as pytester from _pytest.pytester import HookRecorder from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot @@ -408,6 +409,18 @@ def test_testdir_run_no_timeout(testdir): 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=10) + end = time.time() + duration = end - start + + assert result.ret == EXIT_OK + assert duration < 1 + + def test_testdir_run_timeout_expires(testdir): testfile = testdir.makepyfile( """ From 0d095fc978dde9994d81a7fb40d5bc5551d32283 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:00:17 -0400 Subject: [PATCH 08/22] Up timeout to 1 second for test --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index de5145250..caca4f687 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -430,4 +430,4 @@ def test_testdir_run_timeout_expires(testdir): time.sleep(10)""" ) with pytest.raises(testdir.TimeoutExpired): - testdir.runpytest_subprocess(testfile, timeout=0.5) + testdir.runpytest_subprocess(testfile, timeout=1) From 900cef639710682c67a4ab88bd9e7666b14a9da2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:11:26 -0400 Subject: [PATCH 09/22] Use signal.alarm() for py2 timeout --- src/_pytest/pytester.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 73265f52b..cf50f9ec9 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -2,11 +2,13 @@ from __future__ import absolute_import, division, print_function import codecs +import contextlib import gc import os import platform import re import subprocess +import signal import six import sys import time @@ -1076,6 +1078,21 @@ class Testdir(object): popen.wait() raise self.TimeoutExpired(timeout_message) + @contextlib.contextmanager + def timeout_manager(handler, timeout): + original_handler = signal.getsignal(signal.SIGALRM) + if original_handler != signal.SIG_DFL: + # TODO: use an informative exception + raise Exception() + + signal.signal(signal.SIGALRM, handler) + signal.alarm(timeout) + + yield + + signal.alarm(0) + signal.signal(signal.SIGALRM, original_handler) + if timeout is None: ret = popen.wait() elif six.PY3: @@ -1084,20 +1101,10 @@ class Testdir(object): 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 - - remaining = end - time.time() - if remaining <= 0: - handle_timeout() - - time.sleep(resolution) + with timeout_manager( + handler=lambda _1, _2: handle_timeout(), timeout=timeout + ): + ret = popen.wait() finally: f1.close() f2.close() From dd225e1b9d17212dfa079c0266336d6fb40de592 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:15:30 -0400 Subject: [PATCH 10/22] Tidy getting of timeout from kwargs --- src/_pytest/pytester.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index cf50f9ec9..3f4d43eb5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1163,12 +1163,7 @@ class Testdir(object): args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - if "timeout" in kwargs: - timeout = {"timeout": kwargs["timeout"]} - else: - timeout = {} - - return self.run(*args, **timeout) + return self.run(*args, timeout=kwargs.get("timeout")) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From dcf9eb01043fb4131cf7e8e7b402695d3f8bdecc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:25:43 -0400 Subject: [PATCH 11/22] Raise an exception on unexpected kwargs --- src/_pytest/pytester.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3f4d43eb5..f16e5f19a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -63,6 +63,11 @@ def pytest_configure(config): config.pluginmanager.register(checker) +def raise_on_kwargs(kwargs): + if kwargs: + raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs)))) + + class LsofFdLeakChecker(object): def get_open_files(self): out = self._exec_lsof() @@ -1052,6 +1057,9 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) + cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1066,7 +1074,6 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - timeout = kwargs.get("timeout") def handle_timeout(): timeout_message = ( @@ -1154,6 +1161,9 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) + p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) @@ -1163,7 +1173,7 @@ class Testdir(object): args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=kwargs.get("timeout")) + return self.run(*args, timeout=timeout) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From 5c38a5160d7878a12b459f8da6f441681ff9d59f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 4 Oct 2018 23:33:38 -0400 Subject: [PATCH 12/22] Slight diff tidy --- src/_pytest/pytester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f16e5f19a..d519162b1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1172,7 +1172,6 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=timeout) def spawn_pytest(self, string, expect_timeout=10.0): From f3a173b7369cbf5807dbc8b8c7d08189debb471f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 00:05:46 -0400 Subject: [PATCH 13/22] Revert "Use signal.alarm() for py2 timeout" This reverts commit 900cef639710682c67a4ab88bd9e7666b14a9da2. --- src/_pytest/pytester.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d519162b1..a20c281e1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -2,13 +2,11 @@ from __future__ import absolute_import, division, print_function import codecs -import contextlib import gc import os import platform import re import subprocess -import signal import six import sys import time @@ -1085,21 +1083,6 @@ class Testdir(object): popen.wait() raise self.TimeoutExpired(timeout_message) - @contextlib.contextmanager - def timeout_manager(handler, timeout): - original_handler = signal.getsignal(signal.SIGALRM) - if original_handler != signal.SIG_DFL: - # TODO: use an informative exception - raise Exception() - - signal.signal(signal.SIGALRM, handler) - signal.alarm(timeout) - - yield - - signal.alarm(0) - signal.signal(signal.SIGALRM, original_handler) - if timeout is None: ret = popen.wait() elif six.PY3: @@ -1108,10 +1091,20 @@ class Testdir(object): except subprocess.TimeoutExpired: handle_timeout() else: - with timeout_manager( - handler=lambda _1, _2: handle_timeout(), timeout=timeout - ): - ret = popen.wait() + end = time.time() + timeout + + resolution = min(0.1, timeout / 10) + + while True: + ret = popen.poll() + if ret is not None: + break + + remaining = end - time.time() + if remaining <= 0: + handle_timeout() + + time.sleep(resolution) finally: f1.close() f2.close() From 42422a7f629265bdcd1afb76a44911cb4673b95e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 00:30:25 -0400 Subject: [PATCH 14/22] Throw away arbitrary args to runpytest_subprocess() --- src/_pytest/pytester.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a20c281e1..8c283431a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1154,9 +1154,6 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ - timeout = kwargs.pop("timeout", None) - raise_on_kwargs(kwargs) - p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) @@ -1165,7 +1162,7 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=timeout) + return self.run(*args, timeout=kwargs.get("timeout")) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From 8e0e862c84f44dfa3e429275b3a3a5d29b5836a8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 01:38:01 -0400 Subject: [PATCH 15/22] Stretch out the time assertion for slow AppVeyor --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index caca4f687..42f245524 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -418,7 +418,7 @@ def test_testdir_run_with_timeout(testdir): duration = end - start assert result.ret == EXIT_OK - assert duration < 1 + assert duration < 5 def test_testdir_run_timeout_expires(testdir): From ee64f1fb9f00a82e91055a3e70febfff643f016e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 09:02:53 -0400 Subject: [PATCH 16/22] Add changelog file for 4073 --- changelog/4073.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4073.feature.rst diff --git a/changelog/4073.feature.rst b/changelog/4073.feature.rst new file mode 100644 index 000000000..7ee46c60f --- /dev/null +++ b/changelog/4073.feature.rst @@ -0,0 +1 @@ +Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. \ No newline at end of file From ed5556bdac8b864937d5df2ea690573df27a4f83 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:00:52 -0400 Subject: [PATCH 17/22] Add to docstrings --- src/_pytest/pytester.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8c283431a..603de4b65 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1052,6 +1052,10 @@ class Testdir(object): 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`. """ @@ -1151,6 +1155,10 @@ class Testdir(object): with "runpytest-" so they do not conflict with the normal numbered 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`. """ From 20902deb75d7d84b24c9f640439a45625aaac0d6 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:02:31 -0400 Subject: [PATCH 18/22] Add Kyle Altendorf to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index d99261478..988d0e5fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -121,6 +121,7 @@ Katerina Koukiou Kevin Cox Kodi B. Arfer Kostis Anagnostopoulos +Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov From 5ecbb0acba66e56a88805bde53515b6dda1c5452 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 10:22:07 -0400 Subject: [PATCH 19/22] Correct new changelog to have newline at end --- changelog/4073.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4073.feature.rst b/changelog/4073.feature.rst index 7ee46c60f..5b0ed5927 100644 --- a/changelog/4073.feature.rst +++ b/changelog/4073.feature.rst @@ -1 +1 @@ -Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. \ No newline at end of file +Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. From 4b36f9aa64b90fde67db88d39c63d6c5e6116b9b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 5 Oct 2018 16:46:24 -0400 Subject: [PATCH 20/22] Tidy timeout checking --- src/_pytest/pytester.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 603de4b65..8e00f0ebf 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1104,8 +1104,7 @@ class Testdir(object): if ret is not None: break - remaining = end - time.time() - if remaining <= 0: + if time.time() > end: handle_timeout() time.sleep(resolution) From ccaec8d360e17e041a8f95e2a0942859615feae3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 6 Oct 2018 21:56:59 -0400 Subject: [PATCH 21/22] __tracebackhide__ = True --- src/_pytest/pytester.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8e00f0ebf..31d9f7860 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1059,6 +1059,8 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + timeout = kwargs.pop("timeout", None) raise_on_kwargs(kwargs) @@ -1078,6 +1080,8 @@ class Testdir(object): ) def handle_timeout(): + __tracebackhide__ = True + timeout_message = ( "{seconds} second timeout expired running:" " {command}".format(seconds=timeout, command=cmdargs) @@ -1161,6 +1165,8 @@ class Testdir(object): Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) From 48dcc6727421bed1f9fe335ba53fae775a8c2fcc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 6 Oct 2018 22:02:33 -0400 Subject: [PATCH 22/22] Increase timeout in test_testdir_run_with_timeout to decrease false failures --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 42f245524..1c456e167 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -413,7 +413,7 @@ def test_testdir_run_with_timeout(testdir): testfile = testdir.makepyfile("def test_no_timeout(): pass") start = time.time() - result = testdir.runpytest_subprocess(testfile, timeout=10) + result = testdir.runpytest_subprocess(testfile, timeout=120) end = time.time() duration = end - start