From 96b2ae66549b74b4f838e7f4169ae376e9528b52 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 3 Oct 2018 23:50:42 -0400 Subject: [PATCH] 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)