[Piglit] [PATCH] framework: add a generic timeout mechanism

Thomas Wood thomas.wood at intel.com
Tue Sep 9 08:15:12 PDT 2014


The timeout mechanism within igt.py can no longer be used since
get_command_result was renamed and made private in commit 5e9dd69
(exectest.py: rename get_command_result to __run_command). Therefore,
move the timeout mechanism into exectest.py, allowing all test profiles
to use it if needed and also avoiding code duplication between igt.py
and exectest.py.

Cc: Dylan Baker <baker.dylan.c at gmail.com>
Cc: Chad Versace <chad.versace at linux.intel.com>
Signed-off-by: Thomas Wood <thomas.wood at intel.com>
---
 framework/exectest.py | 67 +++++++++++++++++++++++++++++++++++--
 framework/profile.py  |  3 +-
 tests/igt.py          | 93 +--------------------------------------------------
 3 files changed, 68 insertions(+), 95 deletions(-)

diff --git a/framework/exectest.py b/framework/exectest.py
index 3ed9b6f..90920c6 100644
--- a/framework/exectest.py
+++ b/framework/exectest.py
@@ -29,6 +29,9 @@ import shlex
 import time
 import sys
 import traceback
+from datetime import datetime
+import threading
+import signal
 import itertools
 import abc
 try:
@@ -50,6 +53,45 @@ else:
     TEST_BIN_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__),
                                                  '../bin'))
 
+# This class is for timing out tests that hang. 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.
+
+class ProcessTimeout(threading.Thread):
+    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
+        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 Test(object):
     """ Abstract base class for Test classes
@@ -82,12 +124,14 @@ class Test(object):
         self.env = {}
         self.result = TestResult({'result': 'fail'})
         self.cwd = None
+        self.timeout = 0
+        self.__proc_timeout = None
 
         # This is a hook for doing some testing on execute right before
         # self.run is called.
         self._test_hook_execute_run = lambda: None
 
-    def execute(self, path, log, dmesg):
+    def execute(self, path, log, dmesg, timeout):
         """ Run a test
 
         Run a test, but with features. This times the test, uses dmesg checking
@@ -99,6 +143,7 @@ class Test(object):
         dmesg -- a dmesg.BaseDmesg derived class
 
         """
+        self.timeout = timeout
         log_current = log.pre_log(path if self.OPTS.verbose else None)
 
         # Run the test
@@ -199,6 +244,11 @@ class Test(object):
                 # Test passed but has valgrind errors.
                 self.result['result'] = 'fail'
 
+        if self.timeout > 0:
+            # check for timeout
+            if self.__proc_timeout.join() > 0:
+                self.result['result'] = 'timeout'
+
     def is_skip(self):
         """ Application specific check for skip
 
@@ -208,6 +258,10 @@ class Test(object):
         """
         return False
 
+    def __set_process_group(self):
+        if hasattr(os, 'setpgrp'):
+            os.setpgrp()
+
     def __run_command(self):
         """ Run the test command and get the result
 
@@ -240,7 +294,16 @@ class Test(object):
                                     stderr=subprocess.PIPE,
                                     cwd=self.cwd,
                                     env=fullenv,
-                                    universal_newlines=True)
+                                    universal_newlines=True,
+                                    preexec_fn=self.__set_process_group)
+            # 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()
             returncode = proc.returncode
         except OSError as e:
diff --git a/framework/profile.py b/framework/profile.py
index b801147..f810096 100644
--- a/framework/profile.py
+++ b/framework/profile.py
@@ -71,6 +71,7 @@ class TestProfile(object):
         self._dmesg = None
         self.dmesg = False
         self.results_dir = None
+        self.timeout = 0
 
     @property
     def dmesg(self):
@@ -202,7 +203,7 @@ class TestProfile(object):
 
             """
             name, test = pair
-            test.execute(name, log, self.dmesg)
+            test.execute(name, log, self.dmesg, self.timeout)
             backend.write_test(name, test.result)
 
         def run_threads(pool, testlist):
diff --git a/tests/igt.py b/tests/igt.py
index 22250ce..e7ee518 100644
--- a/tests/igt.py
+++ b/tests/igt.py
@@ -84,47 +84,6 @@ igtEnvironmentOk = checkEnvironment()
 
 profile = TestProfile()
 
-# This class is for timing out tests that hang. 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.
-
-class ProcessTimeout(threading.Thread):
-    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
-        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
-            os.killpg(self.proc.pid, signal.SIGKILL)
-            self.proc.wait()
-
-
-    def join(self):
-        threading.Thread.join(self)
-        return self.status
-
-
-
 class IGTTest(Test):
     def __init__(self, binary, arguments=None):
         if arguments is None:
@@ -153,57 +112,6 @@ class IGTTest(Test):
 
         super(IGTTest, self).run()
 
-
-    def get_command_result(self):
-        out = ""
-        err = ""
-        returncode = 0
-        fullenv = os.environ.copy()
-        for key, value in self.env.iteritems():
-            fullenv[key] = str(value)
-
-        try:
-            proc = subprocess.Popen(self.command,
-                                    stdout=subprocess.PIPE,
-                                    stderr=subprocess.PIPE,
-                                    env=fullenv,
-                                    universal_newlines=True,
-                                    preexec_fn=os.setpgrp)
-
-            # 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
-            timeout = ProcessTimeout(600, proc)
-            timeout.start()
-            out, err = proc.communicate()
-
-            # check for pass or skip first
-            if proc.returncode in (0, 77):
-                returncode = proc.returncode
-            elif timeout.join() > 0:
-                returncode = 78
-            else:
-                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:
-                out = "PIGLIT: {'result': 'skip'}\n" \
-                    + "Test executable not found.\n"
-                err = ""
-                returncode = None
-            else:
-                raise e
-        self.result['out'] = out.decode('utf-8', 'replace')
-        self.result['err'] = err.decode('utf-8', 'replace')
-        self.result['returncode'] = returncode
-
-
 def listTests(listname):
     with open(path.join(igtTestRoot, listname + '.txt'), 'r') as f:
         lines = (line.rstrip() for line in f.readlines())
@@ -255,6 +163,7 @@ for test in tests:
     addSubTestCases(test)
 
 profile.dmesg = True
+profile.timeout = 600
 
 # the dmesg property of TestProfile returns a Dmesg object
 profile.dmesg.regex = re.compile(r"(\[drm:|drm_|intel_|i915_)")
-- 
1.9.3



More information about the Piglit mailing list