| #!/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 |