xref: /illumos-gate/usr/src/test/test-runner/cmd/run (revision 64ee6612ceb686fa3f41561cf589072ebb0132b1)
19f923083SAlexander Pyhalov#!@PYTHON@
29f923083SAlexander Pyhalov
39f923083SAlexander Pyhalov#
49f923083SAlexander Pyhalov# This file and its contents are supplied under the terms of the
59f923083SAlexander Pyhalov# Common Development and Distribution License ("CDDL"), version 1.0.
69f923083SAlexander Pyhalov# You may only use this file in accordance with the terms of version
79f923083SAlexander Pyhalov# 1.0 of the CDDL.
89f923083SAlexander Pyhalov#
99f923083SAlexander Pyhalov# A full copy of the text of the CDDL should have accompanied this
109f923083SAlexander Pyhalov# source.  A copy of the CDDL is also available via the Internet at
119f923083SAlexander Pyhalov# http://www.illumos.org/license/CDDL.
129f923083SAlexander Pyhalov#
139f923083SAlexander Pyhalov
149f923083SAlexander Pyhalov#
15dfc11533SChris Williamson# Copyright (c) 2012, 2016 by Delphix. All rights reserved.
16*64ee6612SChris Fraire# Copyright (c) 2017, Chris Fraire <cfraire@me.com>.
179f923083SAlexander Pyhalov#
189f923083SAlexander Pyhalov
199f923083SAlexander Pyhalovimport ConfigParser
209f923083SAlexander Pyhalovimport os
219f923083SAlexander Pyhalovimport logging
229f923083SAlexander Pyhalovfrom logging.handlers import WatchedFileHandler
239f923083SAlexander Pyhalovfrom datetime import datetime
249f923083SAlexander Pyhalovfrom optparse import OptionParser
259f923083SAlexander Pyhalovfrom pwd import getpwnam
269f923083SAlexander Pyhalovfrom pwd import getpwuid
279f923083SAlexander Pyhalovfrom select import select
289f923083SAlexander Pyhalovfrom subprocess import PIPE
299f923083SAlexander Pyhalovfrom subprocess import Popen
309f923083SAlexander Pyhalovfrom sys import argv
319f923083SAlexander Pyhalovfrom sys import maxint
329f923083SAlexander Pyhalovfrom threading import Timer
339f923083SAlexander Pyhalovfrom time import time
349f923083SAlexander Pyhalov
359f923083SAlexander PyhalovBASEDIR = '/var/tmp/test_results'
369f923083SAlexander PyhalovKILL = '/usr/bin/kill'
379f923083SAlexander PyhalovTRUE = '/usr/bin/true'
389f923083SAlexander PyhalovSUDO = '/usr/bin/sudo'
399f923083SAlexander Pyhalov
409f923083SAlexander Pyhalov# Custom class to reopen the log file in case it is forcibly closed by a test.
419f923083SAlexander Pyhalovclass WatchedFileHandlerClosed(WatchedFileHandler):
429f923083SAlexander Pyhalov    """Watch files, including closed files.
439f923083SAlexander Pyhalov    Similar to (and inherits from) logging.handler.WatchedFileHandler,
449f923083SAlexander Pyhalov    except that IOErrors are handled by reopening the stream and retrying.
459f923083SAlexander Pyhalov    This will be retried up to a configurable number of times before
469f923083SAlexander Pyhalov    giving up, default 5.
479f923083SAlexander Pyhalov    """
489f923083SAlexander Pyhalov
499f923083SAlexander Pyhalov    def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
509f923083SAlexander Pyhalov        self.max_tries = max_tries
519f923083SAlexander Pyhalov        self.tries = 0
529f923083SAlexander Pyhalov        WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
539f923083SAlexander Pyhalov
549f923083SAlexander Pyhalov    def emit(self, record):
559f923083SAlexander Pyhalov        while True:
569f923083SAlexander Pyhalov            try:
579f923083SAlexander Pyhalov                WatchedFileHandler.emit(self, record)
589f923083SAlexander Pyhalov                self.tries = 0
599f923083SAlexander Pyhalov                return
609f923083SAlexander Pyhalov            except IOError as err:
619f923083SAlexander Pyhalov                if self.tries == self.max_tries:
629f923083SAlexander Pyhalov                    raise
639f923083SAlexander Pyhalov                self.stream.close()
649f923083SAlexander Pyhalov                self.stream = self._open()
659f923083SAlexander Pyhalov                self.tries += 1
669f923083SAlexander Pyhalov
679f923083SAlexander Pyhalovclass Result(object):
689f923083SAlexander Pyhalov    total = 0
699f923083SAlexander Pyhalov    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
709f923083SAlexander Pyhalov
719f923083SAlexander Pyhalov    def __init__(self):
729f923083SAlexander Pyhalov        self.starttime = None
739f923083SAlexander Pyhalov        self.returncode = None
749f923083SAlexander Pyhalov        self.runtime = ''
759f923083SAlexander Pyhalov        self.stdout = []
769f923083SAlexander Pyhalov        self.stderr = []
779f923083SAlexander Pyhalov        self.result = ''
789f923083SAlexander Pyhalov
799f923083SAlexander Pyhalov    def done(self, proc, killed):
809f923083SAlexander Pyhalov        """
819f923083SAlexander Pyhalov        Finalize the results of this Cmd.
829f923083SAlexander Pyhalov        """
839f923083SAlexander Pyhalov        Result.total += 1
849f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
859f923083SAlexander Pyhalov        self.runtime = '%02d:%02d' % (m, s)
869f923083SAlexander Pyhalov        self.returncode = proc.returncode
879f923083SAlexander Pyhalov        if killed:
889f923083SAlexander Pyhalov            self.result = 'KILLED'
899f923083SAlexander Pyhalov            Result.runresults['KILLED'] += 1
909f923083SAlexander Pyhalov        elif self.returncode is 0:
919f923083SAlexander Pyhalov            self.result = 'PASS'
929f923083SAlexander Pyhalov            Result.runresults['PASS'] += 1
939f923083SAlexander Pyhalov        elif self.returncode is not 0:
949f923083SAlexander Pyhalov            self.result = 'FAIL'
959f923083SAlexander Pyhalov            Result.runresults['FAIL'] += 1
969f923083SAlexander Pyhalov
979f923083SAlexander Pyhalov
989f923083SAlexander Pyhalovclass Output(object):
999f923083SAlexander Pyhalov    """
1009f923083SAlexander Pyhalov    This class is a slightly modified version of the 'Stream' class found
1019f923083SAlexander Pyhalov    here: http://goo.gl/aSGfv
1029f923083SAlexander Pyhalov    """
1039f923083SAlexander Pyhalov    def __init__(self, stream):
1049f923083SAlexander Pyhalov        self.stream = stream
1059f923083SAlexander Pyhalov        self._buf = ''
1069f923083SAlexander Pyhalov        self.lines = []
1079f923083SAlexander Pyhalov
1089f923083SAlexander Pyhalov    def fileno(self):
1099f923083SAlexander Pyhalov        return self.stream.fileno()
1109f923083SAlexander Pyhalov
1119f923083SAlexander Pyhalov    def read(self, drain=0):
1129f923083SAlexander Pyhalov        """
1139f923083SAlexander Pyhalov        Read from the file descriptor. If 'drain' set, read until EOF.
1149f923083SAlexander Pyhalov        """
1159f923083SAlexander Pyhalov        while self._read() is not None:
1169f923083SAlexander Pyhalov            if not drain:
1179f923083SAlexander Pyhalov                break
1189f923083SAlexander Pyhalov
1199f923083SAlexander Pyhalov    def _read(self):
1209f923083SAlexander Pyhalov        """
1219f923083SAlexander Pyhalov        Read up to 4k of data from this output stream. Collect the output
1229f923083SAlexander Pyhalov        up to the last newline, and append it to any leftover data from a
1239f923083SAlexander Pyhalov        previous call. The lines are stored as a (timestamp, data) tuple
1249f923083SAlexander Pyhalov        for easy sorting/merging later.
1259f923083SAlexander Pyhalov        """
1269f923083SAlexander Pyhalov        fd = self.fileno()
1279f923083SAlexander Pyhalov        buf = os.read(fd, 4096)
1289f923083SAlexander Pyhalov        if not buf:
1299f923083SAlexander Pyhalov            return None
1309f923083SAlexander Pyhalov        if '\n' not in buf:
1319f923083SAlexander Pyhalov            self._buf += buf
1329f923083SAlexander Pyhalov            return []
1339f923083SAlexander Pyhalov
1349f923083SAlexander Pyhalov        buf = self._buf + buf
1359f923083SAlexander Pyhalov        tmp, rest = buf.rsplit('\n', 1)
1369f923083SAlexander Pyhalov        self._buf = rest
1379f923083SAlexander Pyhalov        now = datetime.now()
1389f923083SAlexander Pyhalov        rows = tmp.split('\n')
1399f923083SAlexander Pyhalov        self.lines += [(now, r) for r in rows]
1409f923083SAlexander Pyhalov
1419f923083SAlexander Pyhalov
1429f923083SAlexander Pyhalovclass Cmd(object):
1439f923083SAlexander Pyhalov    verified_users = []
1449f923083SAlexander Pyhalov
1459f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None):
1469f923083SAlexander Pyhalov        self.pathname = pathname
1479f923083SAlexander Pyhalov        self.outputdir = outputdir or 'BASEDIR'
1489f923083SAlexander Pyhalov        self.timeout = timeout
1499f923083SAlexander Pyhalov        self.user = user or ''
1509f923083SAlexander Pyhalov        self.killed = False
1519f923083SAlexander Pyhalov        self.result = Result()
1529f923083SAlexander Pyhalov
1539f923083SAlexander Pyhalov        if self.timeout is None:
1549f923083SAlexander Pyhalov            self.timeout = 60
1559f923083SAlexander Pyhalov
1569f923083SAlexander Pyhalov    def __str__(self):
157*64ee6612SChris Fraire        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % \
1589f923083SAlexander Pyhalov            (self.pathname, self.outputdir, self.timeout, self.user)
1599f923083SAlexander Pyhalov
1609f923083SAlexander Pyhalov    def kill_cmd(self, proc):
1619f923083SAlexander Pyhalov        """
1629f923083SAlexander Pyhalov        Kill a running command due to timeout, or ^C from the keyboard. If
1639f923083SAlexander Pyhalov        sudo is required, this user was verified previously.
1649f923083SAlexander Pyhalov        """
1659f923083SAlexander Pyhalov        self.killed = True
1669f923083SAlexander Pyhalov        do_sudo = len(self.user) != 0
1679f923083SAlexander Pyhalov        signal = '-TERM'
1689f923083SAlexander Pyhalov
1699f923083SAlexander Pyhalov        cmd = [SUDO, KILL, signal, str(proc.pid)]
1709f923083SAlexander Pyhalov        if not do_sudo:
1719f923083SAlexander Pyhalov            del cmd[0]
1729f923083SAlexander Pyhalov
1739f923083SAlexander Pyhalov        try:
1749f923083SAlexander Pyhalov            kp = Popen(cmd)
1759f923083SAlexander Pyhalov            kp.wait()
1769f923083SAlexander Pyhalov        except:
1779f923083SAlexander Pyhalov            pass
1789f923083SAlexander Pyhalov
1799f923083SAlexander Pyhalov    def update_cmd_privs(self, cmd, user):
1809f923083SAlexander Pyhalov        """
1819f923083SAlexander Pyhalov        If a user has been specified to run this Cmd and we're not already
1829f923083SAlexander Pyhalov        running as that user, prepend the appropriate sudo command to run
1839f923083SAlexander Pyhalov        as that user.
1849f923083SAlexander Pyhalov        """
1859f923083SAlexander Pyhalov        me = getpwuid(os.getuid())
1869f923083SAlexander Pyhalov
1879f923083SAlexander Pyhalov        if not user or user is me:
1889f923083SAlexander Pyhalov            return cmd
1899f923083SAlexander Pyhalov
1909f923083SAlexander Pyhalov        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
1919f923083SAlexander Pyhalov        return ret.split(' ')
1929f923083SAlexander Pyhalov
1939f923083SAlexander Pyhalov    def collect_output(self, proc):
1949f923083SAlexander Pyhalov        """
1959f923083SAlexander Pyhalov        Read from stdout/stderr as data becomes available, until the
1969f923083SAlexander Pyhalov        process is no longer running. Return the lines from the stdout and
1979f923083SAlexander Pyhalov        stderr Output objects.
1989f923083SAlexander Pyhalov        """
1999f923083SAlexander Pyhalov        out = Output(proc.stdout)
2009f923083SAlexander Pyhalov        err = Output(proc.stderr)
2019f923083SAlexander Pyhalov        res = []
2029f923083SAlexander Pyhalov        while proc.returncode is None:
2039f923083SAlexander Pyhalov            proc.poll()
2049f923083SAlexander Pyhalov            res = select([out, err], [], [], .1)
2059f923083SAlexander Pyhalov            for fd in res[0]:
2069f923083SAlexander Pyhalov                fd.read()
2079f923083SAlexander Pyhalov        for fd in res[0]:
2089f923083SAlexander Pyhalov            fd.read(drain=1)
2099f923083SAlexander Pyhalov
2109f923083SAlexander Pyhalov        return out.lines, err.lines
2119f923083SAlexander Pyhalov
2129f923083SAlexander Pyhalov    def run(self, options):
2139f923083SAlexander Pyhalov        """
2149f923083SAlexander Pyhalov        This is the main function that runs each individual test.
2159f923083SAlexander Pyhalov        Determine whether or not the command requires sudo, and modify it
2169f923083SAlexander Pyhalov        if needed. Run the command, and update the result object.
2179f923083SAlexander Pyhalov        """
2189f923083SAlexander Pyhalov        if options.dryrun is True:
2199f923083SAlexander Pyhalov            print self
2209f923083SAlexander Pyhalov            return
2219f923083SAlexander Pyhalov
2229f923083SAlexander Pyhalov        privcmd = self.update_cmd_privs(self.pathname, self.user)
2239f923083SAlexander Pyhalov        try:
2249f923083SAlexander Pyhalov            old = os.umask(0)
2259f923083SAlexander Pyhalov            if not os.path.isdir(self.outputdir):
2269f923083SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0777)
2279f923083SAlexander Pyhalov            os.umask(old)
2289f923083SAlexander Pyhalov        except OSError, e:
2299f923083SAlexander Pyhalov            fail('%s' % e)
2309f923083SAlexander Pyhalov
2319f923083SAlexander Pyhalov        try:
2329f923083SAlexander Pyhalov            self.result.starttime = time()
2339f923083SAlexander Pyhalov            proc = Popen(privcmd, stdout=PIPE, stderr=PIPE)
2349f923083SAlexander Pyhalov
2359f923083SAlexander Pyhalov            # Allow a special timeout value of 0 to mean infinity
2369f923083SAlexander Pyhalov            if int(self.timeout) == 0:
2379f923083SAlexander Pyhalov                self.timeout = maxint
2389f923083SAlexander Pyhalov            t = Timer(int(self.timeout), self.kill_cmd, [proc])
2399f923083SAlexander Pyhalov            t.start()
2409f923083SAlexander Pyhalov            self.result.stdout, self.result.stderr = self.collect_output(proc)
2419f923083SAlexander Pyhalov        except KeyboardInterrupt:
2429f923083SAlexander Pyhalov            self.kill_cmd(proc)
2439f923083SAlexander Pyhalov            fail('\nRun terminated at user request.')
2449f923083SAlexander Pyhalov        finally:
2459f923083SAlexander Pyhalov            t.cancel()
2469f923083SAlexander Pyhalov
2479f923083SAlexander Pyhalov        self.result.done(proc, self.killed)
2489f923083SAlexander Pyhalov
2499f923083SAlexander Pyhalov    def skip(self):
2509f923083SAlexander Pyhalov        """
2519f923083SAlexander Pyhalov        Initialize enough of the test result that we can log a skipped
2529f923083SAlexander Pyhalov        command.
2539f923083SAlexander Pyhalov        """
2549f923083SAlexander Pyhalov        Result.total += 1
2559f923083SAlexander Pyhalov        Result.runresults['SKIP'] += 1
2569f923083SAlexander Pyhalov        self.result.stdout = self.result.stderr = []
2579f923083SAlexander Pyhalov        self.result.starttime = time()
2589f923083SAlexander Pyhalov        m, s = divmod(time() - self.result.starttime, 60)
2599f923083SAlexander Pyhalov        self.result.runtime = '%02d:%02d' % (m, s)
2609f923083SAlexander Pyhalov        self.result.result = 'SKIP'
2619f923083SAlexander Pyhalov
2629f923083SAlexander Pyhalov    def log(self, logger, options):
2639f923083SAlexander Pyhalov        """
2649f923083SAlexander Pyhalov        This function is responsible for writing all output. This includes
2659f923083SAlexander Pyhalov        the console output, the logfile of all results (with timestamped
2669f923083SAlexander Pyhalov        merged stdout and stderr), and for each test, the unmodified
2679f923083SAlexander Pyhalov        stdout/stderr/merged in it's own file.
2689f923083SAlexander Pyhalov        """
2699f923083SAlexander Pyhalov        if logger is None:
2709f923083SAlexander Pyhalov            return
2719f923083SAlexander Pyhalov
2729f923083SAlexander Pyhalov        logname = getpwuid(os.getuid()).pw_name
2739f923083SAlexander Pyhalov        user = ' (run as %s)' % (self.user if len(self.user) else logname)
2749f923083SAlexander Pyhalov        msga = 'Test: %s%s ' % (self.pathname, user)
2759f923083SAlexander Pyhalov        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
2769f923083SAlexander Pyhalov        pad = ' ' * (80 - (len(msga) + len(msgb)))
2779f923083SAlexander Pyhalov
2789f923083SAlexander Pyhalov        # If -q is specified, only print a line for tests that didn't pass.
2799f923083SAlexander Pyhalov        # This means passing tests need to be logged as DEBUG, or the one
2809f923083SAlexander Pyhalov        # line summary will only be printed in the logfile for failures.
2819f923083SAlexander Pyhalov        if not options.quiet:
2829f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
2839f923083SAlexander Pyhalov        elif self.result.result is not 'PASS':
2849f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
2859f923083SAlexander Pyhalov        else:
2869f923083SAlexander Pyhalov            logger.debug('%s%s%s' % (msga, pad, msgb))
2879f923083SAlexander Pyhalov
2889f923083SAlexander Pyhalov        lines = sorted(self.result.stdout + self.result.stderr,
2899f923083SAlexander Pyhalov                       cmp=lambda x, y: cmp(x[0], y[0]))
2909f923083SAlexander Pyhalov
2919f923083SAlexander Pyhalov        for dt, line in lines:
2929f923083SAlexander Pyhalov            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
2939f923083SAlexander Pyhalov
2949f923083SAlexander Pyhalov        if len(self.result.stdout):
2959f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
2969f923083SAlexander Pyhalov                for _, line in self.result.stdout:
2979f923083SAlexander Pyhalov                    os.write(out.fileno(), '%s\n' % line)
2989f923083SAlexander Pyhalov        if len(self.result.stderr):
2999f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
3009f923083SAlexander Pyhalov                for _, line in self.result.stderr:
3019f923083SAlexander Pyhalov                    os.write(err.fileno(), '%s\n' % line)
3029f923083SAlexander Pyhalov        if len(self.result.stdout) and len(self.result.stderr):
3039f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
3049f923083SAlexander Pyhalov                for _, line in lines:
3059f923083SAlexander Pyhalov                    os.write(merged.fileno(), '%s\n' % line)
3069f923083SAlexander Pyhalov
3079f923083SAlexander Pyhalov
3089f923083SAlexander Pyhalovclass Test(Cmd):
3099f923083SAlexander Pyhalov    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
3109f923083SAlexander Pyhalov             'post_user']
3119f923083SAlexander Pyhalov
3129f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
3139f923083SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None):
3149f923083SAlexander Pyhalov        super(Test, self).__init__(pathname, outputdir, timeout, user)
3159f923083SAlexander Pyhalov        self.pre = pre or ''
3169f923083SAlexander Pyhalov        self.pre_user = pre_user or ''
3179f923083SAlexander Pyhalov        self.post = post or ''
3189f923083SAlexander Pyhalov        self.post_user = post_user or ''
3199f923083SAlexander Pyhalov
3209f923083SAlexander Pyhalov    def __str__(self):
3219f923083SAlexander Pyhalov        post_user = pre_user = ''
3229f923083SAlexander Pyhalov        if len(self.pre_user):
3239f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
3249f923083SAlexander Pyhalov        if len(self.post_user):
3259f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
3269f923083SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %d\nPre: %s%s\nPost: " \
3279f923083SAlexander Pyhalov               "%s%s\nUser: %s\n" % \
3289f923083SAlexander Pyhalov               (self.pathname, self.outputdir, self.timeout, self.pre,
3299f923083SAlexander Pyhalov                pre_user, self.post, post_user, self.user)
3309f923083SAlexander Pyhalov
3319f923083SAlexander Pyhalov    def verify(self, logger):
3329f923083SAlexander Pyhalov        """
3339f923083SAlexander Pyhalov        Check the pre/post scripts, user and Test. Omit the Test from this
3349f923083SAlexander Pyhalov        run if there are any problems.
3359f923083SAlexander Pyhalov        """
3369f923083SAlexander Pyhalov        files = [self.pre, self.pathname, self.post]
3379f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
3389f923083SAlexander Pyhalov
3399f923083SAlexander Pyhalov        for f in [f for f in files if len(f)]:
3409f923083SAlexander Pyhalov            if not verify_file(f):
3419f923083SAlexander Pyhalov                logger.info("Warning: Test '%s' not added to this run because"
3429f923083SAlexander Pyhalov                            " it failed verification." % f)
3439f923083SAlexander Pyhalov                return False
3449f923083SAlexander Pyhalov
3459f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
3469f923083SAlexander Pyhalov            if not verify_user(user, logger):
3479f923083SAlexander Pyhalov                logger.info("Not adding Test '%s' to this run." %
3489f923083SAlexander Pyhalov                            self.pathname)
3499f923083SAlexander Pyhalov                return False
3509f923083SAlexander Pyhalov
3519f923083SAlexander Pyhalov        return True
3529f923083SAlexander Pyhalov
3539f923083SAlexander Pyhalov    def run(self, logger, options):
3549f923083SAlexander Pyhalov        """
3559f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
3569f923083SAlexander Pyhalov        doesn't pass, skip this Test. Run the post script regardless.
3579f923083SAlexander Pyhalov        """
3589f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
3599f923083SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
3609f923083SAlexander Pyhalov                      user=self.pre_user)
3619f923083SAlexander Pyhalov        test = Cmd(self.pathname, outputdir=self.outputdir,
3629f923083SAlexander Pyhalov                   timeout=self.timeout, user=self.user)
3639f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
3649f923083SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
3659f923083SAlexander Pyhalov                       user=self.post_user)
3669f923083SAlexander Pyhalov
3679f923083SAlexander Pyhalov        cont = True
3689f923083SAlexander Pyhalov        if len(pretest.pathname):
3699f923083SAlexander Pyhalov            pretest.run(options)
3709f923083SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
3719f923083SAlexander Pyhalov            pretest.log(logger, options)
3729f923083SAlexander Pyhalov
3739f923083SAlexander Pyhalov        if cont:
3749f923083SAlexander Pyhalov            test.run(options)
3759f923083SAlexander Pyhalov        else:
3769f923083SAlexander Pyhalov            test.skip()
3779f923083SAlexander Pyhalov
3789f923083SAlexander Pyhalov        test.log(logger, options)
3799f923083SAlexander Pyhalov
3809f923083SAlexander Pyhalov        if len(posttest.pathname):
3819f923083SAlexander Pyhalov            posttest.run(options)
3829f923083SAlexander Pyhalov            posttest.log(logger, options)
3839f923083SAlexander Pyhalov
3849f923083SAlexander Pyhalov
3859f923083SAlexander Pyhalovclass TestGroup(Test):
3869f923083SAlexander Pyhalov    props = Test.props + ['tests']
3879f923083SAlexander Pyhalov
3889f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
3899f923083SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None,
3909f923083SAlexander Pyhalov                 tests=None):
3919f923083SAlexander Pyhalov        super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
3929f923083SAlexander Pyhalov                                        pre, pre_user, post, post_user)
3939f923083SAlexander Pyhalov        self.tests = tests or []
3949f923083SAlexander Pyhalov
3959f923083SAlexander Pyhalov    def __str__(self):
3969f923083SAlexander Pyhalov        post_user = pre_user = ''
3979f923083SAlexander Pyhalov        if len(self.pre_user):
3989f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
3999f923083SAlexander Pyhalov        if len(self.post_user):
4009f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
4019f923083SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %d\n" \
4029f923083SAlexander Pyhalov               "Pre: %s%s\nPost: %s%s\nUser: %s\n" % \
4039f923083SAlexander Pyhalov               (self.pathname, self.outputdir, self.tests, self.timeout,
4049f923083SAlexander Pyhalov                self.pre, pre_user, self.post, post_user, self.user)
4059f923083SAlexander Pyhalov
4069f923083SAlexander Pyhalov    def verify(self, logger):
4079f923083SAlexander Pyhalov        """
4089f923083SAlexander Pyhalov        Check the pre/post scripts, user and tests in this TestGroup. Omit
4099f923083SAlexander Pyhalov        the TestGroup entirely, or simply delete the relevant tests in the
4109f923083SAlexander Pyhalov        group, if that's all that's required.
4119f923083SAlexander Pyhalov        """
4129f923083SAlexander Pyhalov        # If the pre or post scripts are relative pathnames, convert to
4139f923083SAlexander Pyhalov        # absolute, so they stand a chance of passing verification.
4149f923083SAlexander Pyhalov        if len(self.pre) and not os.path.isabs(self.pre):
4159f923083SAlexander Pyhalov            self.pre = os.path.join(self.pathname, self.pre)
4169f923083SAlexander Pyhalov        if len(self.post) and not os.path.isabs(self.post):
4179f923083SAlexander Pyhalov            self.post = os.path.join(self.pathname, self.post)
4189f923083SAlexander Pyhalov
4199f923083SAlexander Pyhalov        auxfiles = [self.pre, self.post]
4209f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
4219f923083SAlexander Pyhalov
4229f923083SAlexander Pyhalov        for f in [f for f in auxfiles if len(f)]:
4239f923083SAlexander Pyhalov            if self.pathname != os.path.dirname(f):
4249f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4259f923083SAlexander Pyhalov                            "Auxiliary script '%s' exists in a different "
4269f923083SAlexander Pyhalov                            "directory." % (self.pathname, f))
4279f923083SAlexander Pyhalov                return False
4289f923083SAlexander Pyhalov
4299f923083SAlexander Pyhalov            if not verify_file(f):
4309f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4319f923083SAlexander Pyhalov                            "Auxiliary script '%s' failed verification." %
4329f923083SAlexander Pyhalov                            (self.pathname, f))
4339f923083SAlexander Pyhalov                return False
4349f923083SAlexander Pyhalov
4359f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
4369f923083SAlexander Pyhalov            if not verify_user(user, logger):
4379f923083SAlexander Pyhalov                logger.info("Not adding TestGroup '%s' to this run." %
4389f923083SAlexander Pyhalov                            self.pathname)
4399f923083SAlexander Pyhalov                return False
4409f923083SAlexander Pyhalov
4419f923083SAlexander Pyhalov        # If one of the tests is invalid, delete it, log it, and drive on.
442dfc11533SChris Williamson        self.tests[:] = [f for f in self.tests if
443dfc11533SChris Williamson          verify_file(os.path.join(self.pathname, f))]
4449f923083SAlexander Pyhalov
4459f923083SAlexander Pyhalov        return len(self.tests) is not 0
4469f923083SAlexander Pyhalov
4479f923083SAlexander Pyhalov    def run(self, logger, options):
4489f923083SAlexander Pyhalov        """
4499f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
4509f923083SAlexander Pyhalov        doesn't pass, skip all the tests in this TestGroup. Run the post
4519f923083SAlexander Pyhalov        script regardless.
4529f923083SAlexander Pyhalov        """
4539f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
4549f923083SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
4559f923083SAlexander Pyhalov                      user=self.pre_user)
4569f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
4579f923083SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
4589f923083SAlexander Pyhalov                       user=self.post_user)
4599f923083SAlexander Pyhalov
4609f923083SAlexander Pyhalov        cont = True
4619f923083SAlexander Pyhalov        if len(pretest.pathname):
4629f923083SAlexander Pyhalov            pretest.run(options)
4639f923083SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
4649f923083SAlexander Pyhalov            pretest.log(logger, options)
4659f923083SAlexander Pyhalov
4669f923083SAlexander Pyhalov        for fname in self.tests:
4679f923083SAlexander Pyhalov            test = Cmd(os.path.join(self.pathname, fname),
4689f923083SAlexander Pyhalov                       outputdir=os.path.join(self.outputdir, fname),
4699f923083SAlexander Pyhalov                       timeout=self.timeout, user=self.user)
4709f923083SAlexander Pyhalov            if cont:
4719f923083SAlexander Pyhalov                test.run(options)
4729f923083SAlexander Pyhalov            else:
4739f923083SAlexander Pyhalov                test.skip()
4749f923083SAlexander Pyhalov
4759f923083SAlexander Pyhalov            test.log(logger, options)
4769f923083SAlexander Pyhalov
4779f923083SAlexander Pyhalov        if len(posttest.pathname):
4789f923083SAlexander Pyhalov            posttest.run(options)
4799f923083SAlexander Pyhalov            posttest.log(logger, options)
4809f923083SAlexander Pyhalov
4819f923083SAlexander Pyhalov
4829f923083SAlexander Pyhalovclass TestRun(object):
4839f923083SAlexander Pyhalov    props = ['quiet', 'outputdir']
4849f923083SAlexander Pyhalov
4859f923083SAlexander Pyhalov    def __init__(self, options):
4869f923083SAlexander Pyhalov        self.tests = {}
4879f923083SAlexander Pyhalov        self.testgroups = {}
4889f923083SAlexander Pyhalov        self.starttime = time()
4899f923083SAlexander Pyhalov        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
4909f923083SAlexander Pyhalov        self.outputdir = os.path.join(options.outputdir, self.timestamp)
4919f923083SAlexander Pyhalov        self.logger = self.setup_logging(options)
4929f923083SAlexander Pyhalov        self.defaults = [
4939f923083SAlexander Pyhalov            ('outputdir', BASEDIR),
4949f923083SAlexander Pyhalov            ('quiet', False),
4959f923083SAlexander Pyhalov            ('timeout', 60),
4969f923083SAlexander Pyhalov            ('user', ''),
4979f923083SAlexander Pyhalov            ('pre', ''),
4989f923083SAlexander Pyhalov            ('pre_user', ''),
4999f923083SAlexander Pyhalov            ('post', ''),
5009f923083SAlexander Pyhalov            ('post_user', '')
5019f923083SAlexander Pyhalov        ]
5029f923083SAlexander Pyhalov
5039f923083SAlexander Pyhalov    def __str__(self):
5049f923083SAlexander Pyhalov        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
5059f923083SAlexander Pyhalov        s += 'TESTS:\n'
5069f923083SAlexander Pyhalov        for key in sorted(self.tests.keys()):
5079f923083SAlexander Pyhalov            s += '%s%s' % (self.tests[key].__str__(), '\n')
5089f923083SAlexander Pyhalov        s += 'TESTGROUPS:\n'
5099f923083SAlexander Pyhalov        for key in sorted(self.testgroups.keys()):
5109f923083SAlexander Pyhalov            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
5119f923083SAlexander Pyhalov        return s
5129f923083SAlexander Pyhalov
5139f923083SAlexander Pyhalov    def addtest(self, pathname, options):
5149f923083SAlexander Pyhalov        """
5159f923083SAlexander Pyhalov        Create a new Test, and apply any properties that were passed in
5169f923083SAlexander Pyhalov        from the command line. If it passes verification, add it to the
5179f923083SAlexander Pyhalov        TestRun.
5189f923083SAlexander Pyhalov        """
5199f923083SAlexander Pyhalov        test = Test(pathname)
5209f923083SAlexander Pyhalov        for prop in Test.props:
5219f923083SAlexander Pyhalov            setattr(test, prop, getattr(options, prop))
5229f923083SAlexander Pyhalov
5239f923083SAlexander Pyhalov        if test.verify(self.logger):
5249f923083SAlexander Pyhalov            self.tests[pathname] = test
5259f923083SAlexander Pyhalov
5269f923083SAlexander Pyhalov    def addtestgroup(self, dirname, filenames, options):
5279f923083SAlexander Pyhalov        """
5289f923083SAlexander Pyhalov        Create a new TestGroup, and apply any properties that were passed
5299f923083SAlexander Pyhalov        in from the command line. If it passes verification, add it to the
5309f923083SAlexander Pyhalov        TestRun.
5319f923083SAlexander Pyhalov        """
5329f923083SAlexander Pyhalov        if dirname not in self.testgroups:
5339f923083SAlexander Pyhalov            testgroup = TestGroup(dirname)
5349f923083SAlexander Pyhalov            for prop in Test.props:
5359f923083SAlexander Pyhalov                setattr(testgroup, prop, getattr(options, prop))
5369f923083SAlexander Pyhalov
5379f923083SAlexander Pyhalov            # Prevent pre/post scripts from running as regular tests
5389f923083SAlexander Pyhalov            for f in [testgroup.pre, testgroup.post]:
5399f923083SAlexander Pyhalov                if f in filenames:
5409f923083SAlexander Pyhalov                    del filenames[filenames.index(f)]
5419f923083SAlexander Pyhalov
5429f923083SAlexander Pyhalov            self.testgroups[dirname] = testgroup
5439f923083SAlexander Pyhalov            self.testgroups[dirname].tests = sorted(filenames)
5449f923083SAlexander Pyhalov
5459f923083SAlexander Pyhalov            testgroup.verify(self.logger)
5469f923083SAlexander Pyhalov
5479f923083SAlexander Pyhalov    def read(self, logger, options):
5489f923083SAlexander Pyhalov        """
5499f923083SAlexander Pyhalov        Read in the specified runfile, and apply the TestRun properties
5509f923083SAlexander Pyhalov        listed in the 'DEFAULT' section to our TestRun. Then read each
5519f923083SAlexander Pyhalov        section, and apply the appropriate properties to the Test or
5529f923083SAlexander Pyhalov        TestGroup. Properties from individual sections override those set
5539f923083SAlexander Pyhalov        in the 'DEFAULT' section. If the Test or TestGroup passes
5549f923083SAlexander Pyhalov        verification, add it to the TestRun.
5559f923083SAlexander Pyhalov        """
5569f923083SAlexander Pyhalov        config = ConfigParser.RawConfigParser()
5579f923083SAlexander Pyhalov        if not len(config.read(options.runfile)):
5589f923083SAlexander Pyhalov            fail("Coulnd't read config file %s" % options.runfile)
5599f923083SAlexander Pyhalov
5609f923083SAlexander Pyhalov        for opt in TestRun.props:
5619f923083SAlexander Pyhalov            if config.has_option('DEFAULT', opt):
5629f923083SAlexander Pyhalov                setattr(self, opt, config.get('DEFAULT', opt))
5639f923083SAlexander Pyhalov        self.outputdir = os.path.join(self.outputdir, self.timestamp)
5649f923083SAlexander Pyhalov
5659f923083SAlexander Pyhalov        for section in config.sections():
5669f923083SAlexander Pyhalov            if 'tests' in config.options(section):
5679f923083SAlexander Pyhalov                testgroup = TestGroup(section)
5689f923083SAlexander Pyhalov                for prop in TestGroup.props:
5699f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
5709f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
5719f923083SAlexander Pyhalov                            setattr(testgroup, prop, config.get(sect, prop))
5729f923083SAlexander Pyhalov
5739f923083SAlexander Pyhalov                # Repopulate tests using eval to convert the string to a list
5749f923083SAlexander Pyhalov                testgroup.tests = eval(config.get(section, 'tests'))
5759f923083SAlexander Pyhalov
5769f923083SAlexander Pyhalov                if testgroup.verify(logger):
5779f923083SAlexander Pyhalov                    self.testgroups[section] = testgroup
578dfc11533SChris Williamson
579dfc11533SChris Williamson            elif 'autotests' in config.options(section):
580dfc11533SChris Williamson                testgroup = TestGroup(section)
581dfc11533SChris Williamson                for prop in TestGroup.props:
582dfc11533SChris Williamson                    for sect in ['DEFAULT', section]:
583dfc11533SChris Williamson                        if config.has_option(sect, prop):
584dfc11533SChris Williamson                            setattr(testgroup, prop, config.get(sect, prop))
585dfc11533SChris Williamson
586dfc11533SChris Williamson                filenames = os.listdir(section)
587dfc11533SChris Williamson                # only files starting with "tst." are considered tests
588dfc11533SChris Williamson                filenames = [f for f in filenames if f.startswith("tst.")]
589dfc11533SChris Williamson                testgroup.tests = sorted(filenames)
590dfc11533SChris Williamson
591dfc11533SChris Williamson                if testgroup.verify(logger):
592dfc11533SChris Williamson                    self.testgroups[section] = testgroup
593dfc11533SChris Williamson
5949f923083SAlexander Pyhalov            else:
5959f923083SAlexander Pyhalov                test = Test(section)
5969f923083SAlexander Pyhalov                for prop in Test.props:
5979f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
5989f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
5999f923083SAlexander Pyhalov                            setattr(test, prop, config.get(sect, prop))
6009f923083SAlexander Pyhalov
6019f923083SAlexander Pyhalov                if test.verify(logger):
6029f923083SAlexander Pyhalov                    self.tests[section] = test
6039f923083SAlexander Pyhalov
6049f923083SAlexander Pyhalov    def write(self, options):
6059f923083SAlexander Pyhalov        """
6069f923083SAlexander Pyhalov        Create a configuration file for editing and later use. The
6079f923083SAlexander Pyhalov        'DEFAULT' section of the config file is created from the
6089f923083SAlexander Pyhalov        properties that were specified on the command line. Tests are
6099f923083SAlexander Pyhalov        simply added as sections that inherit everything from the
6109f923083SAlexander Pyhalov        'DEFAULT' section. TestGroups are the same, except they get an
6119f923083SAlexander Pyhalov        option including all the tests to run in that directory.
6129f923083SAlexander Pyhalov        """
6139f923083SAlexander Pyhalov
6149f923083SAlexander Pyhalov        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
6159f923083SAlexander Pyhalov                         self.defaults])
6169f923083SAlexander Pyhalov        config = ConfigParser.RawConfigParser(defaults)
6179f923083SAlexander Pyhalov
6189f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
6199f923083SAlexander Pyhalov            config.add_section(test)
6209f923083SAlexander Pyhalov
6219f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
6229f923083SAlexander Pyhalov            config.add_section(testgroup)
6239f923083SAlexander Pyhalov            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
6249f923083SAlexander Pyhalov
6259f923083SAlexander Pyhalov        try:
6269f923083SAlexander Pyhalov            with open(options.template, 'w') as f:
6279f923083SAlexander Pyhalov                return config.write(f)
6289f923083SAlexander Pyhalov        except IOError:
6299f923083SAlexander Pyhalov            fail('Could not open \'%s\' for writing.' % options.template)
6309f923083SAlexander Pyhalov
6319f923083SAlexander Pyhalov    def complete_outputdirs(self):
6329f923083SAlexander Pyhalov        """
6339f923083SAlexander Pyhalov        Collect all the pathnames for Tests, and TestGroups. Work
6349f923083SAlexander Pyhalov        backwards one pathname component at a time, to create a unique
6359f923083SAlexander Pyhalov        directory name in which to deposit test output. Tests will be able
6369f923083SAlexander Pyhalov        to write output files directly in the newly modified outputdir.
6379f923083SAlexander Pyhalov        TestGroups will be able to create one subdirectory per test in the
6389f923083SAlexander Pyhalov        outputdir, and are guaranteed uniqueness because a group can only
6399f923083SAlexander Pyhalov        contain files in one directory. Pre and post tests will create a
6409f923083SAlexander Pyhalov        directory rooted at the outputdir of the Test or TestGroup in
6419f923083SAlexander Pyhalov        question for their output.
6429f923083SAlexander Pyhalov        """
6439f923083SAlexander Pyhalov        done = False
6449f923083SAlexander Pyhalov        components = 0
6459f923083SAlexander Pyhalov        tmp_dict = dict(self.tests.items() + self.testgroups.items())
6469f923083SAlexander Pyhalov        total = len(tmp_dict)
6479f923083SAlexander Pyhalov        base = self.outputdir
6489f923083SAlexander Pyhalov
6499f923083SAlexander Pyhalov        while not done:
6509f923083SAlexander Pyhalov            l = []
6519f923083SAlexander Pyhalov            components -= 1
6529f923083SAlexander Pyhalov            for testfile in tmp_dict.keys():
6539f923083SAlexander Pyhalov                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
6549f923083SAlexander Pyhalov                if uniq not in l:
6559f923083SAlexander Pyhalov                    l.append(uniq)
6569f923083SAlexander Pyhalov                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
6579f923083SAlexander Pyhalov                else:
6589f923083SAlexander Pyhalov                    break
6599f923083SAlexander Pyhalov            done = total == len(l)
6609f923083SAlexander Pyhalov
6619f923083SAlexander Pyhalov    def setup_logging(self, options):
6629f923083SAlexander Pyhalov        """
6639f923083SAlexander Pyhalov        Two loggers are set up here. The first is for the logfile which
6649f923083SAlexander Pyhalov        will contain one line summarizing the test, including the test
6659f923083SAlexander Pyhalov        name, result, and running time. This logger will also capture the
6669f923083SAlexander Pyhalov        timestamped combined stdout and stderr of each run. The second
6679f923083SAlexander Pyhalov        logger is optional console output, which will contain only the one
6689f923083SAlexander Pyhalov        line summary. The loggers are initialized at two different levels
6699f923083SAlexander Pyhalov        to facilitate segregating the output.
6709f923083SAlexander Pyhalov        """
6719f923083SAlexander Pyhalov        if options.dryrun is True:
6729f923083SAlexander Pyhalov            return
6739f923083SAlexander Pyhalov
6749f923083SAlexander Pyhalov        testlogger = logging.getLogger(__name__)
6759f923083SAlexander Pyhalov        testlogger.setLevel(logging.DEBUG)
6769f923083SAlexander Pyhalov
6779f923083SAlexander Pyhalov        if options.cmd is not 'wrconfig':
6789f923083SAlexander Pyhalov            try:
6799f923083SAlexander Pyhalov                old = os.umask(0)
6809f923083SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0777)
6819f923083SAlexander Pyhalov                os.umask(old)
6829f923083SAlexander Pyhalov            except OSError, e:
6839f923083SAlexander Pyhalov                fail('%s' % e)
6849f923083SAlexander Pyhalov            filename = os.path.join(self.outputdir, 'log')
6859f923083SAlexander Pyhalov
6869f923083SAlexander Pyhalov            logfile = WatchedFileHandlerClosed(filename)
6879f923083SAlexander Pyhalov            logfile.setLevel(logging.DEBUG)
6889f923083SAlexander Pyhalov            logfilefmt = logging.Formatter('%(message)s')
6899f923083SAlexander Pyhalov            logfile.setFormatter(logfilefmt)
6909f923083SAlexander Pyhalov            testlogger.addHandler(logfile)
6919f923083SAlexander Pyhalov
6929f923083SAlexander Pyhalov        cons = logging.StreamHandler()
6939f923083SAlexander Pyhalov        cons.setLevel(logging.INFO)
6949f923083SAlexander Pyhalov        consfmt = logging.Formatter('%(message)s')
6959f923083SAlexander Pyhalov        cons.setFormatter(consfmt)
6969f923083SAlexander Pyhalov        testlogger.addHandler(cons)
6979f923083SAlexander Pyhalov
6989f923083SAlexander Pyhalov        return testlogger
6999f923083SAlexander Pyhalov
7009f923083SAlexander Pyhalov    def run(self, options):
7019f923083SAlexander Pyhalov        """
7029f923083SAlexander Pyhalov        Walk through all the Tests and TestGroups, calling run().
7039f923083SAlexander Pyhalov        """
704*64ee6612SChris Fraire        if not options.dryrun:
7059f923083SAlexander Pyhalov            try:
7069f923083SAlexander Pyhalov                os.chdir(self.outputdir)
7079f923083SAlexander Pyhalov            except OSError:
7089f923083SAlexander Pyhalov                fail('Could not change to directory %s' % self.outputdir)
7099f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
7109f923083SAlexander Pyhalov            self.tests[test].run(self.logger, options)
7119f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
7129f923083SAlexander Pyhalov            self.testgroups[testgroup].run(self.logger, options)
7139f923083SAlexander Pyhalov
7149f923083SAlexander Pyhalov    def summary(self):
7159f923083SAlexander Pyhalov        if Result.total is 0:
7169f923083SAlexander Pyhalov            return
7179f923083SAlexander Pyhalov
7189f923083SAlexander Pyhalov        print '\nResults Summary'
7199f923083SAlexander Pyhalov        for key in Result.runresults.keys():
7209f923083SAlexander Pyhalov            if Result.runresults[key] is not 0:
7219f923083SAlexander Pyhalov                print '%s\t% 4d' % (key, Result.runresults[key])
7229f923083SAlexander Pyhalov
7239f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
7249f923083SAlexander Pyhalov        h, m = divmod(m, 60)
7259f923083SAlexander Pyhalov        print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
7269f923083SAlexander Pyhalov        print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
7279f923083SAlexander Pyhalov                                            float(Result.total)) * 100)
7289f923083SAlexander Pyhalov        print 'Log directory:\t%s' % self.outputdir
7299f923083SAlexander Pyhalov
7309f923083SAlexander Pyhalov
7319f923083SAlexander Pyhalovdef verify_file(pathname):
7329f923083SAlexander Pyhalov    """
7339f923083SAlexander Pyhalov    Verify that the supplied pathname is an executable regular file.
7349f923083SAlexander Pyhalov    """
7359f923083SAlexander Pyhalov    if os.path.isdir(pathname) or os.path.islink(pathname):
7369f923083SAlexander Pyhalov        return False
7379f923083SAlexander Pyhalov
7389f923083SAlexander Pyhalov    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
7399f923083SAlexander Pyhalov        return True
7409f923083SAlexander Pyhalov
7419f923083SAlexander Pyhalov    return False
7429f923083SAlexander Pyhalov
7439f923083SAlexander Pyhalov
7449f923083SAlexander Pyhalovdef verify_user(user, logger):
7459f923083SAlexander Pyhalov    """
7469f923083SAlexander Pyhalov    Verify that the specified user exists on this system, and can execute
7479f923083SAlexander Pyhalov    sudo without being prompted for a password.
7489f923083SAlexander Pyhalov    """
7499f923083SAlexander Pyhalov    testcmd = [SUDO, '-n', '-u', user, TRUE]
7509f923083SAlexander Pyhalov
7519f923083SAlexander Pyhalov    if user in Cmd.verified_users:
7529f923083SAlexander Pyhalov        return True
7539f923083SAlexander Pyhalov
7549f923083SAlexander Pyhalov    try:
7559f923083SAlexander Pyhalov        _ = getpwnam(user)
7569f923083SAlexander Pyhalov    except KeyError:
7579f923083SAlexander Pyhalov        logger.info("Warning: user '%s' does not exist.", user)
7589f923083SAlexander Pyhalov        return False
7599f923083SAlexander Pyhalov
7609f923083SAlexander Pyhalov    p = Popen(testcmd)
7619f923083SAlexander Pyhalov    p.wait()
7629f923083SAlexander Pyhalov    if p.returncode is not 0:
7639f923083SAlexander Pyhalov        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
7649f923083SAlexander Pyhalov        return False
7659f923083SAlexander Pyhalov    else:
7669f923083SAlexander Pyhalov        Cmd.verified_users.append(user)
7679f923083SAlexander Pyhalov
7689f923083SAlexander Pyhalov    return True
7699f923083SAlexander Pyhalov
7709f923083SAlexander Pyhalov
7719f923083SAlexander Pyhalovdef find_tests(testrun, options):
7729f923083SAlexander Pyhalov    """
7739f923083SAlexander Pyhalov    For the given list of pathnames, add files as Tests. For directories,
7749f923083SAlexander Pyhalov    if do_groups is True, add the directory as a TestGroup. If False,
7759f923083SAlexander Pyhalov    recursively search for executable files.
7769f923083SAlexander Pyhalov    """
7779f923083SAlexander Pyhalov
7789f923083SAlexander Pyhalov    for p in sorted(options.pathnames):
7799f923083SAlexander Pyhalov        if os.path.isdir(p):
7809f923083SAlexander Pyhalov            for dirname, _, filenames in os.walk(p):
7819f923083SAlexander Pyhalov                if options.do_groups:
7829f923083SAlexander Pyhalov                    testrun.addtestgroup(dirname, filenames, options)
7839f923083SAlexander Pyhalov                else:
7849f923083SAlexander Pyhalov                    for f in sorted(filenames):
7859f923083SAlexander Pyhalov                        testrun.addtest(os.path.join(dirname, f), options)
7869f923083SAlexander Pyhalov        else:
7879f923083SAlexander Pyhalov            testrun.addtest(p, options)
7889f923083SAlexander Pyhalov
7899f923083SAlexander Pyhalov
7909f923083SAlexander Pyhalovdef fail(retstr, ret=1):
7919f923083SAlexander Pyhalov    print '%s: %s' % (argv[0], retstr)
7929f923083SAlexander Pyhalov    exit(ret)
7939f923083SAlexander Pyhalov
7949f923083SAlexander Pyhalov
7959f923083SAlexander Pyhalovdef options_cb(option, opt_str, value, parser):
7969f923083SAlexander Pyhalov    path_options = ['runfile', 'outputdir', 'template']
7979f923083SAlexander Pyhalov
7989f923083SAlexander Pyhalov    if option.dest is 'runfile' and '-w' in parser.rargs or \
7999f923083SAlexander Pyhalov            option.dest is 'template' and '-c' in parser.rargs:
8009f923083SAlexander Pyhalov        fail('-c and -w are mutually exclusive.')
8019f923083SAlexander Pyhalov
8029f923083SAlexander Pyhalov    if opt_str in parser.rargs:
8039f923083SAlexander Pyhalov        fail('%s may only be specified once.' % opt_str)
8049f923083SAlexander Pyhalov
8059f923083SAlexander Pyhalov    if option.dest is 'runfile':
8069f923083SAlexander Pyhalov        parser.values.cmd = 'rdconfig'
8079f923083SAlexander Pyhalov    if option.dest is 'template':
8089f923083SAlexander Pyhalov        parser.values.cmd = 'wrconfig'
8099f923083SAlexander Pyhalov
8109f923083SAlexander Pyhalov    setattr(parser.values, option.dest, value)
8119f923083SAlexander Pyhalov    if option.dest in path_options:
8129f923083SAlexander Pyhalov        setattr(parser.values, option.dest, os.path.abspath(value))
8139f923083SAlexander Pyhalov
8149f923083SAlexander Pyhalov
8159f923083SAlexander Pyhalovdef parse_args():
8169f923083SAlexander Pyhalov    parser = OptionParser()
8179f923083SAlexander Pyhalov    parser.add_option('-c', action='callback', callback=options_cb,
8189f923083SAlexander Pyhalov                      type='string', dest='runfile', metavar='runfile',
8199f923083SAlexander Pyhalov                      help='Specify tests to run via config file.')
8209f923083SAlexander Pyhalov    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
8219f923083SAlexander Pyhalov                      help='Dry run. Print tests, but take no other action.')
8229f923083SAlexander Pyhalov    parser.add_option('-g', action='store_true', default=False,
8239f923083SAlexander Pyhalov                      dest='do_groups', help='Make directories TestGroups.')
8249f923083SAlexander Pyhalov    parser.add_option('-o', action='callback', callback=options_cb,
8259f923083SAlexander Pyhalov                      default=BASEDIR, dest='outputdir', type='string',
8269f923083SAlexander Pyhalov                      metavar='outputdir', help='Specify an output directory.')
8279f923083SAlexander Pyhalov    parser.add_option('-p', action='callback', callback=options_cb,
8289f923083SAlexander Pyhalov                      default='', dest='pre', metavar='script',
8299f923083SAlexander Pyhalov                      type='string', help='Specify a pre script.')
8309f923083SAlexander Pyhalov    parser.add_option('-P', action='callback', callback=options_cb,
8319f923083SAlexander Pyhalov                      default='', dest='post', metavar='script',
8329f923083SAlexander Pyhalov                      type='string', help='Specify a post script.')
8339f923083SAlexander Pyhalov    parser.add_option('-q', action='store_true', default=False, dest='quiet',
8349f923083SAlexander Pyhalov                      help='Silence on the console during a test run.')
8359f923083SAlexander Pyhalov    parser.add_option('-t', action='callback', callback=options_cb, default=60,
8369f923083SAlexander Pyhalov                      dest='timeout', metavar='seconds', type='int',
8379f923083SAlexander Pyhalov                      help='Timeout (in seconds) for an individual test.')
8389f923083SAlexander Pyhalov    parser.add_option('-u', action='callback', callback=options_cb,
8399f923083SAlexander Pyhalov                      default='', dest='user', metavar='user', type='string',
8409f923083SAlexander Pyhalov                      help='Specify a different user name to run as.')
8419f923083SAlexander Pyhalov    parser.add_option('-w', action='callback', callback=options_cb,
8429f923083SAlexander Pyhalov                      default=None, dest='template', metavar='template',
8439f923083SAlexander Pyhalov                      type='string', help='Create a new config file.')
8449f923083SAlexander Pyhalov    parser.add_option('-x', action='callback', callback=options_cb, default='',
8459f923083SAlexander Pyhalov                      dest='pre_user', metavar='pre_user', type='string',
8469f923083SAlexander Pyhalov                      help='Specify a user to execute the pre script.')
8479f923083SAlexander Pyhalov    parser.add_option('-X', action='callback', callback=options_cb, default='',
8489f923083SAlexander Pyhalov                      dest='post_user', metavar='post_user', type='string',
8499f923083SAlexander Pyhalov                      help='Specify a user to execute the post script.')
8509f923083SAlexander Pyhalov    (options, pathnames) = parser.parse_args()
8519f923083SAlexander Pyhalov
8529f923083SAlexander Pyhalov    if not options.runfile and not options.template:
8539f923083SAlexander Pyhalov        options.cmd = 'runtests'
8549f923083SAlexander Pyhalov
8559f923083SAlexander Pyhalov    if options.runfile and len(pathnames):
8569f923083SAlexander Pyhalov        fail('Extraneous arguments.')
8579f923083SAlexander Pyhalov
8589f923083SAlexander Pyhalov    options.pathnames = [os.path.abspath(path) for path in pathnames]
8599f923083SAlexander Pyhalov
8609f923083SAlexander Pyhalov    return options
8619f923083SAlexander Pyhalov
8629f923083SAlexander Pyhalov
8639f923083SAlexander Pyhalovdef main():
8649f923083SAlexander Pyhalov    options = parse_args()
8659f923083SAlexander Pyhalov    testrun = TestRun(options)
8669f923083SAlexander Pyhalov
8679f923083SAlexander Pyhalov    if options.cmd is 'runtests':
8689f923083SAlexander Pyhalov        find_tests(testrun, options)
8699f923083SAlexander Pyhalov    elif options.cmd is 'rdconfig':
8709f923083SAlexander Pyhalov        testrun.read(testrun.logger, options)
8719f923083SAlexander Pyhalov    elif options.cmd is 'wrconfig':
8729f923083SAlexander Pyhalov        find_tests(testrun, options)
8739f923083SAlexander Pyhalov        testrun.write(options)
8749f923083SAlexander Pyhalov        exit(0)
8759f923083SAlexander Pyhalov    else:
8769f923083SAlexander Pyhalov        fail('Unknown command specified')
8779f923083SAlexander Pyhalov
8789f923083SAlexander Pyhalov    testrun.complete_outputdirs()
8799f923083SAlexander Pyhalov    testrun.run(options)
8809f923083SAlexander Pyhalov    testrun.summary()
8819f923083SAlexander Pyhalov    exit(0)
8829f923083SAlexander Pyhalov
8839f923083SAlexander Pyhalov
8849f923083SAlexander Pyhalovif __name__ == '__main__':
8859f923083SAlexander Pyhalov    main()
886