[Piglit] [PATCH v2 5/6] framework/test/base.py: Split timeout code into a mixin.

baker.dylan.c at gmail.com baker.dylan.c at gmail.com
Fri Oct 23 18:17:23 PDT 2015


From: Dylan Baker <baker.dylan.c at gmail.com>

This splits the timeout code into a mixin class which is guarded by a
check to ensure that the os is not windows (piglit supports only modern
*nix systems and windows, so checks for old mac os and similar don't
make sense).

It then provides this mixin to tests/igt.py so that the behavior of that
module doesn't change.
---
 framework/test/base.py        | 203 ++++++++++++++++++++++++++----------------
 framework/tests/base_tests.py |  17 ++--
 tests/igt.py                  |   5 +-
 3 files changed, 140 insertions(+), 85 deletions(-)

diff --git a/framework/test/base.py b/framework/test/base.py
index c111f13..4312cbf 100644
--- a/framework/test/base.py
+++ b/framework/test/base.py
@@ -62,56 +62,6 @@ class TestRunError(exceptions.PiglitException):
         self.status = status
 
 
-class ProcessTimeout(threading.Thread):
-    """ Timeout class for test processes
-
-    This class is for terminating tests that run for longer than a certain
-    amount of time. Create an instance by passing it a timeout in seconds
-    and the Popen object that is running your test. Then call the start
-    method (inherited from Thread) to start the timer. The Popen object is
-    killed if the timeout is reached and it has not completed. Wait for the
-    outcome by calling the join() method from the parent.
-
-    """
-
-    def __init__(self, timeout, proc):
-        threading.Thread.__init__(self)
-        self.proc = proc
-        self.timeout = timeout
-        self.status = 0
-
-    def run(self):
-        start_time = datetime.now()
-        delta = 0
-
-        # poll() returns the returncode attribute, which is either the return
-        # code of the child process (which could be zero), or None if the
-        # process has not yet terminated.
-
-        while delta < self.timeout and self.proc.poll() is None:
-            time.sleep(1)
-            delta = (datetime.now() - start_time).total_seconds()
-
-        # if the test is not finished after timeout, first try to terminate it
-        # and if that fails, send SIGKILL to all processes in the test's
-        # process group
-
-        if self.proc.poll() is None:
-            self.status = 1
-            self.proc.terminate()
-            time.sleep(5)
-
-        if self.proc.poll() is None:
-            self.status = 2
-            if hasattr(os, 'killpg'):
-                os.killpg(self.proc.pid, signal.SIGKILL)
-            self.proc.wait()
-
-    def join(self):
-        threading.Thread.join(self)
-        return self.status
-
-
 def _is_crash_returncode(returncode):
     """Determine whether the given process return code correspond to a
     crash.
@@ -147,9 +97,7 @@ class Test(object):
     """
     OPTS = Options()
     __metaclass__ = abc.ABCMeta
-    __slots__ = ['run_concurrent', 'env', 'result', 'cwd', '_command',
-                 '__proc_timeout']
-    timeout = 0
+    __slots__ = ['run_concurrent', 'env', 'result', 'cwd', '_command']
 
     def __init__(self, command, run_concurrent=False):
         assert isinstance(command, list), command
@@ -205,11 +153,7 @@ class Test(object):
         """Convert the raw output of the test into a form piglit understands.
         """
         if _is_crash_returncode(self.result.returncode):
-            # check if the process was terminated by the timeout
-            if self.timeout > 0 and self.__proc_timeout.join() > 0:
-                self.result.result = 'timeout'
-            else:
-                self.result.result = 'crash'
+            self.result.result = 'crash'
         elif self.result.returncode != 0 and self.result.result == 'pass':
             self.result.result = 'warn'
 
@@ -255,10 +199,6 @@ class Test(object):
         """
         pass
 
-    def __set_process_group(self):
-        if hasattr(os, 'setpgrp'):
-            os.setpgrp()
-
     def _make_popen_args(self):
         """Create arguemnts for popen."""
         # Setup the environment for the test. Environment variables are taken
@@ -285,10 +225,6 @@ class Test(object):
             'universal_newlines': True,
         }
 
-        # preexec_fn is not supported on Windows platforms
-        if sys.platform != 'win32':
-            popen_args['preexec_fn'] = self.__set_process_group
-
         return popen_args
 
     def _run_command(self, communicate_args=None):
@@ -303,15 +239,7 @@ class Test(object):
         """
         try:
             proc = subprocess.Popen(self.command,
-                                    **self._make_popen_args)
-            # create a ProcessTimeout object to watch out for test hang if the
-            # process is still going after the timeout, then it will be killed
-            # forcing the communicate function (which is a blocking call) to
-            # return
-            if self.timeout > 0:
-                self.__proc_timeout = ProcessTimeout(self.timeout, proc)
-                self.__proc_timeout.start()
-
+                                    **self._make_popen_args())
             out, err = proc.communicate(**(communicate_args or {}))
             returncode = proc.returncode
         except OSError as e:
@@ -402,3 +330,128 @@ class ValgrindMixin(object):
             else:
                 # Test passed but has valgrind errors.
                 self.result.result = 'fail'
+
+
+if not sys.platform.startswith('win'):
+    class ProcessTimeout(threading.Thread):
+        """ Timeout class for test processes
+
+        This class is for terminating tests that run for longer than a certain
+        amount of time. Create an instance by passing it a timeout in seconds
+        and the Popen object that is running your test. Then call the start
+        method (inherited from Thread) to start the timer. The Popen object is
+        killed if the timeout is reached and it has not completed. Wait for the
+        outcome by calling the join() method from the parent.
+
+        """
+
+        def __init__(self, timeout, proc):
+            threading.Thread.__init__(self)
+            self.proc = proc
+            self.timeout = timeout
+            self.status = 0
+
+        def run(self):
+            start_time = datetime.now()
+            delta = 0
+
+            # poll() returns the returncode attribute, which is either the return
+            # code of the child process (which could be zero), or None if the
+            # process has not yet terminated.
+
+            while delta < self.timeout and self.proc.poll() is None:
+                time.sleep(1)
+                delta = (datetime.now() - start_time).total_seconds()
+
+            # if the test is not finished after timeout, first try to terminate it
+            # and if that fails, send SIGKILL to all processes in the test's
+            # process group
+
+            if self.proc.poll() is None:
+                self.status = 1
+                self.proc.terminate()
+                time.sleep(5)
+
+            if self.proc.poll() is None:
+                self.status = 2
+                if hasattr(os, 'killpg'):
+                    os.killpg(self.proc.pid, signal.SIGKILL)
+                self.proc.wait()
+
+        def join(self):
+            threading.Thread.join(self)
+            return self.status
+
+
+    class TimeoutMixin(object):  # pylint: disable=too-few-public-methods
+        """Mixin poviding timeout on *nix platforms
+
+        This provides a timeout mechanism for piglit tests, with a couple of
+        caveats. First, it only works on *nix platforms. Second, it only works
+        when running non-concurrently.
+
+        It properly handles forking tests.
+
+        This mixin needs to be applied below every other possible Mixin and
+        Test class except the ValgrindMixin, so that it's interpret_result
+        works correctly.
+
+        """
+        timeout = 0
+
+        def __init__(self, *args, **kwargs):
+            super(TimeoutMixin, self).__init__(*args, **kwargs)
+            self.__proc_timeout = None
+
+        # This level of duplication sucks, but there doesn't seem to be a good
+        # way around it without introducing even worse hacks.
+        def _run_command(self, communicate_args=None):
+            """Run the test command and get the result
+
+            This method sets environment options, then runs the executable. If the
+            executable isn't found it sets the result to skip.
+
+            This will populate the following popen arguments automatically: stdout,
+            stderr, cwd, universal newlines.
+
+            """
+            try:
+                proc = subprocess.Popen(self.command,
+                                        **self._make_popen_args())
+                # create a ProcessTimeout object to watch out for test hang if
+                # the process is still going after the timeout, then it will be
+                # killed forcing the communicate function (which is a blocking
+                # call) to return
+                if self.timeout > 0:
+                    self.__proc_timeout = ProcessTimeout(self.timeout, proc)
+                    self.__proc_timeout.start()
+
+                out, err = proc.communicate(**(communicate_args or {}))
+                returncode = proc.returncode
+            except OSError as e:
+                # Different sets of tests get built under different build
+                # configurations.  If a developer chooses to not build a test,
+                # Piglit should not report that test as having failed.
+                if e.errno == errno.ENOENT:
+                    raise TestRunError("Test executable not found.\n", 'skip')
+                else:
+                    raise e
+
+            # The setter handles the bytes/unicode conversion
+            self.result.out = out
+            self.result.err = err
+            self.result.returncode = returncode
+
+        def _make_popen_args(self):
+            popen_args = super(TimeoutMixin, self)._make_popen_args()
+            popen_args['preexec_fn'] = os.setpgrp
+
+            return popen_args
+
+        def interpret_result(self):
+            if (_is_crash_returncode(self.result.returncode) and
+                    self.timeout > 0 and self.__proc_timeout.join() > 0):
+                self.result.result = 'timeout'
+            else:
+                # Only call super if we don't set the result to timeout.
+                super(TimeoutMixin, self).interpret_result()
diff --git a/framework/tests/base_tests.py b/framework/tests/base_tests.py
index 539dff0..6e5a734 100644
--- a/framework/tests/base_tests.py
+++ b/framework/tests/base_tests.py
@@ -27,7 +27,7 @@ from nose.plugins.attrib import attr
 
 import framework.tests.utils as utils
 from framework.test.base import (
-    Test, WindowResizeMixin, ValgrindMixin, TestRunError
+    Test, WindowResizeMixin, ValgrindMixin, TimeoutMixin, TestRunError
 )
 from framework.tests.status_tests import PROBLEMS, STATUSES
 
@@ -64,8 +64,10 @@ def test_run_return_early():
 def test_timeout():
     """test.base.Test.run(): Sets status to 'timeout' when timeout exceeded"""
     utils.binary_check('sleep', 1)
+    class _Test(TimeoutMixin, Test):
+        pass
 
-    test = utils.Test(['sleep', '60'])
+    test = _Test(['sleep', '60'])
     test.timeout = 1
     test.run()
     nt.eq_(test.result.result, 'timeout')
@@ -76,13 +78,12 @@ def test_timeout_pass():
     """test.base.Test.run(): Doesn't change status when timeout not exceeded
     """
     utils.binary_check('true')
+    class _Test(TimeoutMixin, Test):
+        def interpret_result(self):
+            self.result.result = 'pass'
+            super(_Test, self).interpret_result()
 
-    def helper():
-        if (test.result.returncode == 0):
-            test.result.result = "pass"
-
-    test = TestTest(['true'])
-    test.test_interpret_result = helper
+    test = _Test(['true'])
     test.timeout = 1
     test.run()
     nt.eq_(test.result.result, 'pass')
diff --git a/tests/igt.py b/tests/igt.py
index 7ba7842..aad6f73 100644
--- a/tests/igt.py
+++ b/tests/igt.py
@@ -38,7 +38,8 @@ import re
 import subprocess
 
 from framework import grouptools, exceptions, core
-from framework.profile import TestProfile, Test
+from framework.profile import TestProfile
+from framework.test.base import Test, TimeoutMixin
 
 __all__ = ['profile']
 
@@ -99,7 +100,7 @@ class IGTTestProfile(TestProfile):
 profile = IGTTestProfile()  # pylint: disable=invalid-name
 
 
-class IGTTest(Test):
+class IGTTest(TimeoutMixin, Test):
     """Test class for running libdrm."""
     def __init__(self, binary, arguments=None):
         if arguments is None:
-- 
2.6.1



More information about the Piglit mailing list