[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