xref: /titanic_54/usr/src/test/test-runner/cmd/run (revision e16fe9a0ee69b445395c471fdca0a7748063d35d)
12fb4439dSAlexander Pyhalov#!@PYTHON@
22fb4439dSAlexander Pyhalov
32fb4439dSAlexander Pyhalov#
42fb4439dSAlexander Pyhalov# This file and its contents are supplied under the terms of the
52fb4439dSAlexander Pyhalov# Common Development and Distribution License ("CDDL"), version 1.0.
62fb4439dSAlexander Pyhalov# You may only use this file in accordance with the terms of version
72fb4439dSAlexander Pyhalov# 1.0 of the CDDL.
82fb4439dSAlexander Pyhalov#
92fb4439dSAlexander Pyhalov# A full copy of the text of the CDDL should have accompanied this
102fb4439dSAlexander Pyhalov# source.  A copy of the CDDL is also available via the Internet at
112fb4439dSAlexander Pyhalov# http://www.illumos.org/license/CDDL.
122fb4439dSAlexander Pyhalov#
132fb4439dSAlexander Pyhalov
142fb4439dSAlexander Pyhalov#
152fb4439dSAlexander Pyhalov# Copyright (c) 2012, 2015 by Delphix. All rights reserved.
162fb4439dSAlexander Pyhalov#
172fb4439dSAlexander Pyhalov
18*e16fe9a0SAlexander Pyhalovfrom __future__ import print_function
19*e16fe9a0SAlexander Pyhalovimport sys
20*e16fe9a0SAlexander PyhalovPY3 = sys.version_info[0] == 3
21*e16fe9a0SAlexander Pyhalov
22*e16fe9a0SAlexander Pyhalovif PY3:
23*e16fe9a0SAlexander Pyhalov    import configparser
24*e16fe9a0SAlexander Pyhalovelse:
25*e16fe9a0SAlexander Pyhalov    import ConfigParser as configparser
26*e16fe9a0SAlexander Pyhalov
272fb4439dSAlexander Pyhalovimport os
282fb4439dSAlexander Pyhalovimport logging
292fb4439dSAlexander Pyhalovfrom logging.handlers import WatchedFileHandler
302fb4439dSAlexander Pyhalovfrom datetime import datetime
312fb4439dSAlexander Pyhalovfrom optparse import OptionParser
322fb4439dSAlexander Pyhalovfrom pwd import getpwnam
332fb4439dSAlexander Pyhalovfrom pwd import getpwuid
342fb4439dSAlexander Pyhalovfrom select import select
352fb4439dSAlexander Pyhalovfrom subprocess import PIPE
362fb4439dSAlexander Pyhalovfrom subprocess import Popen
372fb4439dSAlexander Pyhalovfrom sys import argv
382fb4439dSAlexander Pyhalovfrom sys import exit
39*e16fe9a0SAlexander Pyhalovfrom sys import maxsize
402fb4439dSAlexander Pyhalovfrom threading import Timer
412fb4439dSAlexander Pyhalovfrom time import time
422fb4439dSAlexander Pyhalov
432fb4439dSAlexander PyhalovBASEDIR = '/var/tmp/test_results'
442fb4439dSAlexander PyhalovKILL = '/usr/bin/kill'
452fb4439dSAlexander PyhalovTRUE = '/usr/bin/true'
462fb4439dSAlexander PyhalovSUDO = '/usr/bin/sudo'
472fb4439dSAlexander Pyhalov
482fb4439dSAlexander Pyhalov# Custom class to reopen the log file in case it is forcibly closed by a test.
492fb4439dSAlexander Pyhalovclass WatchedFileHandlerClosed(WatchedFileHandler):
502fb4439dSAlexander Pyhalov    """Watch files, including closed files.
512fb4439dSAlexander Pyhalov    Similar to (and inherits from) logging.handler.WatchedFileHandler,
522fb4439dSAlexander Pyhalov    except that IOErrors are handled by reopening the stream and retrying.
532fb4439dSAlexander Pyhalov    This will be retried up to a configurable number of times before
542fb4439dSAlexander Pyhalov    giving up, default 5.
552fb4439dSAlexander Pyhalov    """
562fb4439dSAlexander Pyhalov
572fb4439dSAlexander Pyhalov    def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
582fb4439dSAlexander Pyhalov        self.max_tries = max_tries
592fb4439dSAlexander Pyhalov        self.tries = 0
602fb4439dSAlexander Pyhalov        WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
612fb4439dSAlexander Pyhalov
622fb4439dSAlexander Pyhalov    def emit(self, record):
632fb4439dSAlexander Pyhalov        while True:
642fb4439dSAlexander Pyhalov            try:
652fb4439dSAlexander Pyhalov                WatchedFileHandler.emit(self, record)
662fb4439dSAlexander Pyhalov                self.tries = 0
672fb4439dSAlexander Pyhalov                return
682fb4439dSAlexander Pyhalov            except IOError as err:
692fb4439dSAlexander Pyhalov                if self.tries == self.max_tries:
702fb4439dSAlexander Pyhalov                    raise
712fb4439dSAlexander Pyhalov                self.stream.close()
722fb4439dSAlexander Pyhalov                self.stream = self._open()
732fb4439dSAlexander Pyhalov                self.tries += 1
742fb4439dSAlexander Pyhalov
752fb4439dSAlexander Pyhalovclass Result(object):
762fb4439dSAlexander Pyhalov    total = 0
772fb4439dSAlexander Pyhalov    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
782fb4439dSAlexander Pyhalov
792fb4439dSAlexander Pyhalov    def __init__(self):
802fb4439dSAlexander Pyhalov        self.starttime = None
812fb4439dSAlexander Pyhalov        self.returncode = None
822fb4439dSAlexander Pyhalov        self.runtime = ''
832fb4439dSAlexander Pyhalov        self.stdout = []
842fb4439dSAlexander Pyhalov        self.stderr = []
852fb4439dSAlexander Pyhalov        self.result = ''
862fb4439dSAlexander Pyhalov
872fb4439dSAlexander Pyhalov    def done(self, proc, killed):
882fb4439dSAlexander Pyhalov        """
892fb4439dSAlexander Pyhalov        Finalize the results of this Cmd.
902fb4439dSAlexander Pyhalov        """
912fb4439dSAlexander Pyhalov        Result.total += 1
922fb4439dSAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
932fb4439dSAlexander Pyhalov        self.runtime = '%02d:%02d' % (m, s)
942fb4439dSAlexander Pyhalov        self.returncode = proc.returncode
952fb4439dSAlexander Pyhalov        if killed:
962fb4439dSAlexander Pyhalov            self.result = 'KILLED'
972fb4439dSAlexander Pyhalov            Result.runresults['KILLED'] += 1
982fb4439dSAlexander Pyhalov        elif self.returncode is 0:
992fb4439dSAlexander Pyhalov            self.result = 'PASS'
1002fb4439dSAlexander Pyhalov            Result.runresults['PASS'] += 1
1012fb4439dSAlexander Pyhalov        elif self.returncode is not 0:
1022fb4439dSAlexander Pyhalov            self.result = 'FAIL'
1032fb4439dSAlexander Pyhalov            Result.runresults['FAIL'] += 1
1042fb4439dSAlexander Pyhalov
1052fb4439dSAlexander Pyhalov
1062fb4439dSAlexander Pyhalovclass Output(object):
1072fb4439dSAlexander Pyhalov    """
1082fb4439dSAlexander Pyhalov    This class is a slightly modified version of the 'Stream' class found
1092fb4439dSAlexander Pyhalov    here: http://goo.gl/aSGfv
1102fb4439dSAlexander Pyhalov    """
1112fb4439dSAlexander Pyhalov    def __init__(self, stream):
1122fb4439dSAlexander Pyhalov        self.stream = stream
1132fb4439dSAlexander Pyhalov        self._buf = ''
1142fb4439dSAlexander Pyhalov        self.lines = []
1152fb4439dSAlexander Pyhalov
1162fb4439dSAlexander Pyhalov    def fileno(self):
1172fb4439dSAlexander Pyhalov        return self.stream.fileno()
1182fb4439dSAlexander Pyhalov
1192fb4439dSAlexander Pyhalov    def read(self, drain=0):
1202fb4439dSAlexander Pyhalov        """
1212fb4439dSAlexander Pyhalov        Read from the file descriptor. If 'drain' set, read until EOF.
1222fb4439dSAlexander Pyhalov        """
1232fb4439dSAlexander Pyhalov        while self._read() is not None:
1242fb4439dSAlexander Pyhalov            if not drain:
1252fb4439dSAlexander Pyhalov                break
1262fb4439dSAlexander Pyhalov
1272fb4439dSAlexander Pyhalov    def _read(self):
1282fb4439dSAlexander Pyhalov        """
1292fb4439dSAlexander Pyhalov        Read up to 4k of data from this output stream. Collect the output
1302fb4439dSAlexander Pyhalov        up to the last newline, and append it to any leftover data from a
1312fb4439dSAlexander Pyhalov        previous call. The lines are stored as a (timestamp, data) tuple
1322fb4439dSAlexander Pyhalov        for easy sorting/merging later.
1332fb4439dSAlexander Pyhalov        """
1342fb4439dSAlexander Pyhalov        fd = self.fileno()
135*e16fe9a0SAlexander Pyhalov        buf = os.read(fd, 4096).decode()
1362fb4439dSAlexander Pyhalov        if not buf:
1372fb4439dSAlexander Pyhalov            return None
1382fb4439dSAlexander Pyhalov        if '\n' not in buf:
1392fb4439dSAlexander Pyhalov            self._buf += buf
1402fb4439dSAlexander Pyhalov            return []
1412fb4439dSAlexander Pyhalov
1422fb4439dSAlexander Pyhalov        buf = self._buf + buf
1432fb4439dSAlexander Pyhalov        tmp, rest = buf.rsplit('\n', 1)
1442fb4439dSAlexander Pyhalov        self._buf = rest
1452fb4439dSAlexander Pyhalov        now = datetime.now()
1462fb4439dSAlexander Pyhalov        rows = tmp.split('\n')
1472fb4439dSAlexander Pyhalov        self.lines += [(now, r) for r in rows]
1482fb4439dSAlexander Pyhalov
1492fb4439dSAlexander Pyhalov
1502fb4439dSAlexander Pyhalovclass Cmd(object):
1512fb4439dSAlexander Pyhalov    verified_users = []
1522fb4439dSAlexander Pyhalov
1532fb4439dSAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None):
1542fb4439dSAlexander Pyhalov        self.pathname = pathname
1552fb4439dSAlexander Pyhalov        self.outputdir = outputdir or 'BASEDIR'
1562fb4439dSAlexander Pyhalov        self.timeout = timeout or 60
1572fb4439dSAlexander Pyhalov        self.user = user or ''
1582fb4439dSAlexander Pyhalov        self.killed = False
1592fb4439dSAlexander Pyhalov        self.result = Result()
1602fb4439dSAlexander Pyhalov
1612fb4439dSAlexander Pyhalov    def __str__(self):
1622fb4439dSAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % (
1632fb4439dSAlexander Pyhalov                self.pathname, self.outputdir, self.timeout, self.user)
1642fb4439dSAlexander Pyhalov
1652fb4439dSAlexander Pyhalov    def kill_cmd(self, proc):
1662fb4439dSAlexander Pyhalov        """
1672fb4439dSAlexander Pyhalov        Kill a running command due to timeout, or ^C from the keyboard. If
1682fb4439dSAlexander Pyhalov        sudo is required, this user was verified previously.
1692fb4439dSAlexander Pyhalov        """
1702fb4439dSAlexander Pyhalov        self.killed = True
1712fb4439dSAlexander Pyhalov        do_sudo = len(self.user) != 0
1722fb4439dSAlexander Pyhalov        signal = '-TERM'
1732fb4439dSAlexander Pyhalov
1742fb4439dSAlexander Pyhalov        cmd = [SUDO, KILL, signal, str(proc.pid)]
1752fb4439dSAlexander Pyhalov        if not do_sudo:
1762fb4439dSAlexander Pyhalov            del cmd[0]
1772fb4439dSAlexander Pyhalov
1782fb4439dSAlexander Pyhalov        try:
1792fb4439dSAlexander Pyhalov            kp = Popen(cmd)
1802fb4439dSAlexander Pyhalov            kp.wait()
1812fb4439dSAlexander Pyhalov        except:
1822fb4439dSAlexander Pyhalov            pass
1832fb4439dSAlexander Pyhalov
1842fb4439dSAlexander Pyhalov    def update_cmd_privs(self, cmd, user):
1852fb4439dSAlexander Pyhalov        """
1862fb4439dSAlexander Pyhalov        If a user has been specified to run this Cmd and we're not already
1872fb4439dSAlexander Pyhalov        running as that user, prepend the appropriate sudo command to run
1882fb4439dSAlexander Pyhalov        as that user.
1892fb4439dSAlexander Pyhalov        """
1902fb4439dSAlexander Pyhalov        me = getpwuid(os.getuid())
1912fb4439dSAlexander Pyhalov
1922fb4439dSAlexander Pyhalov        if not user or user is me:
1932fb4439dSAlexander Pyhalov            return cmd
1942fb4439dSAlexander Pyhalov
1952fb4439dSAlexander Pyhalov        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
1962fb4439dSAlexander Pyhalov        return ret.split(' ')
1972fb4439dSAlexander Pyhalov
1982fb4439dSAlexander Pyhalov    def collect_output(self, proc):
1992fb4439dSAlexander Pyhalov        """
2002fb4439dSAlexander Pyhalov        Read from stdout/stderr as data becomes available, until the
2012fb4439dSAlexander Pyhalov        process is no longer running. Return the lines from the stdout and
2022fb4439dSAlexander Pyhalov        stderr Output objects.
2032fb4439dSAlexander Pyhalov        """
2042fb4439dSAlexander Pyhalov        out = Output(proc.stdout)
2052fb4439dSAlexander Pyhalov        err = Output(proc.stderr)
2062fb4439dSAlexander Pyhalov        res = []
2072fb4439dSAlexander Pyhalov        while proc.returncode is None:
2082fb4439dSAlexander Pyhalov            proc.poll()
2092fb4439dSAlexander Pyhalov            res = select([out, err], [], [], .1)
2102fb4439dSAlexander Pyhalov            for fd in res[0]:
2112fb4439dSAlexander Pyhalov                fd.read()
2122fb4439dSAlexander Pyhalov        for fd in res[0]:
2132fb4439dSAlexander Pyhalov            fd.read(drain=1)
2142fb4439dSAlexander Pyhalov
2152fb4439dSAlexander Pyhalov        return out.lines, err.lines
2162fb4439dSAlexander Pyhalov
2172fb4439dSAlexander Pyhalov    def run(self, options):
2182fb4439dSAlexander Pyhalov        """
2192fb4439dSAlexander Pyhalov        This is the main function that runs each individual test.
2202fb4439dSAlexander Pyhalov        Determine whether or not the command requires sudo, and modify it
2212fb4439dSAlexander Pyhalov        if needed. Run the command, and update the result object.
2222fb4439dSAlexander Pyhalov        """
2232fb4439dSAlexander Pyhalov        if options.dryrun is True:
224*e16fe9a0SAlexander Pyhalov            print(self)
2252fb4439dSAlexander Pyhalov            return
2262fb4439dSAlexander Pyhalov
2272fb4439dSAlexander Pyhalov        privcmd = self.update_cmd_privs(self.pathname, self.user)
2282fb4439dSAlexander Pyhalov        try:
2292fb4439dSAlexander Pyhalov            old = os.umask(0)
2302fb4439dSAlexander Pyhalov            if not os.path.isdir(self.outputdir):
231*e16fe9a0SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
2322fb4439dSAlexander Pyhalov            os.umask(old)
233*e16fe9a0SAlexander Pyhalov        except OSError as e:
2342fb4439dSAlexander Pyhalov            fail('%s' % e)
2352fb4439dSAlexander Pyhalov
2362fb4439dSAlexander Pyhalov        try:
2372fb4439dSAlexander Pyhalov            self.result.starttime = time()
238*e16fe9a0SAlexander Pyhalov            proc = Popen(privcmd, stdout=PIPE, stderr=PIPE,
239*e16fe9a0SAlexander Pyhalov                         universal_newlines=True)
2402fb4439dSAlexander Pyhalov            t = Timer(int(self.timeout), self.kill_cmd, [proc])
2412fb4439dSAlexander Pyhalov            t.start()
2422fb4439dSAlexander Pyhalov            self.result.stdout, self.result.stderr = self.collect_output(proc)
2432fb4439dSAlexander Pyhalov        except KeyboardInterrupt:
2442fb4439dSAlexander Pyhalov            self.kill_cmd(proc)
2452fb4439dSAlexander Pyhalov            fail('\nRun terminated at user request.')
2462fb4439dSAlexander Pyhalov        finally:
2472fb4439dSAlexander Pyhalov            t.cancel()
2482fb4439dSAlexander Pyhalov
2492fb4439dSAlexander Pyhalov        self.result.done(proc, self.killed)
2502fb4439dSAlexander Pyhalov
2512fb4439dSAlexander Pyhalov    def skip(self):
2522fb4439dSAlexander Pyhalov        """
2532fb4439dSAlexander Pyhalov        Initialize enough of the test result that we can log a skipped
2542fb4439dSAlexander Pyhalov        command.
2552fb4439dSAlexander Pyhalov        """
2562fb4439dSAlexander Pyhalov        Result.total += 1
2572fb4439dSAlexander Pyhalov        Result.runresults['SKIP'] += 1
2582fb4439dSAlexander Pyhalov        self.result.stdout = self.result.stderr = []
2592fb4439dSAlexander Pyhalov        self.result.starttime = time()
2602fb4439dSAlexander Pyhalov        m, s = divmod(time() - self.result.starttime, 60)
2612fb4439dSAlexander Pyhalov        self.result.runtime = '%02d:%02d' % (m, s)
2622fb4439dSAlexander Pyhalov        self.result.result = 'SKIP'
2632fb4439dSAlexander Pyhalov
2642fb4439dSAlexander Pyhalov    def log(self, logger, options):
2652fb4439dSAlexander Pyhalov        """
2662fb4439dSAlexander Pyhalov        This function is responsible for writing all output. This includes
2672fb4439dSAlexander Pyhalov        the console output, the logfile of all results (with timestamped
2682fb4439dSAlexander Pyhalov        merged stdout and stderr), and for each test, the unmodified
2692fb4439dSAlexander Pyhalov        stdout/stderr/merged in it's own file.
2702fb4439dSAlexander Pyhalov        """
2712fb4439dSAlexander Pyhalov        if logger is None:
2722fb4439dSAlexander Pyhalov            return
2732fb4439dSAlexander Pyhalov
2742fb4439dSAlexander Pyhalov        logname = getpwuid(os.getuid()).pw_name
2752fb4439dSAlexander Pyhalov        user = ' (run as %s)' % (self.user if len(self.user) else logname)
2762fb4439dSAlexander Pyhalov        msga = 'Test: %s%s ' % (self.pathname, user)
2772fb4439dSAlexander Pyhalov        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
2782fb4439dSAlexander Pyhalov        pad = ' ' * (80 - (len(msga) + len(msgb)))
2792fb4439dSAlexander Pyhalov
2802fb4439dSAlexander Pyhalov        # If -q is specified, only print a line for tests that didn't pass.
2812fb4439dSAlexander Pyhalov        # This means passing tests need to be logged as DEBUG, or the one
2822fb4439dSAlexander Pyhalov        # line summary will only be printed in the logfile for failures.
2832fb4439dSAlexander Pyhalov        if not options.quiet:
2842fb4439dSAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
2852fb4439dSAlexander Pyhalov        elif self.result.result is not 'PASS':
2862fb4439dSAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
2872fb4439dSAlexander Pyhalov        else:
2882fb4439dSAlexander Pyhalov            logger.debug('%s%s%s' % (msga, pad, msgb))
2892fb4439dSAlexander Pyhalov
2902fb4439dSAlexander Pyhalov        lines = sorted(self.result.stdout + self.result.stderr,
291*e16fe9a0SAlexander Pyhalov                       key=lambda x: x[0])
2922fb4439dSAlexander Pyhalov
2932fb4439dSAlexander Pyhalov        for dt, line in lines:
2942fb4439dSAlexander Pyhalov            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
2952fb4439dSAlexander Pyhalov
2962fb4439dSAlexander Pyhalov        if len(self.result.stdout):
2972fb4439dSAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
2982fb4439dSAlexander Pyhalov                for _, line in self.result.stdout:
299*e16fe9a0SAlexander Pyhalov                    out.write('%s\n' % line)
3002fb4439dSAlexander Pyhalov        if len(self.result.stderr):
3012fb4439dSAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
3022fb4439dSAlexander Pyhalov                for _, line in self.result.stderr:
303*e16fe9a0SAlexander Pyhalov                    err.write('%s\n' % line)
3042fb4439dSAlexander Pyhalov        if len(self.result.stdout) and len(self.result.stderr):
3052fb4439dSAlexander Pyhalov            with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
3062fb4439dSAlexander Pyhalov                for _, line in lines:
307*e16fe9a0SAlexander Pyhalov                    merged.write('%s\n' % line)
3082fb4439dSAlexander Pyhalov
3092fb4439dSAlexander Pyhalov
3102fb4439dSAlexander Pyhalovclass Test(Cmd):
3112fb4439dSAlexander Pyhalov    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
3122fb4439dSAlexander Pyhalov             'post_user']
3132fb4439dSAlexander Pyhalov
3142fb4439dSAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
3152fb4439dSAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None):
3162fb4439dSAlexander Pyhalov        super(Test, self).__init__(pathname, outputdir, timeout, user)
3172fb4439dSAlexander Pyhalov        self.pre = pre or ''
3182fb4439dSAlexander Pyhalov        self.pre_user = pre_user or ''
3192fb4439dSAlexander Pyhalov        self.post = post or ''
3202fb4439dSAlexander Pyhalov        self.post_user = post_user or ''
3212fb4439dSAlexander Pyhalov
3222fb4439dSAlexander Pyhalov    def __str__(self):
3232fb4439dSAlexander Pyhalov        post_user = pre_user = ''
3242fb4439dSAlexander Pyhalov        if len(self.pre_user):
3252fb4439dSAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
3262fb4439dSAlexander Pyhalov        if len(self.post_user):
3272fb4439dSAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
3282fb4439dSAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \
3292fb4439dSAlexander Pyhalov               "%s%s\nUser: %s\n" % (self.pathname, self.outputdir,
3302fb4439dSAlexander Pyhalov                self.timeout, self.pre, pre_user, self.post, post_user,
3312fb4439dSAlexander Pyhalov                self.user)
3322fb4439dSAlexander Pyhalov
3332fb4439dSAlexander Pyhalov    def verify(self, logger):
3342fb4439dSAlexander Pyhalov        """
3352fb4439dSAlexander Pyhalov        Check the pre/post scripts, user and Test. Omit the Test from this
3362fb4439dSAlexander Pyhalov        run if there are any problems.
3372fb4439dSAlexander Pyhalov        """
3382fb4439dSAlexander Pyhalov        files = [self.pre, self.pathname, self.post]
3392fb4439dSAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
3402fb4439dSAlexander Pyhalov
3412fb4439dSAlexander Pyhalov        for f in [f for f in files if len(f)]:
3422fb4439dSAlexander Pyhalov            if not verify_file(f):
3432fb4439dSAlexander Pyhalov                logger.info("Warning: Test '%s' not added to this run because"
3442fb4439dSAlexander Pyhalov                            " it failed verification." % f)
3452fb4439dSAlexander Pyhalov                return False
3462fb4439dSAlexander Pyhalov
3472fb4439dSAlexander Pyhalov        for user in [user for user in users if len(user)]:
3482fb4439dSAlexander Pyhalov            if not verify_user(user, logger):
3492fb4439dSAlexander Pyhalov                logger.info("Not adding Test '%s' to this run." %
3502fb4439dSAlexander Pyhalov                            self.pathname)
3512fb4439dSAlexander Pyhalov                return False
3522fb4439dSAlexander Pyhalov
3532fb4439dSAlexander Pyhalov        return True
3542fb4439dSAlexander Pyhalov
3552fb4439dSAlexander Pyhalov    def run(self, logger, options):
3562fb4439dSAlexander Pyhalov        """
3572fb4439dSAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
3582fb4439dSAlexander Pyhalov        doesn't pass, skip this Test. Run the post script regardless.
3592fb4439dSAlexander Pyhalov        """
3602fb4439dSAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
3612fb4439dSAlexander Pyhalov                      os.path.basename(self.pre)), timeout=self.timeout,
3622fb4439dSAlexander Pyhalov                      user=self.pre_user)
3632fb4439dSAlexander Pyhalov        test = Cmd(self.pathname, outputdir=self.outputdir,
3642fb4439dSAlexander Pyhalov                   timeout=self.timeout, user=self.user)
3652fb4439dSAlexander Pyhalov        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
3662fb4439dSAlexander Pyhalov                       os.path.basename(self.post)), timeout=self.timeout,
3672fb4439dSAlexander Pyhalov                       user=self.post_user)
3682fb4439dSAlexander Pyhalov
3692fb4439dSAlexander Pyhalov        cont = True
3702fb4439dSAlexander Pyhalov        if len(pretest.pathname):
3712fb4439dSAlexander Pyhalov            pretest.run(options)
3722fb4439dSAlexander Pyhalov            cont = pretest.result.result is 'PASS'
3732fb4439dSAlexander Pyhalov            pretest.log(logger, options)
3742fb4439dSAlexander Pyhalov
3752fb4439dSAlexander Pyhalov        if cont:
3762fb4439dSAlexander Pyhalov            test.run(options)
3772fb4439dSAlexander Pyhalov        else:
3782fb4439dSAlexander Pyhalov            test.skip()
3792fb4439dSAlexander Pyhalov
3802fb4439dSAlexander Pyhalov        test.log(logger, options)
3812fb4439dSAlexander Pyhalov
3822fb4439dSAlexander Pyhalov        if len(posttest.pathname):
3832fb4439dSAlexander Pyhalov            posttest.run(options)
3842fb4439dSAlexander Pyhalov            posttest.log(logger, options)
3852fb4439dSAlexander Pyhalov
3862fb4439dSAlexander Pyhalov
3872fb4439dSAlexander Pyhalovclass TestGroup(Test):
3882fb4439dSAlexander Pyhalov    props = Test.props + ['tests']
3892fb4439dSAlexander Pyhalov
3902fb4439dSAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
3912fb4439dSAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None,
3922fb4439dSAlexander Pyhalov                 tests=None):
3932fb4439dSAlexander Pyhalov        super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
3942fb4439dSAlexander Pyhalov                                        pre, pre_user, post, post_user)
3952fb4439dSAlexander Pyhalov        self.tests = tests or []
3962fb4439dSAlexander Pyhalov
3972fb4439dSAlexander Pyhalov    def __str__(self):
3982fb4439dSAlexander Pyhalov        post_user = pre_user = ''
3992fb4439dSAlexander Pyhalov        if len(self.pre_user):
4002fb4439dSAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
4012fb4439dSAlexander Pyhalov        if len(self.post_user):
4022fb4439dSAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
4032fb4439dSAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \
4042fb4439dSAlexander Pyhalov               "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname,
4052fb4439dSAlexander Pyhalov                self.outputdir, self.tests, self.timeout, self.pre, pre_user,
4062fb4439dSAlexander Pyhalov                self.post, post_user, self.user)
4072fb4439dSAlexander Pyhalov
4082fb4439dSAlexander Pyhalov    def verify(self, logger):
4092fb4439dSAlexander Pyhalov        """
4102fb4439dSAlexander Pyhalov        Check the pre/post scripts, user and tests in this TestGroup. Omit
4112fb4439dSAlexander Pyhalov        the TestGroup entirely, or simply delete the relevant tests in the
4122fb4439dSAlexander Pyhalov        group, if that's all that's required.
4132fb4439dSAlexander Pyhalov        """
4142fb4439dSAlexander Pyhalov        # If the pre or post scripts are relative pathnames, convert to
4152fb4439dSAlexander Pyhalov        # absolute, so they stand a chance of passing verification.
4162fb4439dSAlexander Pyhalov        if len(self.pre) and not os.path.isabs(self.pre):
4172fb4439dSAlexander Pyhalov            self.pre = os.path.join(self.pathname, self.pre)
4182fb4439dSAlexander Pyhalov        if len(self.post) and not os.path.isabs(self.post):
4192fb4439dSAlexander Pyhalov            self.post = os.path.join(self.pathname, self.post)
4202fb4439dSAlexander Pyhalov
4212fb4439dSAlexander Pyhalov        auxfiles = [self.pre, self.post]
4222fb4439dSAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
4232fb4439dSAlexander Pyhalov
4242fb4439dSAlexander Pyhalov        for f in [f for f in auxfiles if len(f)]:
4252fb4439dSAlexander Pyhalov            if self.pathname != os.path.dirname(f):
4262fb4439dSAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4272fb4439dSAlexander Pyhalov                            "Auxiliary script '%s' exists in a different "
4282fb4439dSAlexander Pyhalov                            "directory." % (self.pathname, f))
4292fb4439dSAlexander Pyhalov                return False
4302fb4439dSAlexander Pyhalov
4312fb4439dSAlexander Pyhalov            if not verify_file(f):
4322fb4439dSAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4332fb4439dSAlexander Pyhalov                            "Auxiliary script '%s' failed verification." %
4342fb4439dSAlexander Pyhalov                            (self.pathname, f))
4352fb4439dSAlexander Pyhalov                return False
4362fb4439dSAlexander Pyhalov
4372fb4439dSAlexander Pyhalov        for user in [user for user in users if len(user)]:
4382fb4439dSAlexander Pyhalov            if not verify_user(user, logger):
4392fb4439dSAlexander Pyhalov                logger.info("Not adding TestGroup '%s' to this run." %
4402fb4439dSAlexander Pyhalov                            self.pathname)
4412fb4439dSAlexander Pyhalov                return False
4422fb4439dSAlexander Pyhalov
4432fb4439dSAlexander Pyhalov        # If one of the tests is invalid, delete it, log it, and drive on.
4442fb4439dSAlexander Pyhalov        for test in self.tests:
4452fb4439dSAlexander Pyhalov            if not verify_file(os.path.join(self.pathname, test)):
4462fb4439dSAlexander Pyhalov                del self.tests[self.tests.index(test)]
4472fb4439dSAlexander Pyhalov                logger.info("Warning: Test '%s' removed from TestGroup '%s' "
4482fb4439dSAlexander Pyhalov                            "because it failed verification." % (test,
4492fb4439dSAlexander Pyhalov                            self.pathname))
4502fb4439dSAlexander Pyhalov
4512fb4439dSAlexander Pyhalov        return len(self.tests) is not 0
4522fb4439dSAlexander Pyhalov
4532fb4439dSAlexander Pyhalov    def run(self, logger, options):
4542fb4439dSAlexander Pyhalov        """
4552fb4439dSAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
4562fb4439dSAlexander Pyhalov        doesn't pass, skip all the tests in this TestGroup. Run the post
4572fb4439dSAlexander Pyhalov        script regardless.
4582fb4439dSAlexander Pyhalov        """
4592fb4439dSAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
4602fb4439dSAlexander Pyhalov                      os.path.basename(self.pre)), timeout=self.timeout,
4612fb4439dSAlexander Pyhalov                      user=self.pre_user)
4622fb4439dSAlexander Pyhalov        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
4632fb4439dSAlexander Pyhalov                       os.path.basename(self.post)), timeout=self.timeout,
4642fb4439dSAlexander Pyhalov                       user=self.post_user)
4652fb4439dSAlexander Pyhalov
4662fb4439dSAlexander Pyhalov        cont = True
4672fb4439dSAlexander Pyhalov        if len(pretest.pathname):
4682fb4439dSAlexander Pyhalov            pretest.run(options)
4692fb4439dSAlexander Pyhalov            cont = pretest.result.result is 'PASS'
4702fb4439dSAlexander Pyhalov            pretest.log(logger, options)
4712fb4439dSAlexander Pyhalov
4722fb4439dSAlexander Pyhalov        for fname in self.tests:
4732fb4439dSAlexander Pyhalov            test = Cmd(os.path.join(self.pathname, fname),
4742fb4439dSAlexander Pyhalov                       outputdir=os.path.join(self.outputdir, fname),
4752fb4439dSAlexander Pyhalov                       timeout=self.timeout, user=self.user)
4762fb4439dSAlexander Pyhalov            if cont:
4772fb4439dSAlexander Pyhalov                test.run(options)
4782fb4439dSAlexander Pyhalov            else:
4792fb4439dSAlexander Pyhalov                test.skip()
4802fb4439dSAlexander Pyhalov
4812fb4439dSAlexander Pyhalov            test.log(logger, options)
4822fb4439dSAlexander Pyhalov
4832fb4439dSAlexander Pyhalov        if len(posttest.pathname):
4842fb4439dSAlexander Pyhalov            posttest.run(options)
4852fb4439dSAlexander Pyhalov            posttest.log(logger, options)
4862fb4439dSAlexander Pyhalov
4872fb4439dSAlexander Pyhalov
4882fb4439dSAlexander Pyhalovclass TestRun(object):
4892fb4439dSAlexander Pyhalov    props = ['quiet', 'outputdir']
4902fb4439dSAlexander Pyhalov
4912fb4439dSAlexander Pyhalov    def __init__(self, options):
4922fb4439dSAlexander Pyhalov        self.tests = {}
4932fb4439dSAlexander Pyhalov        self.testgroups = {}
4942fb4439dSAlexander Pyhalov        self.starttime = time()
4952fb4439dSAlexander Pyhalov        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
4962fb4439dSAlexander Pyhalov        self.outputdir = os.path.join(options.outputdir, self.timestamp)
4972fb4439dSAlexander Pyhalov        self.logger = self.setup_logging(options)
4982fb4439dSAlexander Pyhalov        self.defaults = [
4992fb4439dSAlexander Pyhalov            ('outputdir', BASEDIR),
5002fb4439dSAlexander Pyhalov            ('quiet', False),
5012fb4439dSAlexander Pyhalov            ('timeout', 60),
5022fb4439dSAlexander Pyhalov            ('user', ''),
5032fb4439dSAlexander Pyhalov            ('pre', ''),
5042fb4439dSAlexander Pyhalov            ('pre_user', ''),
5052fb4439dSAlexander Pyhalov            ('post', ''),
5062fb4439dSAlexander Pyhalov            ('post_user', '')
5072fb4439dSAlexander Pyhalov        ]
5082fb4439dSAlexander Pyhalov
5092fb4439dSAlexander Pyhalov    def __str__(self):
5102fb4439dSAlexander Pyhalov        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
5112fb4439dSAlexander Pyhalov        s += 'TESTS:\n'
5122fb4439dSAlexander Pyhalov        for key in sorted(self.tests.keys()):
5132fb4439dSAlexander Pyhalov            s += '%s%s' % (self.tests[key].__str__(), '\n')
5142fb4439dSAlexander Pyhalov        s += 'TESTGROUPS:\n'
5152fb4439dSAlexander Pyhalov        for key in sorted(self.testgroups.keys()):
5162fb4439dSAlexander Pyhalov            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
5172fb4439dSAlexander Pyhalov        return s
5182fb4439dSAlexander Pyhalov
5192fb4439dSAlexander Pyhalov    def addtest(self, pathname, options):
5202fb4439dSAlexander Pyhalov        """
5212fb4439dSAlexander Pyhalov        Create a new Test, and apply any properties that were passed in
5222fb4439dSAlexander Pyhalov        from the command line. If it passes verification, add it to the
5232fb4439dSAlexander Pyhalov        TestRun.
5242fb4439dSAlexander Pyhalov        """
5252fb4439dSAlexander Pyhalov        test = Test(pathname)
5262fb4439dSAlexander Pyhalov        for prop in Test.props:
5272fb4439dSAlexander Pyhalov            setattr(test, prop, getattr(options, prop))
5282fb4439dSAlexander Pyhalov
5292fb4439dSAlexander Pyhalov        if test.verify(self.logger):
5302fb4439dSAlexander Pyhalov            self.tests[pathname] = test
5312fb4439dSAlexander Pyhalov
5322fb4439dSAlexander Pyhalov    def addtestgroup(self, dirname, filenames, options):
5332fb4439dSAlexander Pyhalov        """
5342fb4439dSAlexander Pyhalov        Create a new TestGroup, and apply any properties that were passed
5352fb4439dSAlexander Pyhalov        in from the command line. If it passes verification, add it to the
5362fb4439dSAlexander Pyhalov        TestRun.
5372fb4439dSAlexander Pyhalov        """
5382fb4439dSAlexander Pyhalov        if dirname not in self.testgroups:
5392fb4439dSAlexander Pyhalov            testgroup = TestGroup(dirname)
5402fb4439dSAlexander Pyhalov            for prop in Test.props:
5412fb4439dSAlexander Pyhalov                setattr(testgroup, prop, getattr(options, prop))
5422fb4439dSAlexander Pyhalov
5432fb4439dSAlexander Pyhalov            # Prevent pre/post scripts from running as regular tests
5442fb4439dSAlexander Pyhalov            for f in [testgroup.pre, testgroup.post]:
5452fb4439dSAlexander Pyhalov                if f in filenames:
5462fb4439dSAlexander Pyhalov                    del filenames[filenames.index(f)]
5472fb4439dSAlexander Pyhalov
5482fb4439dSAlexander Pyhalov            self.testgroups[dirname] = testgroup
5492fb4439dSAlexander Pyhalov            self.testgroups[dirname].tests = sorted(filenames)
5502fb4439dSAlexander Pyhalov
5512fb4439dSAlexander Pyhalov            testgroup.verify(self.logger)
5522fb4439dSAlexander Pyhalov
5532fb4439dSAlexander Pyhalov    def read(self, logger, options):
5542fb4439dSAlexander Pyhalov        """
5552fb4439dSAlexander Pyhalov        Read in the specified runfile, and apply the TestRun properties
5562fb4439dSAlexander Pyhalov        listed in the 'DEFAULT' section to our TestRun. Then read each
5572fb4439dSAlexander Pyhalov        section, and apply the appropriate properties to the Test or
5582fb4439dSAlexander Pyhalov        TestGroup. Properties from individual sections override those set
5592fb4439dSAlexander Pyhalov        in the 'DEFAULT' section. If the Test or TestGroup passes
5602fb4439dSAlexander Pyhalov        verification, add it to the TestRun.
5612fb4439dSAlexander Pyhalov        """
562*e16fe9a0SAlexander Pyhalov        config = configparser.RawConfigParser()
5632fb4439dSAlexander Pyhalov        if not len(config.read(options.runfile)):
5642fb4439dSAlexander Pyhalov            fail("Coulnd't read config file %s" % options.runfile)
5652fb4439dSAlexander Pyhalov
5662fb4439dSAlexander Pyhalov        for opt in TestRun.props:
5672fb4439dSAlexander Pyhalov            if config.has_option('DEFAULT', opt):
5682fb4439dSAlexander Pyhalov                setattr(self, opt, config.get('DEFAULT', opt))
5692fb4439dSAlexander Pyhalov        self.outputdir = os.path.join(self.outputdir, self.timestamp)
5702fb4439dSAlexander Pyhalov
5712fb4439dSAlexander Pyhalov        for section in config.sections():
5722fb4439dSAlexander Pyhalov            if 'tests' in config.options(section):
5732fb4439dSAlexander Pyhalov                testgroup = TestGroup(section)
5742fb4439dSAlexander Pyhalov                for prop in TestGroup.props:
5752fb4439dSAlexander Pyhalov                    try:
5762fb4439dSAlexander Pyhalov                        setattr(testgroup, prop, config.get('DEFAULT', prop))
5772fb4439dSAlexander Pyhalov                        setattr(testgroup, prop, config.get(section, prop))
5782fb4439dSAlexander Pyhalov                    except ConfigParser.NoOptionError:
5792fb4439dSAlexander Pyhalov                        pass
5802fb4439dSAlexander Pyhalov
5812fb4439dSAlexander Pyhalov                # Repopulate tests using eval to convert the string to a list
5822fb4439dSAlexander Pyhalov                testgroup.tests = eval(config.get(section, 'tests'))
5832fb4439dSAlexander Pyhalov
5842fb4439dSAlexander Pyhalov                if testgroup.verify(logger):
5852fb4439dSAlexander Pyhalov                    self.testgroups[section] = testgroup
5862fb4439dSAlexander Pyhalov            else:
5872fb4439dSAlexander Pyhalov                test = Test(section)
5882fb4439dSAlexander Pyhalov                for prop in Test.props:
5892fb4439dSAlexander Pyhalov                    try:
5902fb4439dSAlexander Pyhalov                        setattr(test, prop, config.get('DEFAULT', prop))
5912fb4439dSAlexander Pyhalov                        setattr(test, prop, config.get(section, prop))
5922fb4439dSAlexander Pyhalov                    except ConfigParser.NoOptionError:
5932fb4439dSAlexander Pyhalov                        pass
5942fb4439dSAlexander Pyhalov                if test.verify(logger):
5952fb4439dSAlexander Pyhalov                    self.tests[section] = test
5962fb4439dSAlexander Pyhalov
5972fb4439dSAlexander Pyhalov    def write(self, options):
5982fb4439dSAlexander Pyhalov        """
5992fb4439dSAlexander Pyhalov        Create a configuration file for editing and later use. The
6002fb4439dSAlexander Pyhalov        'DEFAULT' section of the config file is created from the
6012fb4439dSAlexander Pyhalov        properties that were specified on the command line. Tests are
6022fb4439dSAlexander Pyhalov        simply added as sections that inherit everything from the
6032fb4439dSAlexander Pyhalov        'DEFAULT' section. TestGroups are the same, except they get an
6042fb4439dSAlexander Pyhalov        option including all the tests to run in that directory.
6052fb4439dSAlexander Pyhalov        """
6062fb4439dSAlexander Pyhalov
6072fb4439dSAlexander Pyhalov        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
6082fb4439dSAlexander Pyhalov                        self.defaults])
609*e16fe9a0SAlexander Pyhalov        config = configparser.RawConfigParser(defaults)
6102fb4439dSAlexander Pyhalov
6112fb4439dSAlexander Pyhalov        for test in sorted(self.tests.keys()):
6122fb4439dSAlexander Pyhalov            config.add_section(test)
6132fb4439dSAlexander Pyhalov
6142fb4439dSAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
6152fb4439dSAlexander Pyhalov            config.add_section(testgroup)
6162fb4439dSAlexander Pyhalov            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
6172fb4439dSAlexander Pyhalov
6182fb4439dSAlexander Pyhalov        try:
6192fb4439dSAlexander Pyhalov            with open(options.template, 'w') as f:
6202fb4439dSAlexander Pyhalov                return config.write(f)
6212fb4439dSAlexander Pyhalov        except IOError:
6222fb4439dSAlexander Pyhalov            fail('Could not open \'%s\' for writing.' % options.template)
6232fb4439dSAlexander Pyhalov
6242fb4439dSAlexander Pyhalov    def complete_outputdirs(self, options):
6252fb4439dSAlexander Pyhalov        """
6262fb4439dSAlexander Pyhalov        Collect all the pathnames for Tests, and TestGroups. Work
6272fb4439dSAlexander Pyhalov        backwards one pathname component at a time, to create a unique
6282fb4439dSAlexander Pyhalov        directory name in which to deposit test output. Tests will be able
6292fb4439dSAlexander Pyhalov        to write output files directly in the newly modified outputdir.
6302fb4439dSAlexander Pyhalov        TestGroups will be able to create one subdirectory per test in the
6312fb4439dSAlexander Pyhalov        outputdir, and are guaranteed uniqueness because a group can only
6322fb4439dSAlexander Pyhalov        contain files in one directory. Pre and post tests will create a
6332fb4439dSAlexander Pyhalov        directory rooted at the outputdir of the Test or TestGroup in
6342fb4439dSAlexander Pyhalov        question for their output.
6352fb4439dSAlexander Pyhalov        """
6362fb4439dSAlexander Pyhalov        done = False
6372fb4439dSAlexander Pyhalov        components = 0
638*e16fe9a0SAlexander Pyhalov        tmp_dict = dict(list(self.tests.items()) + list(self.testgroups.items()))
6392fb4439dSAlexander Pyhalov        total = len(tmp_dict)
6402fb4439dSAlexander Pyhalov        base = self.outputdir
6412fb4439dSAlexander Pyhalov
6422fb4439dSAlexander Pyhalov        while not done:
6432fb4439dSAlexander Pyhalov            l = []
6442fb4439dSAlexander Pyhalov            components -= 1
645*e16fe9a0SAlexander Pyhalov            for testfile in list(tmp_dict.keys()):
6462fb4439dSAlexander Pyhalov                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
6472fb4439dSAlexander Pyhalov                if not uniq in l:
6482fb4439dSAlexander Pyhalov                    l.append(uniq)
6492fb4439dSAlexander Pyhalov                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
6502fb4439dSAlexander Pyhalov                else:
6512fb4439dSAlexander Pyhalov                    break
6522fb4439dSAlexander Pyhalov            done = total == len(l)
6532fb4439dSAlexander Pyhalov
6542fb4439dSAlexander Pyhalov    def setup_logging(self, options):
6552fb4439dSAlexander Pyhalov        """
6562fb4439dSAlexander Pyhalov        Two loggers are set up here. The first is for the logfile which
6572fb4439dSAlexander Pyhalov        will contain one line summarizing the test, including the test
6582fb4439dSAlexander Pyhalov        name, result, and running time. This logger will also capture the
6592fb4439dSAlexander Pyhalov        timestamped combined stdout and stderr of each run. The second
6602fb4439dSAlexander Pyhalov        logger is optional console output, which will contain only the one
6612fb4439dSAlexander Pyhalov        line summary. The loggers are initialized at two different levels
6622fb4439dSAlexander Pyhalov        to facilitate segregating the output.
6632fb4439dSAlexander Pyhalov        """
6642fb4439dSAlexander Pyhalov        if options.dryrun is True:
6652fb4439dSAlexander Pyhalov            return
6662fb4439dSAlexander Pyhalov
6672fb4439dSAlexander Pyhalov        testlogger = logging.getLogger(__name__)
6682fb4439dSAlexander Pyhalov        testlogger.setLevel(logging.DEBUG)
6692fb4439dSAlexander Pyhalov
6702fb4439dSAlexander Pyhalov        if options.cmd is not 'wrconfig':
6712fb4439dSAlexander Pyhalov            try:
6722fb4439dSAlexander Pyhalov                old = os.umask(0)
673*e16fe9a0SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
6742fb4439dSAlexander Pyhalov                os.umask(old)
675*e16fe9a0SAlexander Pyhalov            except OSError as e:
6762fb4439dSAlexander Pyhalov                fail('%s' % e)
6772fb4439dSAlexander Pyhalov            filename = os.path.join(self.outputdir, 'log')
6782fb4439dSAlexander Pyhalov
6792fb4439dSAlexander Pyhalov            logfile = WatchedFileHandlerClosed(filename)
6802fb4439dSAlexander Pyhalov            logfile.setLevel(logging.DEBUG)
6812fb4439dSAlexander Pyhalov            logfilefmt = logging.Formatter('%(message)s')
6822fb4439dSAlexander Pyhalov            logfile.setFormatter(logfilefmt)
6832fb4439dSAlexander Pyhalov            testlogger.addHandler(logfile)
6842fb4439dSAlexander Pyhalov
6852fb4439dSAlexander Pyhalov        cons = logging.StreamHandler()
6862fb4439dSAlexander Pyhalov        cons.setLevel(logging.INFO)
6872fb4439dSAlexander Pyhalov        consfmt = logging.Formatter('%(message)s')
6882fb4439dSAlexander Pyhalov        cons.setFormatter(consfmt)
6892fb4439dSAlexander Pyhalov        testlogger.addHandler(cons)
6902fb4439dSAlexander Pyhalov
6912fb4439dSAlexander Pyhalov        return testlogger
6922fb4439dSAlexander Pyhalov
6932fb4439dSAlexander Pyhalov    def run(self, options):
6942fb4439dSAlexander Pyhalov        """
6952fb4439dSAlexander Pyhalov        Walk through all the Tests and TestGroups, calling run().
6962fb4439dSAlexander Pyhalov        """
6972fb4439dSAlexander Pyhalov        try:
6982fb4439dSAlexander Pyhalov            os.chdir(self.outputdir)
6992fb4439dSAlexander Pyhalov        except OSError:
7002fb4439dSAlexander Pyhalov            fail('Could not change to directory %s' % self.outputdir)
7012fb4439dSAlexander Pyhalov        for test in sorted(self.tests.keys()):
7022fb4439dSAlexander Pyhalov            self.tests[test].run(self.logger, options)
7032fb4439dSAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
7042fb4439dSAlexander Pyhalov            self.testgroups[testgroup].run(self.logger, options)
7052fb4439dSAlexander Pyhalov
7062fb4439dSAlexander Pyhalov    def summary(self):
7072fb4439dSAlexander Pyhalov        if Result.total is 0:
7082fb4439dSAlexander Pyhalov            return
7092fb4439dSAlexander Pyhalov
710*e16fe9a0SAlexander Pyhalov        print('\nResults Summary')
711*e16fe9a0SAlexander Pyhalov        for key in list(Result.runresults.keys()):
7122fb4439dSAlexander Pyhalov            if Result.runresults[key] is not 0:
713*e16fe9a0SAlexander Pyhalov                print('%s\t% 4d' % (key, Result.runresults[key]))
7142fb4439dSAlexander Pyhalov
7152fb4439dSAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
7162fb4439dSAlexander Pyhalov        h, m = divmod(m, 60)
717*e16fe9a0SAlexander Pyhalov        print('\nRunning Time:\t%02d:%02d:%02d' % (h, m, s))
718*e16fe9a0SAlexander Pyhalov        print('Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
719*e16fe9a0SAlexander Pyhalov               float(Result.total)) * 100))
720*e16fe9a0SAlexander Pyhalov        print('Log directory:\t%s' % self.outputdir)
7212fb4439dSAlexander Pyhalov
7222fb4439dSAlexander Pyhalov
7232fb4439dSAlexander Pyhalovdef verify_file(pathname):
7242fb4439dSAlexander Pyhalov    """
7252fb4439dSAlexander Pyhalov    Verify that the supplied pathname is an executable regular file.
7262fb4439dSAlexander Pyhalov    """
7272fb4439dSAlexander Pyhalov    if os.path.isdir(pathname) or os.path.islink(pathname):
7282fb4439dSAlexander Pyhalov        return False
7292fb4439dSAlexander Pyhalov
7302fb4439dSAlexander Pyhalov    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
7312fb4439dSAlexander Pyhalov        return True
7322fb4439dSAlexander Pyhalov
7332fb4439dSAlexander Pyhalov    return False
7342fb4439dSAlexander Pyhalov
7352fb4439dSAlexander Pyhalov
7362fb4439dSAlexander Pyhalovdef verify_user(user, logger):
7372fb4439dSAlexander Pyhalov    """
7382fb4439dSAlexander Pyhalov    Verify that the specified user exists on this system, and can execute
7392fb4439dSAlexander Pyhalov    sudo without being prompted for a password.
7402fb4439dSAlexander Pyhalov    """
7412fb4439dSAlexander Pyhalov    testcmd = [SUDO, '-n', '-u', user, TRUE]
7422fb4439dSAlexander Pyhalov    can_sudo = exists = True
7432fb4439dSAlexander Pyhalov
7442fb4439dSAlexander Pyhalov    if user in Cmd.verified_users:
7452fb4439dSAlexander Pyhalov        return True
7462fb4439dSAlexander Pyhalov
7472fb4439dSAlexander Pyhalov    try:
7482fb4439dSAlexander Pyhalov        _ = getpwnam(user)
7492fb4439dSAlexander Pyhalov    except KeyError:
7502fb4439dSAlexander Pyhalov        exists = False
7512fb4439dSAlexander Pyhalov        logger.info("Warning: user '%s' does not exist.", user)
7522fb4439dSAlexander Pyhalov        return False
7532fb4439dSAlexander Pyhalov
7542fb4439dSAlexander Pyhalov    p = Popen(testcmd)
7552fb4439dSAlexander Pyhalov    p.wait()
7562fb4439dSAlexander Pyhalov    if p.returncode is not 0:
7572fb4439dSAlexander Pyhalov        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
7582fb4439dSAlexander Pyhalov        return False
7592fb4439dSAlexander Pyhalov    else:
7602fb4439dSAlexander Pyhalov        Cmd.verified_users.append(user)
7612fb4439dSAlexander Pyhalov
7622fb4439dSAlexander Pyhalov    return True
7632fb4439dSAlexander Pyhalov
7642fb4439dSAlexander Pyhalov
7652fb4439dSAlexander Pyhalovdef find_tests(testrun, options):
7662fb4439dSAlexander Pyhalov    """
7672fb4439dSAlexander Pyhalov    For the given list of pathnames, add files as Tests. For directories,
7682fb4439dSAlexander Pyhalov    if do_groups is True, add the directory as a TestGroup. If False,
7692fb4439dSAlexander Pyhalov    recursively search for executable files.
7702fb4439dSAlexander Pyhalov    """
7712fb4439dSAlexander Pyhalov
7722fb4439dSAlexander Pyhalov    for p in sorted(options.pathnames):
7732fb4439dSAlexander Pyhalov        if os.path.isdir(p):
7742fb4439dSAlexander Pyhalov            for dirname, _, filenames in os.walk(p):
7752fb4439dSAlexander Pyhalov                if options.do_groups:
7762fb4439dSAlexander Pyhalov                    testrun.addtestgroup(dirname, filenames, options)
7772fb4439dSAlexander Pyhalov                else:
7782fb4439dSAlexander Pyhalov                    for f in sorted(filenames):
7792fb4439dSAlexander Pyhalov                        testrun.addtest(os.path.join(dirname, f), options)
7802fb4439dSAlexander Pyhalov        else:
7812fb4439dSAlexander Pyhalov            testrun.addtest(p, options)
7822fb4439dSAlexander Pyhalov
7832fb4439dSAlexander Pyhalov
7842fb4439dSAlexander Pyhalovdef fail(retstr, ret=1):
785*e16fe9a0SAlexander Pyhalov    print('%s: %s' % (argv[0], retstr))
7862fb4439dSAlexander Pyhalov    exit(ret)
7872fb4439dSAlexander Pyhalov
7882fb4439dSAlexander Pyhalov
7892fb4439dSAlexander Pyhalovdef options_cb(option, opt_str, value, parser):
7902fb4439dSAlexander Pyhalov    path_options = ['runfile', 'outputdir', 'template']
7912fb4439dSAlexander Pyhalov
7922fb4439dSAlexander Pyhalov    if option.dest is 'runfile' and '-w' in parser.rargs or \
7932fb4439dSAlexander Pyhalov        option.dest is 'template' and '-c' in parser.rargs:
7942fb4439dSAlexander Pyhalov        fail('-c and -w are mutually exclusive.')
7952fb4439dSAlexander Pyhalov
7962fb4439dSAlexander Pyhalov    if opt_str in parser.rargs:
7972fb4439dSAlexander Pyhalov        fail('%s may only be specified once.' % opt_str)
7982fb4439dSAlexander Pyhalov
7992fb4439dSAlexander Pyhalov    if option.dest is 'runfile':
8002fb4439dSAlexander Pyhalov        parser.values.cmd = 'rdconfig'
8012fb4439dSAlexander Pyhalov    if option.dest is 'template':
8022fb4439dSAlexander Pyhalov        parser.values.cmd = 'wrconfig'
8032fb4439dSAlexander Pyhalov
8042fb4439dSAlexander Pyhalov    setattr(parser.values, option.dest, value)
8052fb4439dSAlexander Pyhalov    if option.dest in path_options:
8062fb4439dSAlexander Pyhalov        setattr(parser.values, option.dest, os.path.abspath(value))
8072fb4439dSAlexander Pyhalov
8082fb4439dSAlexander Pyhalov
8092fb4439dSAlexander Pyhalovdef parse_args():
8102fb4439dSAlexander Pyhalov    parser = OptionParser()
8112fb4439dSAlexander Pyhalov    parser.add_option('-c', action='callback', callback=options_cb,
8122fb4439dSAlexander Pyhalov                      type='string', dest='runfile', metavar='runfile',
8132fb4439dSAlexander Pyhalov                      help='Specify tests to run via config file.')
8142fb4439dSAlexander Pyhalov    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
8152fb4439dSAlexander Pyhalov                      help='Dry run. Print tests, but take no other action.')
8162fb4439dSAlexander Pyhalov    parser.add_option('-g', action='store_true', default=False,
8172fb4439dSAlexander Pyhalov                      dest='do_groups', help='Make directories TestGroups.')
8182fb4439dSAlexander Pyhalov    parser.add_option('-o', action='callback', callback=options_cb,
8192fb4439dSAlexander Pyhalov                      default=BASEDIR, dest='outputdir', type='string',
8202fb4439dSAlexander Pyhalov                      metavar='outputdir', help='Specify an output directory.')
8212fb4439dSAlexander Pyhalov    parser.add_option('-p', action='callback', callback=options_cb,
8222fb4439dSAlexander Pyhalov                      default='', dest='pre', metavar='script',
8232fb4439dSAlexander Pyhalov                      type='string', help='Specify a pre script.')
8242fb4439dSAlexander Pyhalov    parser.add_option('-P', action='callback', callback=options_cb,
8252fb4439dSAlexander Pyhalov                      default='', dest='post', metavar='script',
8262fb4439dSAlexander Pyhalov                      type='string', help='Specify a post script.')
8272fb4439dSAlexander Pyhalov    parser.add_option('-q', action='store_true', default=False, dest='quiet',
8282fb4439dSAlexander Pyhalov                      help='Silence on the console during a test run.')
8292fb4439dSAlexander Pyhalov    parser.add_option('-t', action='callback', callback=options_cb, default=60,
8302fb4439dSAlexander Pyhalov                      dest='timeout', metavar='seconds', type='int',
8312fb4439dSAlexander Pyhalov                      help='Timeout (in seconds) for an individual test.')
8322fb4439dSAlexander Pyhalov    parser.add_option('-u', action='callback', callback=options_cb,
8332fb4439dSAlexander Pyhalov                      default='', dest='user', metavar='user', type='string',
8342fb4439dSAlexander Pyhalov                      help='Specify a different user name to run as.')
8352fb4439dSAlexander Pyhalov    parser.add_option('-w', action='callback', callback=options_cb,
8362fb4439dSAlexander Pyhalov                      default=None, dest='template', metavar='template',
8372fb4439dSAlexander Pyhalov                      type='string', help='Create a new config file.')
8382fb4439dSAlexander Pyhalov    parser.add_option('-x', action='callback', callback=options_cb, default='',
8392fb4439dSAlexander Pyhalov                      dest='pre_user', metavar='pre_user', type='string',
8402fb4439dSAlexander Pyhalov                      help='Specify a user to execute the pre script.')
8412fb4439dSAlexander Pyhalov    parser.add_option('-X', action='callback', callback=options_cb, default='',
8422fb4439dSAlexander Pyhalov                      dest='post_user', metavar='post_user', type='string',
8432fb4439dSAlexander Pyhalov                      help='Specify a user to execute the post script.')
8442fb4439dSAlexander Pyhalov    (options, pathnames) = parser.parse_args()
8452fb4439dSAlexander Pyhalov
8462fb4439dSAlexander Pyhalov    if not options.runfile and not options.template:
8472fb4439dSAlexander Pyhalov        options.cmd = 'runtests'
8482fb4439dSAlexander Pyhalov
8492fb4439dSAlexander Pyhalov    if options.runfile and len(pathnames):
8502fb4439dSAlexander Pyhalov        fail('Extraneous arguments.')
8512fb4439dSAlexander Pyhalov
8522fb4439dSAlexander Pyhalov    options.pathnames = [os.path.abspath(path) for path in pathnames]
8532fb4439dSAlexander Pyhalov
8542fb4439dSAlexander Pyhalov    return options
8552fb4439dSAlexander Pyhalov
8562fb4439dSAlexander Pyhalov
8572fb4439dSAlexander Pyhalovdef main(args):
8582fb4439dSAlexander Pyhalov    options = parse_args()
8592fb4439dSAlexander Pyhalov    testrun = TestRun(options)
8602fb4439dSAlexander Pyhalov
8612fb4439dSAlexander Pyhalov    if options.cmd is 'runtests':
8622fb4439dSAlexander Pyhalov        find_tests(testrun, options)
8632fb4439dSAlexander Pyhalov    elif options.cmd is 'rdconfig':
8642fb4439dSAlexander Pyhalov        testrun.read(testrun.logger, options)
8652fb4439dSAlexander Pyhalov    elif options.cmd is 'wrconfig':
8662fb4439dSAlexander Pyhalov        find_tests(testrun, options)
8672fb4439dSAlexander Pyhalov        testrun.write(options)
8682fb4439dSAlexander Pyhalov        exit(0)
8692fb4439dSAlexander Pyhalov    else:
8702fb4439dSAlexander Pyhalov        fail('Unknown command specified')
8712fb4439dSAlexander Pyhalov
8722fb4439dSAlexander Pyhalov    testrun.complete_outputdirs(options)
8732fb4439dSAlexander Pyhalov    testrun.run(options)
8742fb4439dSAlexander Pyhalov    testrun.summary()
8752fb4439dSAlexander Pyhalov    exit(0)
8762fb4439dSAlexander Pyhalov
8772fb4439dSAlexander Pyhalov
8782fb4439dSAlexander Pyhalovif __name__ == '__main__':
8792fb4439dSAlexander Pyhalov    main(argv[1:])
880