普通文本  |  258行  |  8.3 KB

#!/usr/bin/env python
###
### Copyright (C) 2011 Texas Instruments
###
### Licensed under the Apache License, Version 2.0 (the "License");
### you may not use this file except in compliance with the License.
### You may obtain a copy of the License at
###
###      http://www.apache.org/licenses/LICENSE-2.0
###
### Unless required by applicable law or agreed to in writing, software
### distributed under the License is distributed on an "AS IS" BASIS,
### WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
### See the License for the specific language governing permissions and
### limitations under the License.
###

"""TestFlinger meta-test execution framework

When writing a master test script that runs several scripts, this module
can be used to execute those tests in a detached process (sandbox).
Thus, if the test case fails by a segfault or timeout, this can be
detected and the upper-level script simply moves on to the next script.
"""

import os
import time
import subprocess
import sys
import time

g_default_timeout = 300

class TestCase:
    """Test running wrapper object."""

    def __init__(self, TestDict = {}, Logfile = None):
        """Set up the test runner object.

        TestDict: dictionary with the test properties.  (string: value).  The
                  recognized properties are:

                  filename - name of executable test file
                      Type: string
                      Required: yes

                  args - command line arguments for test
                      Type: list of strings, or None
                      Required: no
                      Default: None

                  timeout - upper limit on execution time (secs).  If test takes
                      this long to run, then it is deemed a failure
                      Type: integer
                      Required: no
                      Default: TestFlinger.g_default_timeout (typ. 300 sec)

                  expect-fail - If the test is expected to fail (return non-zero)
                      in order to pass, set this to True
                      Type: bool
                      Required: no
                      Default: False

                  expect-signal If the test is expected to fail because of
                      a signal (e.g. SIGTERM, SIGSEGV) then this is considered success
                      Type: bool
                      Required: no
                      Default: False

        Logfile: a file object where stdout/stderr for the tests should be dumped.
            If null, then no logging will be done.  (See also TestFlinger.setup_logfile()
            and TestFlinger.close_logfile().
        """
        global g_default_timeout

        self._program = None
        self._args = None
        self._timeout = g_default_timeout # Default timeout
        self._verdict = None
        self._expect_fail = False
        self._expect_signal = False
        self._logfile = Logfile

        self._proc = None
        self._time_expire = None

        self._program = TestDict['filename']
        if 'args' in TestDict:
            self._args = TestDict['args']
        if 'timeout' in TestDict and TestDict['timeout'] is not None:
            self._timeout = TestDict['timeout']
        if 'expect-fail' in TestDict and TestDict['expect-fail'] is not None:
            self._expect_fail = TestDict['expect-fail']
        if 'expect-signal' in TestDict and TestDict['expect-signal'] is not None:
            self._expect_signal = TestDict['expect-signal']

    def __del__(self):
        pass

    def start(self):
        """Starts the test in another process.  Returns True if the
        test was successfully spawned.  False if there was an error.
        """

        command = os.path.abspath(self._program)

        if not os.path.exists(command):
            print "ERROR: The program to execute does not exist (%s)" % (command,)
            return False

        timestamp = time.strftime("%Y.%m.%d %H:%M:%S")
        now = time.time()
        self._time_expire = self._timeout + now
        self._kill_timeout = False

        self._log_write("====================================================================\n")
        self._log_write("BEGINNG TEST '%s' at %s\n" % (self._program, timestamp))
        self._log_write("--------------------------------------------------------------------\n")
        self._log_flush()

        self._proc = subprocess.Popen(args=command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

        return (self._proc is not None)

    def wait(self):
        """Blocks until the test completes or times out, whichever
        comes first.  If test fails, returns False.  Otherwise returns
        true.
        """

        if self._proc is None:
            print "ERROR: Test was never started"
            return False

        self._proc.poll()
        while (time.time() < self._time_expire) and (self._proc.poll() is None):
            self._process_logs()
            time.sleep(.5)

        if self._proc.returncode is None:
            self.kill()
            return False

        self._process_logs()
        self._finalize_log()

        return True

    def kill(self):
        """Kill the currently running test (if there is one).
        """

        if self._proc is None:
            print "WARNING: killing a test was never started"
            return False

        self._kill_timeout = True
        self._proc.terminate()
        time.sleep(2)
        self._proc.kill()
        self._log_write("\nKilling process by request...\n")
        self._log_flush()
        self._finalize_log()

        return True


    def verdict(self):
        """Returns a string, either 'PASS', 'FAIL', 'FAIL/TIMEOUT', or 'FAIL/SIGNAL(n)
        '"""
        self._proc.poll()

        rc = self._proc.returncode

        if rc is None:
            print "ERROR: test is still running"

        if self._kill_timeout:
            return "FAIL/TIMOUT"

        if rc < 0 and self._expect_signal:
            return "PASS"
        elif rc < 0:
            return "FAIL/SIGNAL(%d)" % (-rc,)

        if self._expect_fail:
            if rc != 0:
                return "PASS"
            else:
                return "FAIL"
        else:
            if rc == 0:
                return "PASS"
            else:
                return "FAIL"

    def _process_logs(self):
        if self._logfile is not None:
            data = self._proc.stdout.read()
            self._logfile.write(data)
            self._logfile.flush()

    def _finalize_log(self):
        timestamp = time.strftime("%Y.%m.%d %H:%M:%S")
        self._log_write("--------------------------------------------------------------------\n")
        self._log_write("ENDING TEST '%s' at %s\n" % (self._program, timestamp))
        self._log_write("====================================================================\n")
        self._log_flush()

    def _log_write(self, data):
        if self._logfile is not None:
            self._logfile.write(data)

    def _log_flush(self):
        if self._logfile is not None:
            self._logfile.flush()

def setup_logfile(override_logfile_name = None):
    """Open a logfile and prepare it for use with TestFlinger logging.
    The filename will be generated based on the current date/time.

    If override_logfile_name is not None, then that filename will be
    used instead.

    See also: close_logfile()
    """

    tmpfile = None
    if override_logfile_name is not None:
        tmpfile = override_logfile_name
        if os.path.exists(tmpfile):
            os.unlink(tmpfile)
    else:
        tmpfile = time.strftime("test-log-%Y.%m.%d.%H%M%S.txt")
        while os.path.exists(tmpfile):
            tmpfile = time.strftime("test-log-%Y.%m.%d.%H%M%S.txt")
    fobj = open(tmpfile, 'wt')
    print "Logging to", tmpfile
    timestamp = time.strftime("%Y.%m.%d %H:%M:%S")
    fobj.write("BEGINNING TEST SET %s\n" % (timestamp,))
    fobj.write("====================================================================\n")
    return fobj

def close_logfile(fobj):
    """Convenience function for closing a TestFlinger log file.

    fobj: an open and writeable file object

    See also : setup_logfile()
    """

    timestamp = time.strftime("%Y.%m.%d %H:%M:%S")
    fobj.write("====================================================================\n")
    fobj.write("CLOSING TEST SET %s\n" % (timestamp,))

if __name__ == "__main__":
    pass