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