[Piglit] [PATCH] framework: add a generic timeout mechanism
Thomas Wood
thomas.wood at intel.com
Wed Sep 17 07:25:53 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.
v2: Fix some small style issues. (Dylan Baker)
Make timeout a class attribute of Test, rather than an instance
attribute and remove it from the profile class. (Dylan Baker)
Only check the timeout value if the process was abnormally
terminated.
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 | 78 ++++++++++++++++++++++++++++++++++++++++--
tests/igt.py | 93 +--------------------------------------------------
2 files changed, 76 insertions(+), 95 deletions(-)
diff --git a/framework/exectest.py b/framework/exectest.py
index 3ed9b6f..6635f8e 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:
@@ -51,6 +54,56 @@ else:
'../bin'))
+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 Test(object):
""" Abstract base class for Test classes
@@ -73,7 +126,8 @@ class Test(object):
OPTS = Options()
__metaclass__ = abc.ABCMeta
__slots__ = ['run_concurrent', 'env', 'result', 'cwd', '_command',
- '_test_hook_execute_run']
+ '_test_hook_execute_run', '__proc_timeout']
+ timeout = 0
def __init__(self, command, run_concurrent=False):
self._command = None
@@ -82,6 +136,7 @@ class Test(object):
self.env = {}
self.result = TestResult({'result': 'fail'})
self.cwd = None
+ self.__proc_timeout = None
# This is a hook for doing some testing on execute right before
# self.run is called.
@@ -183,7 +238,11 @@ class Test(object):
self.interpret_result()
if self.result['returncode'] < 0:
- self.result['result'] = 'crash'
+ # 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'
elif self.result['returncode'] != 0 and self.result['result'] == 'pass':
self.result['result'] = 'warn'
@@ -208,6 +267,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 +303,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/tests/igt.py b/tests/igt.py
index 22250ce..13860a7 100644
--- a/tests/igt.py
+++ b/tests/igt.py
@@ -84,53 +84,13 @@ 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:
arguments = []
super(IGTTest, self).__init__(
[path.join(igtTestRoot, binary)] + arguments)
+ self.timeout = 600
def interpret_result(self):
if not igtEnvironmentOk:
@@ -153,57 +113,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())
--
1.9.3
More information about the Piglit
mailing list