<div dir="ltr">Bump.<br></div><div class="gmail_extra"><br><div class="gmail_quote">On Wed, Dec 16, 2015 at 4:52 PM,  <span dir="ltr"><<a href="mailto:baker.dylan.c@gmail.com" target="_blank">baker.dylan.c@gmail.com</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">From: Dylan Baker <<a href="mailto:baker.dylan.c@gmail.com">baker.dylan.c@gmail.com</a>><br>
<br>
Subprocess32 provides a backport of python 3.2's subprocess module,<br>
which has a timeout parameter for Popen.communicate. When the timeout<br>
<span class="">runs out then an exception is raised, and when that exception is caught<br>
we can kill the process.<br>
<br>
This is fairly similar to the way the current timeout mechanism works,<br>
except that the current mechanism is not thread safe. Since one of the<br>
major features of piglit is that it offer's processes isolated<br>
concurrency of tests, it makes sense to make the effort to provide a<br>
</span>timeout mechanism that supports concurrency. Unfortunately there isn't a<br>
good cross platform mechanism for this in python 2.x, even with<br>
subprocess 32 only *nix systems are supported, not windows.<br>
<br>
The big advantage of this is it allows consistent behavior between<br>
python 2.x and 3.x as we look toward the future and the possibility of<br>
using a hybrid approach to transition to python 3.x while maintaining<br>
2.x support until it's not needed.<br>
<span class=""><br>
This patch look pretty substantial. It's not as big as it looks, since<br>
it adds some fairly big tests.<br>
<br>
v3: - Wait before terminating process<br>
    - use os.getpgid instead of os.getsid<br>
    - use subprocess32 for accel profile in tox<br>
    - Add warning when using the dummy version of TimeoutExpired<br>
    - mark more unittests as slow and add timeouts to them<br>
    - Remove the use of a Mixin, timeouts are an important enough<br>
      feature every test suite should have the option to use them, and<br>
      by not using a mixin there is no need for the ugly interface.<br>
v4: - Fix rebasing artifacts, including many changes in v3 that appeared<br>
      to be unrelated<br>
    - do not wait to terminate in the timeout case<br>
    - Update timeout message to be clearer<br>
    - Fix test descriptions to not include TimeoutMixin, which was<br>
      removed in a previous version of this patch<br>
    - Rebase on latest master<br>
</span>v5: - Set base timeout to None instead of 0. The default argument for<br>
      timeout is None, so setting it to 0 makes the timeout period 0<br>
      seconds instead of the intended unlimited. This broke test classes<br>
      that didn't implement a timeout parameter.<br>
<span class=""><br>
Signed-off-by: Dylan Baker <<a href="mailto:dylanx.c.baker@intel.com">dylanx.c.baker@intel.com</a>><br>
---<br>
</span> framework/test/base.py        | 144 ++++++++++++++++++------------------------<br>
 framework/tests/base_tests.py | 117 +++++++++++++++++++++++++++++++++-<br>
 tox.ini                       |   2 +<br>
 3 files changed, 179 insertions(+), 84 deletions(-)<br>
<br>
diff --git a/framework/test/base.py b/framework/test/base.py<br>
index 4232e6c..5e30ede 100644<br>
<div><div class="h5">--- a/framework/test/base.py<br>
+++ b/framework/test/base.py<br>
@@ -25,16 +25,43 @@<br>
 from __future__ import print_function, absolute_import<br>
 import errno<br>
 import os<br>
-import subprocess<br>
 import time<br>
 import sys<br>
 import traceback<br>
-from datetime import datetime<br>
-import threading<br>
-import signal<br>
 import itertools<br>
 import abc<br>
 import copy<br>
+import signal<br>
+import warnings<br>
+<br>
+try:<br>
+    # subprocess32 only supports *nix systems, this is important because<br>
+    # "start_new_session" is not a valid argument on windows<br>
+<br>
+    import subprocess32 as subprocess<br>
+    _EXTRA_POPEN_ARGS = {'start_new_session': True}<br>
+except ImportError:<br>
+    # If there is no timeout support, fake it. Add a TimeoutExpired exception<br>
+    # and a Popen that accepts a timeout parameter (and ignores it), then<br>
+    # shadow the actual Popen with this wrapper.<br>
+<br>
+    import subprocess<br>
+<br>
+    class TimeoutExpired(Exception):<br>
+        pass<br>
+<br>
+    class Popen(subprocess.Popen):<br>
+        """Sublcass of Popen that accepts and ignores a timeout argument."""<br>
+        def communicate(self, *args, **kwargs):<br>
+            if 'timeout' in kwargs:<br>
+                del kwargs['timeout']<br>
+            return super(Popen, self).communicate(*args, **kwargs)<br>
+<br>
+    subprocess.TimeoutExpired = TimeoutExpired<br>
+    subprocess.Popen = Popen<br>
+    _EXTRA_POPEN_ARGS = {}<br>
+<br>
+    warnings.warn('Timeouts are not available')<br>
<br>
 from framework import exceptions, options<br>
 from framework.results import TestResult<br>
@@ -62,56 +89,6 @@ class TestRunError(exceptions.PiglitException):<br>
         self.status = status<br>
<br>
<br>
-class ProcessTimeout(threading.Thread):<br>
-    """ Timeout class for test processes<br>
-<br>
-    This class is for terminating tests that run for longer than a certain<br>
-    amount of time. Create an instance by passing it a timeout in seconds<br>
-    and the Popen object that is running your test. Then call the start<br>
-    method (inherited from Thread) to start the timer. The Popen object is<br>
-    killed if the timeout is reached and it has not completed. Wait for the<br>
-    outcome by calling the join() method from the parent.<br>
-<br>
-    """<br>
-<br>
-    def __init__(self, timeout, proc):<br>
-        threading.Thread.__init__(self)<br>
-        self.proc = proc<br>
-        self.timeout = timeout<br>
-        self.status = 0<br>
-<br>
-    def run(self):<br>
-        start_time = datetime.now()<br>
-        delta = 0<br>
-<br>
-        # poll() returns the returncode attribute, which is either the return<br>
-        # code of the child process (which could be zero), or None if the<br>
-        # process has not yet terminated.<br>
-<br>
-        while delta < self.timeout and self.proc.poll() is None:<br>
-            time.sleep(1)<br>
-            delta = (datetime.now() - start_time).total_seconds()<br>
-<br>
-        # if the test is not finished after timeout, first try to terminate it<br>
-        # and if that fails, send SIGKILL to all processes in the test's<br>
-        # process group<br>
-<br>
-        if self.proc.poll() is None:<br>
-            self.status = 1<br>
-            self.proc.terminate()<br>
-            time.sleep(5)<br>
-<br>
-        if self.proc.poll() is None:<br>
-            self.status = 2<br>
-            if hasattr(os, 'killpg'):<br>
-                os.killpg(self.proc.pid, signal.SIGKILL)<br>
-            self.proc.wait()<br>
-<br>
-    def join(self):<br>
-        threading.Thread.join(self)<br>
-        return self.status<br>
-<br>
-<br>
 def is_crash_returncode(returncode):<br>
     """Determine whether the given process return code correspond to a<br>
     crash.<br>
@@ -147,10 +124,10 @@ class Test(object):<br>
     """<br>
     __metaclass__ = abc.ABCMeta<br>
     __slots__ = ['run_concurrent', 'env', 'result', 'cwd', '_command',<br>
-                 '__proc_timeout']<br>
</div></div>-    timeout = 0<br>
+                 'timeout']<br>
+    timeout = None<br>
<span class=""><br>
-    def __init__(self, command, run_concurrent=False):<br>
+    def __init__(self, command, run_concurrent=False, timeout=None):<br>
         assert isinstance(command, list), command<br>
<br>
         self.run_concurrent = run_concurrent<br>
@@ -158,7 +135,9 @@ class Test(object):<br>
         self.env = {}<br>
         self.result = TestResult()<br>
         self.cwd = None<br>
-        self.__proc_timeout = None<br>
+        if timeout is not None:<br>
+            assert isinstance(timeout, int)<br>
+            self.timeout = timeout<br>
<br>
     def execute(self, path, log, dmesg):<br>
         """ Run a test<br>
</span>@@ -205,11 +184,7 @@ class Test(object):<br>
<span class="">         """Convert the raw output of the test into a form piglit understands.<br>
         """<br>
         if is_crash_returncode(self.result.returncode):<br>
-            # check if the process was terminated by the timeout<br>
-            if self.timeout > 0 and self.__proc_timeout.join() > 0:<br>
-                self.result.result = 'timeout'<br>
-            else:<br>
-                self.result.result = 'crash'<br>
+            self.result.result = 'crash'<br>
</span>         elif self.result.returncode != 0:<br>
             if self.result.result == 'pass':<br>
                 self.result.result = 'warn'<br>
@@ -258,10 +233,6 @@ class Test(object):<br>
<span class="">         """<br>
         pass<br>
<br>
-    def __set_process_group(self):<br>
-        if hasattr(os, 'setpgrp'):<br>
-            os.setpgrp()<br>
-<br>
     def _run_command(self):<br>
         """ Run the test command and get the result<br>
<br>
</span>@@ -288,11 +259,6 @@ class Test(object):<br>
<span class="">                                           self.env.iteritems()):<br>
             fullenv[key] = str(value)<br>
<br>
-        # preexec_fn is not supported on Windows platforms<br>
-        if sys.platform == 'win32':<br>
-            preexec_fn = None<br>
-        else:<br>
-            preexec_fn = self.__set_process_group<br>
<br>
         try:<br>
             proc = subprocess.Popen(self.command,<br>
</span>@@ -301,16 +267,9 @@ class Test(object):<br>
<span class="">                                     cwd=self.cwd,<br>
                                     env=fullenv,<br>
                                     universal_newlines=True,<br>
-                                    preexec_fn=preexec_fn)<br>
-            # create a ProcessTimeout object to watch out for test hang if the<br>
-            # process is still going after the timeout, then it will be killed<br>
-            # forcing the communicate function (which is a blocking call) to<br>
-            # return<br>
-            if self.timeout > 0:<br>
-                self.__proc_timeout = ProcessTimeout(self.timeout, proc)<br>
-                self.__proc_timeout.start()<br>
-<br>
-            out, err = proc.communicate()<br>
+                                    **_EXTRA_POPEN_ARGS)<br>
+<br>
+            out, err = proc.communicate(timeout=self.timeout)<br>
             returncode = proc.returncode<br>
         except OSError as e:<br>
             # Different sets of tests get built under different build<br>
</span>@@ -320,6 +279,27 @@ class Test(object):<br>
<span class="">                 raise TestRunError("Test executable not found.\n", 'skip')<br>
             else:<br>
                 raise e<br>
+        except subprocess.TimeoutExpired:<br>
</span>+            # This can only be reached if subprocess32 is present, since<br>
+            # TimeoutExpired is never raised by the fallback code.<br>
+<br>
+            proc.terminate()<br>
+<br>
+            # XXX: A number of the os calls in this block don't work on windows,<br>
+            # when porting to python 3 this will likely require an<br>
+            # "if windows/else" block<br>
<span class="">+            if proc.poll() is None:<br>
+                time.sleep(3)<br>
+                os.killpg(os.getpgid(proc.pid), signal.SIGKILL)<br>
+<br>
</span>+            # Since the process isn't running it's safe to get any remaining<br>
+            # stdout/stderr values out and store them.<br>
<span class="">+            self.result.out, self.result.err = proc.communicate()<br>
+<br>
+            raise TestRunError(<br>
+                'Test run time exceeded timeout value ({} seconds)\n'.format(<br>
+                    self.timeout),<br>
+                'timeout')<br>
<br>
         # The setter handles the bytes/unicode conversion<br>
         self.result.out = out<br>
diff --git a/framework/tests/base_tests.py b/framework/tests/base_tests.py<br>
</span>index 453ee50..00ba61f 100644<br>
<div><div class="h5">--- a/framework/tests/base_tests.py<br>
+++ b/framework/tests/base_tests.py<br>
@@ -21,11 +21,19 @@<br>
 """ Tests for the exectest module """<br>
<br>
 from __future__ import print_function, absolute_import<br>
+import tempfile<br>
+import textwrap<br>
+import os<br>
<br>
 import mock<br>
 import nose.tools as nt<br>
 from nose.plugins.attrib import attr<br>
<br>
+try:<br>
+    import psutil<br>
+except ImportError:<br>
+    pass<br>
+<br>
 import framework.tests.utils as utils<br>
 from framework.test.base import (<br>
     Test,<br>
@@ -71,8 +79,111 @@ def test_run_return_early():<br>
<br>
<br>
 @attr('slow')<br>
+@nt.timed(6)<br>
+def test_timeout_kill_children():<br>
+    """test.base.Test: kill children if terminate fails<br>
+<br>
+    This creates a process that forks multiple times, and then checks that the<br>
+    children have been killed.<br>
+<br>
+    This test could leave processes running if it fails.<br>
+<br>
+    """<br>
+    utils.module_check('subprocess32')<br>
+    utils.module_check('psutil')<br>
+<br>
+    import subprocess32  # pylint: disable=import-error<br>
+<br>
+    class PopenProxy(object):<br>
+        """An object that proxies Popen, and saves the Popen instance as an<br>
+        attribute.<br>
+<br>
+        This is useful for testing the Popen instance.<br>
+<br>
+        """<br>
+        def __init__(self):<br>
+            self.popen = None<br>
+<br>
+        def __call__(self, *args, **kwargs):<br>
+            self.popen = subprocess32.Popen(*args, **kwargs)<br>
+<br>
+            # if commuincate cis called successfully then the proc will be<br>
+            # reset to None, whic will make the test fail.<br>
+            self.popen.communicate = mock.Mock(return_value=('out', 'err'))<br>
+<br>
+            return self.popen<br>
+<br>
+    with tempfile.NamedTemporaryFile() as f:<br>
+        # Create a file that will be executed as a python script<br>
+        # Create a process with two subproccesses (not threads) that will run<br>
+        # for a long time.<br>
+        f.write(textwrap.dedent("""\<br>
+            import time<br>
+            from multiprocessing import Process<br>
+<br>
+            def p():<br>
+                for _ in range(100):<br>
+                    time.sleep(1)<br>
+<br>
+            a = Process(target=p)<br>
+            b = Process(target=p)<br>
+            a.start()<br>
+            b.start()<br>
+            a.join()<br>
+            b.join()<br>
+        """))<br>
+        f.seek(0)  # we'll need to read the file back<br>
+<br>
+        # Create an object that will return a popen object, but also store it<br>
+        # so we can access it later<br>
+        proxy = PopenProxy()<br>
+<br>
+        test = TimeoutTest(['python', <a href="http://f.name" rel="noreferrer" target="_blank">f.name</a>])<br>
+        test.timeout = 1<br>
+<br>
+        # mock out subprocess.Popen with our proxy object<br>
+        with mock.patch('framework.test.base.subprocess') as mock_subp:<br>
+            mock_subp.Popen = proxy<br>
+            mock_subp.TimeoutExpired = subprocess32.TimeoutExpired<br>
+            test.run()<br>
+<br>
+        # Check to see if the Popen has children, even after it should have<br>
+        # recieved a TimeoutExpired.<br>
+        proc = psutil.Process(os.getsid(proxy.popen.pid))<br>
+        children = proc.children(recursive=True)<br>
+<br>
+        if children:<br>
+            # If there are still running children attempt to clean them up,<br>
+            # starting with the final generation and working back to the first<br>
+            for child in reversed(children):<br>
+                child.kill()<br>
+<br>
+            raise utils.TestFailure(<br>
+                'Test process had children when it should not')<br>
+<br>
+<br>
+@attr('slow')<br>
+@nt.timed(6)<br>
 def test_timeout():<br>
-    """test.base.Test.run(): Sets status to 'timeout' when timeout exceeded"""<br>
+    """test.base.Test: Stops running test after timeout expires<br>
+<br>
+    This is a little bit of extra time here, but without a sleep of 60 seconds<br>
+    if the test runs 5 seconds it's run too long<br>
+<br>
+    """<br>
+    utils.module_check('subprocess32')<br>
+    utils.binary_check('sleep', 1)<br>
+<br>
+    test = TimeoutTest(['sleep', '60'])<br>
+    test.timeout = 1<br>
+    test.run()<br>
+<br>
+<br>
+@attr('slow')<br>
+@nt.timed(6)<br>
+def test_timeout_timeout():<br>
+    """test.base.Test: Sets status to 'timeout' when timeout exceeded"""<br>
+    utils.module_check('subprocess32')<br>
     utils.binary_check('sleep', 1)<br>
<br>
     test = TimeoutTest(['sleep', '60'])<br>
@@ -81,9 +192,11 @@ def test_timeout():<br>
     nt.eq_(test.result.result, 'timeout')<br>
<br>
<br>
+@nt.timed(2)<br>
 def test_timeout_pass():<br>
-    """test.base.Test.run(): Doesn't change status when timeout not exceeded<br>
+    """test.base.Test: Doesn't change status when timeout not exceeded<br>
     """<br>
+    utils.module_check('subprocess32')<br>
     utils.binary_check('true')<br>
<br>
     test = TimeoutTest(['true'])<br>
diff --git a/tox.ini b/tox.ini<br>
index cea28ac..3d284be 100644<br>
--- a/tox.ini<br>
+++ b/tox.ini<br>
@@ -26,7 +26,9 @@ deps =<br>
     nose<br>
     coverage<br>
     mock<br>
+    psutil<br>
     simplejson<br>
     lxml<br>
     backports.lzma<br>
+    subprocess32<br>
 commands = nosetests framework/tests --with-cover --cover-package=framework --cover-tests<br>
--<br>
</div></div>2.6.4<br>
<br>
</blockquote></div><br></div>