xref: /titanic_50/usr/src/test/test-runner/cmd/run (revision 83d13c2ebd59665a62ceb7ce4a99a349794efaaf)
124bb1048SAlexander Pyhalov#!@PYTHON@
224bb1048SAlexander Pyhalov
324bb1048SAlexander Pyhalov#
424bb1048SAlexander Pyhalov# This file and its contents are supplied under the terms of the
524bb1048SAlexander Pyhalov# Common Development and Distribution License ("CDDL"), version 1.0.
624bb1048SAlexander Pyhalov# You may only use this file in accordance with the terms of version
724bb1048SAlexander Pyhalov# 1.0 of the CDDL.
824bb1048SAlexander Pyhalov#
924bb1048SAlexander Pyhalov# A full copy of the text of the CDDL should have accompanied this
1024bb1048SAlexander Pyhalov# source.  A copy of the CDDL is also available via the Internet at
1124bb1048SAlexander Pyhalov# http://www.illumos.org/license/CDDL.
1224bb1048SAlexander Pyhalov#
1324bb1048SAlexander Pyhalov
1424bb1048SAlexander Pyhalov#
1524bb1048SAlexander Pyhalov# Copyright (c) 2012, 2015 by Delphix. All rights reserved.
1624bb1048SAlexander Pyhalov#
1724bb1048SAlexander Pyhalov
18*83d13c2eSAlexander Pyhalovfrom __future__ import print_function
19*83d13c2eSAlexander Pyhalovimport sys
20*83d13c2eSAlexander PyhalovPY3 = sys.version_info[0] == 3
21*83d13c2eSAlexander Pyhalov
22*83d13c2eSAlexander Pyhalovif PY3:
23*83d13c2eSAlexander Pyhalov    import configparser
24*83d13c2eSAlexander Pyhalovelse:
25*83d13c2eSAlexander Pyhalov    import ConfigParser as configparser
26*83d13c2eSAlexander Pyhalov
2724bb1048SAlexander Pyhalovimport os
2824bb1048SAlexander Pyhalovimport logging
2924bb1048SAlexander Pyhalovfrom logging.handlers import WatchedFileHandler
3024bb1048SAlexander Pyhalovfrom datetime import datetime
3124bb1048SAlexander Pyhalovfrom optparse import OptionParser
3224bb1048SAlexander Pyhalovfrom pwd import getpwnam
3324bb1048SAlexander Pyhalovfrom pwd import getpwuid
3424bb1048SAlexander Pyhalovfrom select import select
3524bb1048SAlexander Pyhalovfrom subprocess import PIPE
3624bb1048SAlexander Pyhalovfrom subprocess import Popen
3724bb1048SAlexander Pyhalovfrom sys import argv
3824bb1048SAlexander Pyhalovfrom sys import exit
39*83d13c2eSAlexander Pyhalovfrom sys import maxsize
4024bb1048SAlexander Pyhalovfrom threading import Timer
4124bb1048SAlexander Pyhalovfrom time import time
4224bb1048SAlexander Pyhalov
4324bb1048SAlexander PyhalovBASEDIR = '/var/tmp/test_results'
4424bb1048SAlexander PyhalovKILL = '/usr/bin/kill'
4524bb1048SAlexander PyhalovTRUE = '/usr/bin/true'
4624bb1048SAlexander PyhalovSUDO = '/usr/bin/sudo'
4724bb1048SAlexander Pyhalov
4824bb1048SAlexander Pyhalov# Custom class to reopen the log file in case it is forcibly closed by a test.
4924bb1048SAlexander Pyhalovclass WatchedFileHandlerClosed(WatchedFileHandler):
5024bb1048SAlexander Pyhalov    """Watch files, including closed files.
5124bb1048SAlexander Pyhalov    Similar to (and inherits from) logging.handler.WatchedFileHandler,
5224bb1048SAlexander Pyhalov    except that IOErrors are handled by reopening the stream and retrying.
5324bb1048SAlexander Pyhalov    This will be retried up to a configurable number of times before
5424bb1048SAlexander Pyhalov    giving up, default 5.
5524bb1048SAlexander Pyhalov    """
5624bb1048SAlexander Pyhalov
5724bb1048SAlexander Pyhalov    def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
5824bb1048SAlexander Pyhalov        self.max_tries = max_tries
5924bb1048SAlexander Pyhalov        self.tries = 0
6024bb1048SAlexander Pyhalov        WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
6124bb1048SAlexander Pyhalov
6224bb1048SAlexander Pyhalov    def emit(self, record):
6324bb1048SAlexander Pyhalov        while True:
6424bb1048SAlexander Pyhalov            try:
6524bb1048SAlexander Pyhalov                WatchedFileHandler.emit(self, record)
6624bb1048SAlexander Pyhalov                self.tries = 0
6724bb1048SAlexander Pyhalov                return
6824bb1048SAlexander Pyhalov            except IOError as err:
6924bb1048SAlexander Pyhalov                if self.tries == self.max_tries:
7024bb1048SAlexander Pyhalov                    raise
7124bb1048SAlexander Pyhalov                self.stream.close()
7224bb1048SAlexander Pyhalov                self.stream = self._open()
7324bb1048SAlexander Pyhalov                self.tries += 1
7424bb1048SAlexander Pyhalov
7524bb1048SAlexander Pyhalovclass Result(object):
7624bb1048SAlexander Pyhalov    total = 0
7724bb1048SAlexander Pyhalov    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
7824bb1048SAlexander Pyhalov
7924bb1048SAlexander Pyhalov    def __init__(self):
8024bb1048SAlexander Pyhalov        self.starttime = None
8124bb1048SAlexander Pyhalov        self.returncode = None
8224bb1048SAlexander Pyhalov        self.runtime = ''
8324bb1048SAlexander Pyhalov        self.stdout = []
8424bb1048SAlexander Pyhalov        self.stderr = []
8524bb1048SAlexander Pyhalov        self.result = ''
8624bb1048SAlexander Pyhalov
8724bb1048SAlexander Pyhalov    def done(self, proc, killed):
8824bb1048SAlexander Pyhalov        """
8924bb1048SAlexander Pyhalov        Finalize the results of this Cmd.
9024bb1048SAlexander Pyhalov        """
9124bb1048SAlexander Pyhalov        Result.total += 1
9224bb1048SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
9324bb1048SAlexander Pyhalov        self.runtime = '%02d:%02d' % (m, s)
9424bb1048SAlexander Pyhalov        self.returncode = proc.returncode
9524bb1048SAlexander Pyhalov        if killed:
9624bb1048SAlexander Pyhalov            self.result = 'KILLED'
9724bb1048SAlexander Pyhalov            Result.runresults['KILLED'] += 1
9824bb1048SAlexander Pyhalov        elif self.returncode is 0:
9924bb1048SAlexander Pyhalov            self.result = 'PASS'
10024bb1048SAlexander Pyhalov            Result.runresults['PASS'] += 1
10124bb1048SAlexander Pyhalov        elif self.returncode is not 0:
10224bb1048SAlexander Pyhalov            self.result = 'FAIL'
10324bb1048SAlexander Pyhalov            Result.runresults['FAIL'] += 1
10424bb1048SAlexander Pyhalov
10524bb1048SAlexander Pyhalov
10624bb1048SAlexander Pyhalovclass Output(object):
10724bb1048SAlexander Pyhalov    """
10824bb1048SAlexander Pyhalov    This class is a slightly modified version of the 'Stream' class found
10924bb1048SAlexander Pyhalov    here: http://goo.gl/aSGfv
11024bb1048SAlexander Pyhalov    """
11124bb1048SAlexander Pyhalov    def __init__(self, stream):
11224bb1048SAlexander Pyhalov        self.stream = stream
11324bb1048SAlexander Pyhalov        self._buf = ''
11424bb1048SAlexander Pyhalov        self.lines = []
11524bb1048SAlexander Pyhalov
11624bb1048SAlexander Pyhalov    def fileno(self):
11724bb1048SAlexander Pyhalov        return self.stream.fileno()
11824bb1048SAlexander Pyhalov
11924bb1048SAlexander Pyhalov    def read(self, drain=0):
12024bb1048SAlexander Pyhalov        """
12124bb1048SAlexander Pyhalov        Read from the file descriptor. If 'drain' set, read until EOF.
12224bb1048SAlexander Pyhalov        """
12324bb1048SAlexander Pyhalov        while self._read() is not None:
12424bb1048SAlexander Pyhalov            if not drain:
12524bb1048SAlexander Pyhalov                break
12624bb1048SAlexander Pyhalov
12724bb1048SAlexander Pyhalov    def _read(self):
12824bb1048SAlexander Pyhalov        """
12924bb1048SAlexander Pyhalov        Read up to 4k of data from this output stream. Collect the output
13024bb1048SAlexander Pyhalov        up to the last newline, and append it to any leftover data from a
13124bb1048SAlexander Pyhalov        previous call. The lines are stored as a (timestamp, data) tuple
13224bb1048SAlexander Pyhalov        for easy sorting/merging later.
13324bb1048SAlexander Pyhalov        """
13424bb1048SAlexander Pyhalov        fd = self.fileno()
135*83d13c2eSAlexander Pyhalov        buf = os.read(fd, 4096).decode()
13624bb1048SAlexander Pyhalov        if not buf:
13724bb1048SAlexander Pyhalov            return None
13824bb1048SAlexander Pyhalov        if '\n' not in buf:
13924bb1048SAlexander Pyhalov            self._buf += buf
14024bb1048SAlexander Pyhalov            return []
14124bb1048SAlexander Pyhalov
14224bb1048SAlexander Pyhalov        buf = self._buf + buf
14324bb1048SAlexander Pyhalov        tmp, rest = buf.rsplit('\n', 1)
14424bb1048SAlexander Pyhalov        self._buf = rest
14524bb1048SAlexander Pyhalov        now = datetime.now()
14624bb1048SAlexander Pyhalov        rows = tmp.split('\n')
14724bb1048SAlexander Pyhalov        self.lines += [(now, r) for r in rows]
14824bb1048SAlexander Pyhalov
14924bb1048SAlexander Pyhalov
15024bb1048SAlexander Pyhalovclass Cmd(object):
15124bb1048SAlexander Pyhalov    verified_users = []
15224bb1048SAlexander Pyhalov
15324bb1048SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None):
15424bb1048SAlexander Pyhalov        self.pathname = pathname
15524bb1048SAlexander Pyhalov        self.outputdir = outputdir or 'BASEDIR'
15624bb1048SAlexander Pyhalov        self.timeout = timeout or 60
15724bb1048SAlexander Pyhalov        self.user = user or ''
15824bb1048SAlexander Pyhalov        self.killed = False
15924bb1048SAlexander Pyhalov        self.result = Result()
16024bb1048SAlexander Pyhalov
16124bb1048SAlexander Pyhalov    def __str__(self):
16224bb1048SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % (
16324bb1048SAlexander Pyhalov                self.pathname, self.outputdir, self.timeout, self.user)
16424bb1048SAlexander Pyhalov
16524bb1048SAlexander Pyhalov    def kill_cmd(self, proc):
16624bb1048SAlexander Pyhalov        """
16724bb1048SAlexander Pyhalov        Kill a running command due to timeout, or ^C from the keyboard. If
16824bb1048SAlexander Pyhalov        sudo is required, this user was verified previously.
16924bb1048SAlexander Pyhalov        """
17024bb1048SAlexander Pyhalov        self.killed = True
17124bb1048SAlexander Pyhalov        do_sudo = len(self.user) != 0
17224bb1048SAlexander Pyhalov        signal = '-TERM'
17324bb1048SAlexander Pyhalov
17424bb1048SAlexander Pyhalov        cmd = [SUDO, KILL, signal, str(proc.pid)]
17524bb1048SAlexander Pyhalov        if not do_sudo:
17624bb1048SAlexander Pyhalov            del cmd[0]
17724bb1048SAlexander Pyhalov
17824bb1048SAlexander Pyhalov        try:
17924bb1048SAlexander Pyhalov            kp = Popen(cmd)
18024bb1048SAlexander Pyhalov            kp.wait()
18124bb1048SAlexander Pyhalov        except:
18224bb1048SAlexander Pyhalov            pass
18324bb1048SAlexander Pyhalov
18424bb1048SAlexander Pyhalov    def update_cmd_privs(self, cmd, user):
18524bb1048SAlexander Pyhalov        """
18624bb1048SAlexander Pyhalov        If a user has been specified to run this Cmd and we're not already
18724bb1048SAlexander Pyhalov        running as that user, prepend the appropriate sudo command to run
18824bb1048SAlexander Pyhalov        as that user.
18924bb1048SAlexander Pyhalov        """
19024bb1048SAlexander Pyhalov        me = getpwuid(os.getuid())
19124bb1048SAlexander Pyhalov
19224bb1048SAlexander Pyhalov        if not user or user is me:
19324bb1048SAlexander Pyhalov            return cmd
19424bb1048SAlexander Pyhalov
19524bb1048SAlexander Pyhalov        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
19624bb1048SAlexander Pyhalov        return ret.split(' ')
19724bb1048SAlexander Pyhalov
19824bb1048SAlexander Pyhalov    def collect_output(self, proc):
19924bb1048SAlexander Pyhalov        """
20024bb1048SAlexander Pyhalov        Read from stdout/stderr as data becomes available, until the
20124bb1048SAlexander Pyhalov        process is no longer running. Return the lines from the stdout and
20224bb1048SAlexander Pyhalov        stderr Output objects.
20324bb1048SAlexander Pyhalov        """
20424bb1048SAlexander Pyhalov        out = Output(proc.stdout)
20524bb1048SAlexander Pyhalov        err = Output(proc.stderr)
20624bb1048SAlexander Pyhalov        res = []
20724bb1048SAlexander Pyhalov        while proc.returncode is None:
20824bb1048SAlexander Pyhalov            proc.poll()
20924bb1048SAlexander Pyhalov            res = select([out, err], [], [], .1)
21024bb1048SAlexander Pyhalov            for fd in res[0]:
21124bb1048SAlexander Pyhalov                fd.read()
21224bb1048SAlexander Pyhalov        for fd in res[0]:
21324bb1048SAlexander Pyhalov            fd.read(drain=1)
21424bb1048SAlexander Pyhalov
21524bb1048SAlexander Pyhalov        return out.lines, err.lines
21624bb1048SAlexander Pyhalov
21724bb1048SAlexander Pyhalov    def run(self, options):
21824bb1048SAlexander Pyhalov        """
21924bb1048SAlexander Pyhalov        This is the main function that runs each individual test.
22024bb1048SAlexander Pyhalov        Determine whether or not the command requires sudo, and modify it
22124bb1048SAlexander Pyhalov        if needed. Run the command, and update the result object.
22224bb1048SAlexander Pyhalov        """
22324bb1048SAlexander Pyhalov        if options.dryrun is True:
224*83d13c2eSAlexander Pyhalov            print(self)
22524bb1048SAlexander Pyhalov            return
22624bb1048SAlexander Pyhalov
22724bb1048SAlexander Pyhalov        privcmd = self.update_cmd_privs(self.pathname, self.user)
22824bb1048SAlexander Pyhalov        try:
22924bb1048SAlexander Pyhalov            old = os.umask(0)
23024bb1048SAlexander Pyhalov            if not os.path.isdir(self.outputdir):
231*83d13c2eSAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
23224bb1048SAlexander Pyhalov            os.umask(old)
233*83d13c2eSAlexander Pyhalov        except OSError as e:
23424bb1048SAlexander Pyhalov            fail('%s' % e)
23524bb1048SAlexander Pyhalov
23624bb1048SAlexander Pyhalov        try:
23724bb1048SAlexander Pyhalov            self.result.starttime = time()
238*83d13c2eSAlexander Pyhalov            proc = Popen(privcmd, stdout=PIPE, stderr=PIPE,
239*83d13c2eSAlexander Pyhalov                         universal_newlines=True)
24024bb1048SAlexander Pyhalov            t = Timer(int(self.timeout), self.kill_cmd, [proc])
24124bb1048SAlexander Pyhalov            t.start()
24224bb1048SAlexander Pyhalov            self.result.stdout, self.result.stderr = self.collect_output(proc)
24324bb1048SAlexander Pyhalov        except KeyboardInterrupt:
24424bb1048SAlexander Pyhalov            self.kill_cmd(proc)
24524bb1048SAlexander Pyhalov            fail('\nRun terminated at user request.')
24624bb1048SAlexander Pyhalov        finally:
24724bb1048SAlexander Pyhalov            t.cancel()
24824bb1048SAlexander Pyhalov
24924bb1048SAlexander Pyhalov        self.result.done(proc, self.killed)
25024bb1048SAlexander Pyhalov
25124bb1048SAlexander Pyhalov    def skip(self):
25224bb1048SAlexander Pyhalov        """
25324bb1048SAlexander Pyhalov        Initialize enough of the test result that we can log a skipped
25424bb1048SAlexander Pyhalov        command.
25524bb1048SAlexander Pyhalov        """
25624bb1048SAlexander Pyhalov        Result.total += 1
25724bb1048SAlexander Pyhalov        Result.runresults['SKIP'] += 1
25824bb1048SAlexander Pyhalov        self.result.stdout = self.result.stderr = []
25924bb1048SAlexander Pyhalov        self.result.starttime = time()
26024bb1048SAlexander Pyhalov        m, s = divmod(time() - self.result.starttime, 60)
26124bb1048SAlexander Pyhalov        self.result.runtime = '%02d:%02d' % (m, s)
26224bb1048SAlexander Pyhalov        self.result.result = 'SKIP'
26324bb1048SAlexander Pyhalov
26424bb1048SAlexander Pyhalov    def log(self, logger, options):
26524bb1048SAlexander Pyhalov        """
26624bb1048SAlexander Pyhalov        This function is responsible for writing all output. This includes
26724bb1048SAlexander Pyhalov        the console output, the logfile of all results (with timestamped
26824bb1048SAlexander Pyhalov        merged stdout and stderr), and for each test, the unmodified
26924bb1048SAlexander Pyhalov        stdout/stderr/merged in it's own file.
27024bb1048SAlexander Pyhalov        """
27124bb1048SAlexander Pyhalov        if logger is None:
27224bb1048SAlexander Pyhalov            return
27324bb1048SAlexander Pyhalov
27424bb1048SAlexander Pyhalov        logname = getpwuid(os.getuid()).pw_name
27524bb1048SAlexander Pyhalov        user = ' (run as %s)' % (self.user if len(self.user) else logname)
27624bb1048SAlexander Pyhalov        msga = 'Test: %s%s ' % (self.pathname, user)
27724bb1048SAlexander Pyhalov        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
27824bb1048SAlexander Pyhalov        pad = ' ' * (80 - (len(msga) + len(msgb)))
27924bb1048SAlexander Pyhalov
28024bb1048SAlexander Pyhalov        # If -q is specified, only print a line for tests that didn't pass.
28124bb1048SAlexander Pyhalov        # This means passing tests need to be logged as DEBUG, or the one
28224bb1048SAlexander Pyhalov        # line summary will only be printed in the logfile for failures.
28324bb1048SAlexander Pyhalov        if not options.quiet:
28424bb1048SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
28524bb1048SAlexander Pyhalov        elif self.result.result is not 'PASS':
28624bb1048SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
28724bb1048SAlexander Pyhalov        else:
28824bb1048SAlexander Pyhalov            logger.debug('%s%s%s' % (msga, pad, msgb))
28924bb1048SAlexander Pyhalov
29024bb1048SAlexander Pyhalov        lines = sorted(self.result.stdout + self.result.stderr,
291*83d13c2eSAlexander Pyhalov                       key=lambda x: x[0])
29224bb1048SAlexander Pyhalov
29324bb1048SAlexander Pyhalov        for dt, line in lines:
29424bb1048SAlexander Pyhalov            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
29524bb1048SAlexander Pyhalov
29624bb1048SAlexander Pyhalov        if len(self.result.stdout):
29724bb1048SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
29824bb1048SAlexander Pyhalov                for _, line in self.result.stdout:
299*83d13c2eSAlexander Pyhalov                    out.write('%s\n' % line)
30024bb1048SAlexander Pyhalov        if len(self.result.stderr):
30124bb1048SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
30224bb1048SAlexander Pyhalov                for _, line in self.result.stderr:
303*83d13c2eSAlexander Pyhalov                    err.write('%s\n' % line)
30424bb1048SAlexander Pyhalov        if len(self.result.stdout) and len(self.result.stderr):
30524bb1048SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
30624bb1048SAlexander Pyhalov                for _, line in lines:
307*83d13c2eSAlexander Pyhalov                    merged.write('%s\n' % line)
30824bb1048SAlexander Pyhalov
30924bb1048SAlexander Pyhalov
31024bb1048SAlexander Pyhalovclass Test(Cmd):
31124bb1048SAlexander Pyhalov    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
31224bb1048SAlexander Pyhalov             'post_user']
31324bb1048SAlexander Pyhalov
31424bb1048SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
31524bb1048SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None):
31624bb1048SAlexander Pyhalov        super(Test, self).__init__(pathname, outputdir, timeout, user)
31724bb1048SAlexander Pyhalov        self.pre = pre or ''
31824bb1048SAlexander Pyhalov        self.pre_user = pre_user or ''
31924bb1048SAlexander Pyhalov        self.post = post or ''
32024bb1048SAlexander Pyhalov        self.post_user = post_user or ''
32124bb1048SAlexander Pyhalov
32224bb1048SAlexander Pyhalov    def __str__(self):
32324bb1048SAlexander Pyhalov        post_user = pre_user = ''
32424bb1048SAlexander Pyhalov        if len(self.pre_user):
32524bb1048SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
32624bb1048SAlexander Pyhalov        if len(self.post_user):
32724bb1048SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
32824bb1048SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \
32924bb1048SAlexander Pyhalov               "%s%s\nUser: %s\n" % (self.pathname, self.outputdir,
33024bb1048SAlexander Pyhalov                self.timeout, self.pre, pre_user, self.post, post_user,
33124bb1048SAlexander Pyhalov                self.user)
33224bb1048SAlexander Pyhalov
33324bb1048SAlexander Pyhalov    def verify(self, logger):
33424bb1048SAlexander Pyhalov        """
33524bb1048SAlexander Pyhalov        Check the pre/post scripts, user and Test. Omit the Test from this
33624bb1048SAlexander Pyhalov        run if there are any problems.
33724bb1048SAlexander Pyhalov        """
33824bb1048SAlexander Pyhalov        files = [self.pre, self.pathname, self.post]
33924bb1048SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
34024bb1048SAlexander Pyhalov
34124bb1048SAlexander Pyhalov        for f in [f for f in files if len(f)]:
34224bb1048SAlexander Pyhalov            if not verify_file(f):
34324bb1048SAlexander Pyhalov                logger.info("Warning: Test '%s' not added to this run because"
34424bb1048SAlexander Pyhalov                            " it failed verification." % f)
34524bb1048SAlexander Pyhalov                return False
34624bb1048SAlexander Pyhalov
34724bb1048SAlexander Pyhalov        for user in [user for user in users if len(user)]:
34824bb1048SAlexander Pyhalov            if not verify_user(user, logger):
34924bb1048SAlexander Pyhalov                logger.info("Not adding Test '%s' to this run." %
35024bb1048SAlexander Pyhalov                            self.pathname)
35124bb1048SAlexander Pyhalov                return False
35224bb1048SAlexander Pyhalov
35324bb1048SAlexander Pyhalov        return True
35424bb1048SAlexander Pyhalov
35524bb1048SAlexander Pyhalov    def run(self, logger, options):
35624bb1048SAlexander Pyhalov        """
35724bb1048SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
35824bb1048SAlexander Pyhalov        doesn't pass, skip this Test. Run the post script regardless.
35924bb1048SAlexander Pyhalov        """
36024bb1048SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
36124bb1048SAlexander Pyhalov                      os.path.basename(self.pre)), timeout=self.timeout,
36224bb1048SAlexander Pyhalov                      user=self.pre_user)
36324bb1048SAlexander Pyhalov        test = Cmd(self.pathname, outputdir=self.outputdir,
36424bb1048SAlexander Pyhalov                   timeout=self.timeout, user=self.user)
36524bb1048SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
36624bb1048SAlexander Pyhalov                       os.path.basename(self.post)), timeout=self.timeout,
36724bb1048SAlexander Pyhalov                       user=self.post_user)
36824bb1048SAlexander Pyhalov
36924bb1048SAlexander Pyhalov        cont = True
37024bb1048SAlexander Pyhalov        if len(pretest.pathname):
37124bb1048SAlexander Pyhalov            pretest.run(options)
37224bb1048SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
37324bb1048SAlexander Pyhalov            pretest.log(logger, options)
37424bb1048SAlexander Pyhalov
37524bb1048SAlexander Pyhalov        if cont:
37624bb1048SAlexander Pyhalov            test.run(options)
37724bb1048SAlexander Pyhalov        else:
37824bb1048SAlexander Pyhalov            test.skip()
37924bb1048SAlexander Pyhalov
38024bb1048SAlexander Pyhalov        test.log(logger, options)
38124bb1048SAlexander Pyhalov
38224bb1048SAlexander Pyhalov        if len(posttest.pathname):
38324bb1048SAlexander Pyhalov            posttest.run(options)
38424bb1048SAlexander Pyhalov            posttest.log(logger, options)
38524bb1048SAlexander Pyhalov
38624bb1048SAlexander Pyhalov
38724bb1048SAlexander Pyhalovclass TestGroup(Test):
38824bb1048SAlexander Pyhalov    props = Test.props + ['tests']
38924bb1048SAlexander Pyhalov
39024bb1048SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
39124bb1048SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None,
39224bb1048SAlexander Pyhalov                 tests=None):
39324bb1048SAlexander Pyhalov        super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
39424bb1048SAlexander Pyhalov                                        pre, pre_user, post, post_user)
39524bb1048SAlexander Pyhalov        self.tests = tests or []
39624bb1048SAlexander Pyhalov
39724bb1048SAlexander Pyhalov    def __str__(self):
39824bb1048SAlexander Pyhalov        post_user = pre_user = ''
39924bb1048SAlexander Pyhalov        if len(self.pre_user):
40024bb1048SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
40124bb1048SAlexander Pyhalov        if len(self.post_user):
40224bb1048SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
40324bb1048SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \
40424bb1048SAlexander Pyhalov               "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname,
40524bb1048SAlexander Pyhalov                self.outputdir, self.tests, self.timeout, self.pre, pre_user,
40624bb1048SAlexander Pyhalov                self.post, post_user, self.user)
40724bb1048SAlexander Pyhalov
40824bb1048SAlexander Pyhalov    def verify(self, logger):
40924bb1048SAlexander Pyhalov        """
41024bb1048SAlexander Pyhalov        Check the pre/post scripts, user and tests in this TestGroup. Omit
41124bb1048SAlexander Pyhalov        the TestGroup entirely, or simply delete the relevant tests in the
41224bb1048SAlexander Pyhalov        group, if that's all that's required.
41324bb1048SAlexander Pyhalov        """
41424bb1048SAlexander Pyhalov        # If the pre or post scripts are relative pathnames, convert to
41524bb1048SAlexander Pyhalov        # absolute, so they stand a chance of passing verification.
41624bb1048SAlexander Pyhalov        if len(self.pre) and not os.path.isabs(self.pre):
41724bb1048SAlexander Pyhalov            self.pre = os.path.join(self.pathname, self.pre)
41824bb1048SAlexander Pyhalov        if len(self.post) and not os.path.isabs(self.post):
41924bb1048SAlexander Pyhalov            self.post = os.path.join(self.pathname, self.post)
42024bb1048SAlexander Pyhalov
42124bb1048SAlexander Pyhalov        auxfiles = [self.pre, self.post]
42224bb1048SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
42324bb1048SAlexander Pyhalov
42424bb1048SAlexander Pyhalov        for f in [f for f in auxfiles if len(f)]:
42524bb1048SAlexander Pyhalov            if self.pathname != os.path.dirname(f):
42624bb1048SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
42724bb1048SAlexander Pyhalov                            "Auxiliary script '%s' exists in a different "
42824bb1048SAlexander Pyhalov                            "directory." % (self.pathname, f))
42924bb1048SAlexander Pyhalov                return False
43024bb1048SAlexander Pyhalov
43124bb1048SAlexander Pyhalov            if not verify_file(f):
43224bb1048SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
43324bb1048SAlexander Pyhalov                            "Auxiliary script '%s' failed verification." %
43424bb1048SAlexander Pyhalov                            (self.pathname, f))
43524bb1048SAlexander Pyhalov                return False
43624bb1048SAlexander Pyhalov
43724bb1048SAlexander Pyhalov        for user in [user for user in users if len(user)]:
43824bb1048SAlexander Pyhalov            if not verify_user(user, logger):
43924bb1048SAlexander Pyhalov                logger.info("Not adding TestGroup '%s' to this run." %
44024bb1048SAlexander Pyhalov                            self.pathname)
44124bb1048SAlexander Pyhalov                return False
44224bb1048SAlexander Pyhalov
44324bb1048SAlexander Pyhalov        # If one of the tests is invalid, delete it, log it, and drive on.
44424bb1048SAlexander Pyhalov        for test in self.tests:
44524bb1048SAlexander Pyhalov            if not verify_file(os.path.join(self.pathname, test)):
44624bb1048SAlexander Pyhalov                del self.tests[self.tests.index(test)]
44724bb1048SAlexander Pyhalov                logger.info("Warning: Test '%s' removed from TestGroup '%s' "
44824bb1048SAlexander Pyhalov                            "because it failed verification." % (test,
44924bb1048SAlexander Pyhalov                            self.pathname))
45024bb1048SAlexander Pyhalov
45124bb1048SAlexander Pyhalov        return len(self.tests) is not 0
45224bb1048SAlexander Pyhalov
45324bb1048SAlexander Pyhalov    def run(self, logger, options):
45424bb1048SAlexander Pyhalov        """
45524bb1048SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
45624bb1048SAlexander Pyhalov        doesn't pass, skip all the tests in this TestGroup. Run the post
45724bb1048SAlexander Pyhalov        script regardless.
45824bb1048SAlexander Pyhalov        """
45924bb1048SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
46024bb1048SAlexander Pyhalov                      os.path.basename(self.pre)), timeout=self.timeout,
46124bb1048SAlexander Pyhalov                      user=self.pre_user)
46224bb1048SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
46324bb1048SAlexander Pyhalov                       os.path.basename(self.post)), timeout=self.timeout,
46424bb1048SAlexander Pyhalov                       user=self.post_user)
46524bb1048SAlexander Pyhalov
46624bb1048SAlexander Pyhalov        cont = True
46724bb1048SAlexander Pyhalov        if len(pretest.pathname):
46824bb1048SAlexander Pyhalov            pretest.run(options)
46924bb1048SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
47024bb1048SAlexander Pyhalov            pretest.log(logger, options)
47124bb1048SAlexander Pyhalov
47224bb1048SAlexander Pyhalov        for fname in self.tests:
47324bb1048SAlexander Pyhalov            test = Cmd(os.path.join(self.pathname, fname),
47424bb1048SAlexander Pyhalov                       outputdir=os.path.join(self.outputdir, fname),
47524bb1048SAlexander Pyhalov                       timeout=self.timeout, user=self.user)
47624bb1048SAlexander Pyhalov            if cont:
47724bb1048SAlexander Pyhalov                test.run(options)
47824bb1048SAlexander Pyhalov            else:
47924bb1048SAlexander Pyhalov                test.skip()
48024bb1048SAlexander Pyhalov
48124bb1048SAlexander Pyhalov            test.log(logger, options)
48224bb1048SAlexander Pyhalov
48324bb1048SAlexander Pyhalov        if len(posttest.pathname):
48424bb1048SAlexander Pyhalov            posttest.run(options)
48524bb1048SAlexander Pyhalov            posttest.log(logger, options)
48624bb1048SAlexander Pyhalov
48724bb1048SAlexander Pyhalov
48824bb1048SAlexander Pyhalovclass TestRun(object):
48924bb1048SAlexander Pyhalov    props = ['quiet', 'outputdir']
49024bb1048SAlexander Pyhalov
49124bb1048SAlexander Pyhalov    def __init__(self, options):
49224bb1048SAlexander Pyhalov        self.tests = {}
49324bb1048SAlexander Pyhalov        self.testgroups = {}
49424bb1048SAlexander Pyhalov        self.starttime = time()
49524bb1048SAlexander Pyhalov        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
49624bb1048SAlexander Pyhalov        self.outputdir = os.path.join(options.outputdir, self.timestamp)
49724bb1048SAlexander Pyhalov        self.logger = self.setup_logging(options)
49824bb1048SAlexander Pyhalov        self.defaults = [
49924bb1048SAlexander Pyhalov            ('outputdir', BASEDIR),
50024bb1048SAlexander Pyhalov            ('quiet', False),
50124bb1048SAlexander Pyhalov            ('timeout', 60),
50224bb1048SAlexander Pyhalov            ('user', ''),
50324bb1048SAlexander Pyhalov            ('pre', ''),
50424bb1048SAlexander Pyhalov            ('pre_user', ''),
50524bb1048SAlexander Pyhalov            ('post', ''),
50624bb1048SAlexander Pyhalov            ('post_user', '')
50724bb1048SAlexander Pyhalov        ]
50824bb1048SAlexander Pyhalov
50924bb1048SAlexander Pyhalov    def __str__(self):
51024bb1048SAlexander Pyhalov        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
51124bb1048SAlexander Pyhalov        s += 'TESTS:\n'
51224bb1048SAlexander Pyhalov        for key in sorted(self.tests.keys()):
51324bb1048SAlexander Pyhalov            s += '%s%s' % (self.tests[key].__str__(), '\n')
51424bb1048SAlexander Pyhalov        s += 'TESTGROUPS:\n'
51524bb1048SAlexander Pyhalov        for key in sorted(self.testgroups.keys()):
51624bb1048SAlexander Pyhalov            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
51724bb1048SAlexander Pyhalov        return s
51824bb1048SAlexander Pyhalov
51924bb1048SAlexander Pyhalov    def addtest(self, pathname, options):
52024bb1048SAlexander Pyhalov        """
52124bb1048SAlexander Pyhalov        Create a new Test, and apply any properties that were passed in
52224bb1048SAlexander Pyhalov        from the command line. If it passes verification, add it to the
52324bb1048SAlexander Pyhalov        TestRun.
52424bb1048SAlexander Pyhalov        """
52524bb1048SAlexander Pyhalov        test = Test(pathname)
52624bb1048SAlexander Pyhalov        for prop in Test.props:
52724bb1048SAlexander Pyhalov            setattr(test, prop, getattr(options, prop))
52824bb1048SAlexander Pyhalov
52924bb1048SAlexander Pyhalov        if test.verify(self.logger):
53024bb1048SAlexander Pyhalov            self.tests[pathname] = test
53124bb1048SAlexander Pyhalov
53224bb1048SAlexander Pyhalov    def addtestgroup(self, dirname, filenames, options):
53324bb1048SAlexander Pyhalov        """
53424bb1048SAlexander Pyhalov        Create a new TestGroup, and apply any properties that were passed
53524bb1048SAlexander Pyhalov        in from the command line. If it passes verification, add it to the
53624bb1048SAlexander Pyhalov        TestRun.
53724bb1048SAlexander Pyhalov        """
53824bb1048SAlexander Pyhalov        if dirname not in self.testgroups:
53924bb1048SAlexander Pyhalov            testgroup = TestGroup(dirname)
54024bb1048SAlexander Pyhalov            for prop in Test.props:
54124bb1048SAlexander Pyhalov                setattr(testgroup, prop, getattr(options, prop))
54224bb1048SAlexander Pyhalov
54324bb1048SAlexander Pyhalov            # Prevent pre/post scripts from running as regular tests
54424bb1048SAlexander Pyhalov            for f in [testgroup.pre, testgroup.post]:
54524bb1048SAlexander Pyhalov                if f in filenames:
54624bb1048SAlexander Pyhalov                    del filenames[filenames.index(f)]
54724bb1048SAlexander Pyhalov
54824bb1048SAlexander Pyhalov            self.testgroups[dirname] = testgroup
54924bb1048SAlexander Pyhalov            self.testgroups[dirname].tests = sorted(filenames)
55024bb1048SAlexander Pyhalov
55124bb1048SAlexander Pyhalov            testgroup.verify(self.logger)
55224bb1048SAlexander Pyhalov
55324bb1048SAlexander Pyhalov    def read(self, logger, options):
55424bb1048SAlexander Pyhalov        """
55524bb1048SAlexander Pyhalov        Read in the specified runfile, and apply the TestRun properties
55624bb1048SAlexander Pyhalov        listed in the 'DEFAULT' section to our TestRun. Then read each
55724bb1048SAlexander Pyhalov        section, and apply the appropriate properties to the Test or
55824bb1048SAlexander Pyhalov        TestGroup. Properties from individual sections override those set
55924bb1048SAlexander Pyhalov        in the 'DEFAULT' section. If the Test or TestGroup passes
56024bb1048SAlexander Pyhalov        verification, add it to the TestRun.
56124bb1048SAlexander Pyhalov        """
562*83d13c2eSAlexander Pyhalov        config = configparser.RawConfigParser()
56324bb1048SAlexander Pyhalov        if not len(config.read(options.runfile)):
56424bb1048SAlexander Pyhalov            fail("Coulnd't read config file %s" % options.runfile)
56524bb1048SAlexander Pyhalov
56624bb1048SAlexander Pyhalov        for opt in TestRun.props:
56724bb1048SAlexander Pyhalov            if config.has_option('DEFAULT', opt):
56824bb1048SAlexander Pyhalov                setattr(self, opt, config.get('DEFAULT', opt))
56924bb1048SAlexander Pyhalov        self.outputdir = os.path.join(self.outputdir, self.timestamp)
57024bb1048SAlexander Pyhalov
57124bb1048SAlexander Pyhalov        for section in config.sections():
57224bb1048SAlexander Pyhalov            if 'tests' in config.options(section):
57324bb1048SAlexander Pyhalov                testgroup = TestGroup(section)
57424bb1048SAlexander Pyhalov                for prop in TestGroup.props:
57524bb1048SAlexander Pyhalov                    try:
57624bb1048SAlexander Pyhalov                        setattr(testgroup, prop, config.get('DEFAULT', prop))
57724bb1048SAlexander Pyhalov                        setattr(testgroup, prop, config.get(section, prop))
57824bb1048SAlexander Pyhalov                    except ConfigParser.NoOptionError:
57924bb1048SAlexander Pyhalov                        pass
58024bb1048SAlexander Pyhalov
58124bb1048SAlexander Pyhalov                # Repopulate tests using eval to convert the string to a list
58224bb1048SAlexander Pyhalov                testgroup.tests = eval(config.get(section, 'tests'))
58324bb1048SAlexander Pyhalov
58424bb1048SAlexander Pyhalov                if testgroup.verify(logger):
58524bb1048SAlexander Pyhalov                    self.testgroups[section] = testgroup
58624bb1048SAlexander Pyhalov            else:
58724bb1048SAlexander Pyhalov                test = Test(section)
58824bb1048SAlexander Pyhalov                for prop in Test.props:
58924bb1048SAlexander Pyhalov                    try:
59024bb1048SAlexander Pyhalov                        setattr(test, prop, config.get('DEFAULT', prop))
59124bb1048SAlexander Pyhalov                        setattr(test, prop, config.get(section, prop))
59224bb1048SAlexander Pyhalov                    except ConfigParser.NoOptionError:
59324bb1048SAlexander Pyhalov                        pass
59424bb1048SAlexander Pyhalov                if test.verify(logger):
59524bb1048SAlexander Pyhalov                    self.tests[section] = test
59624bb1048SAlexander Pyhalov
59724bb1048SAlexander Pyhalov    def write(self, options):
59824bb1048SAlexander Pyhalov        """
59924bb1048SAlexander Pyhalov        Create a configuration file for editing and later use. The
60024bb1048SAlexander Pyhalov        'DEFAULT' section of the config file is created from the
60124bb1048SAlexander Pyhalov        properties that were specified on the command line. Tests are
60224bb1048SAlexander Pyhalov        simply added as sections that inherit everything from the
60324bb1048SAlexander Pyhalov        'DEFAULT' section. TestGroups are the same, except they get an
60424bb1048SAlexander Pyhalov        option including all the tests to run in that directory.
60524bb1048SAlexander Pyhalov        """
60624bb1048SAlexander Pyhalov
60724bb1048SAlexander Pyhalov        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
60824bb1048SAlexander Pyhalov                        self.defaults])
609*83d13c2eSAlexander Pyhalov        config = configparser.RawConfigParser(defaults)
61024bb1048SAlexander Pyhalov
61124bb1048SAlexander Pyhalov        for test in sorted(self.tests.keys()):
61224bb1048SAlexander Pyhalov            config.add_section(test)
61324bb1048SAlexander Pyhalov
61424bb1048SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
61524bb1048SAlexander Pyhalov            config.add_section(testgroup)
61624bb1048SAlexander Pyhalov            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
61724bb1048SAlexander Pyhalov
61824bb1048SAlexander Pyhalov        try:
61924bb1048SAlexander Pyhalov            with open(options.template, 'w') as f:
62024bb1048SAlexander Pyhalov                return config.write(f)
62124bb1048SAlexander Pyhalov        except IOError:
62224bb1048SAlexander Pyhalov            fail('Could not open \'%s\' for writing.' % options.template)
62324bb1048SAlexander Pyhalov
62424bb1048SAlexander Pyhalov    def complete_outputdirs(self, options):
62524bb1048SAlexander Pyhalov        """
62624bb1048SAlexander Pyhalov        Collect all the pathnames for Tests, and TestGroups. Work
62724bb1048SAlexander Pyhalov        backwards one pathname component at a time, to create a unique
62824bb1048SAlexander Pyhalov        directory name in which to deposit test output. Tests will be able
62924bb1048SAlexander Pyhalov        to write output files directly in the newly modified outputdir.
63024bb1048SAlexander Pyhalov        TestGroups will be able to create one subdirectory per test in the
63124bb1048SAlexander Pyhalov        outputdir, and are guaranteed uniqueness because a group can only
63224bb1048SAlexander Pyhalov        contain files in one directory. Pre and post tests will create a
63324bb1048SAlexander Pyhalov        directory rooted at the outputdir of the Test or TestGroup in
63424bb1048SAlexander Pyhalov        question for their output.
63524bb1048SAlexander Pyhalov        """
63624bb1048SAlexander Pyhalov        done = False
63724bb1048SAlexander Pyhalov        components = 0
638*83d13c2eSAlexander Pyhalov        tmp_dict = dict(list(self.tests.items()) + list(self.testgroups.items()))
63924bb1048SAlexander Pyhalov        total = len(tmp_dict)
64024bb1048SAlexander Pyhalov        base = self.outputdir
64124bb1048SAlexander Pyhalov
64224bb1048SAlexander Pyhalov        while not done:
64324bb1048SAlexander Pyhalov            l = []
64424bb1048SAlexander Pyhalov            components -= 1
645*83d13c2eSAlexander Pyhalov            for testfile in list(tmp_dict.keys()):
64624bb1048SAlexander Pyhalov                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
64724bb1048SAlexander Pyhalov                if not uniq in l:
64824bb1048SAlexander Pyhalov                    l.append(uniq)
64924bb1048SAlexander Pyhalov                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
65024bb1048SAlexander Pyhalov                else:
65124bb1048SAlexander Pyhalov                    break
65224bb1048SAlexander Pyhalov            done = total == len(l)
65324bb1048SAlexander Pyhalov
65424bb1048SAlexander Pyhalov    def setup_logging(self, options):
65524bb1048SAlexander Pyhalov        """
65624bb1048SAlexander Pyhalov        Two loggers are set up here. The first is for the logfile which
65724bb1048SAlexander Pyhalov        will contain one line summarizing the test, including the test
65824bb1048SAlexander Pyhalov        name, result, and running time. This logger will also capture the
65924bb1048SAlexander Pyhalov        timestamped combined stdout and stderr of each run. The second
66024bb1048SAlexander Pyhalov        logger is optional console output, which will contain only the one
66124bb1048SAlexander Pyhalov        line summary. The loggers are initialized at two different levels
66224bb1048SAlexander Pyhalov        to facilitate segregating the output.
66324bb1048SAlexander Pyhalov        """
66424bb1048SAlexander Pyhalov        if options.dryrun is True:
66524bb1048SAlexander Pyhalov            return
66624bb1048SAlexander Pyhalov
66724bb1048SAlexander Pyhalov        testlogger = logging.getLogger(__name__)
66824bb1048SAlexander Pyhalov        testlogger.setLevel(logging.DEBUG)
66924bb1048SAlexander Pyhalov
67024bb1048SAlexander Pyhalov        if options.cmd is not 'wrconfig':
67124bb1048SAlexander Pyhalov            try:
67224bb1048SAlexander Pyhalov                old = os.umask(0)
673*83d13c2eSAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
67424bb1048SAlexander Pyhalov                os.umask(old)
675*83d13c2eSAlexander Pyhalov            except OSError as e:
67624bb1048SAlexander Pyhalov                fail('%s' % e)
67724bb1048SAlexander Pyhalov            filename = os.path.join(self.outputdir, 'log')
67824bb1048SAlexander Pyhalov
67924bb1048SAlexander Pyhalov            logfile = WatchedFileHandlerClosed(filename)
68024bb1048SAlexander Pyhalov            logfile.setLevel(logging.DEBUG)
68124bb1048SAlexander Pyhalov            logfilefmt = logging.Formatter('%(message)s')
68224bb1048SAlexander Pyhalov            logfile.setFormatter(logfilefmt)
68324bb1048SAlexander Pyhalov            testlogger.addHandler(logfile)
68424bb1048SAlexander Pyhalov
68524bb1048SAlexander Pyhalov        cons = logging.StreamHandler()
68624bb1048SAlexander Pyhalov        cons.setLevel(logging.INFO)
68724bb1048SAlexander Pyhalov        consfmt = logging.Formatter('%(message)s')
68824bb1048SAlexander Pyhalov        cons.setFormatter(consfmt)
68924bb1048SAlexander Pyhalov        testlogger.addHandler(cons)
69024bb1048SAlexander Pyhalov
69124bb1048SAlexander Pyhalov        return testlogger
69224bb1048SAlexander Pyhalov
69324bb1048SAlexander Pyhalov    def run(self, options):
69424bb1048SAlexander Pyhalov        """
69524bb1048SAlexander Pyhalov        Walk through all the Tests and TestGroups, calling run().
69624bb1048SAlexander Pyhalov        """
69724bb1048SAlexander Pyhalov        try:
69824bb1048SAlexander Pyhalov            os.chdir(self.outputdir)
69924bb1048SAlexander Pyhalov        except OSError:
70024bb1048SAlexander Pyhalov            fail('Could not change to directory %s' % self.outputdir)
70124bb1048SAlexander Pyhalov        for test in sorted(self.tests.keys()):
70224bb1048SAlexander Pyhalov            self.tests[test].run(self.logger, options)
70324bb1048SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
70424bb1048SAlexander Pyhalov            self.testgroups[testgroup].run(self.logger, options)
70524bb1048SAlexander Pyhalov
70624bb1048SAlexander Pyhalov    def summary(self):
70724bb1048SAlexander Pyhalov        if Result.total is 0:
70824bb1048SAlexander Pyhalov            return
70924bb1048SAlexander Pyhalov
710*83d13c2eSAlexander Pyhalov        print('\nResults Summary')
711*83d13c2eSAlexander Pyhalov        for key in list(Result.runresults.keys()):
71224bb1048SAlexander Pyhalov            if Result.runresults[key] is not 0:
713*83d13c2eSAlexander Pyhalov                print('%s\t% 4d' % (key, Result.runresults[key]))
71424bb1048SAlexander Pyhalov
71524bb1048SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
71624bb1048SAlexander Pyhalov        h, m = divmod(m, 60)
717*83d13c2eSAlexander Pyhalov        print('\nRunning Time:\t%02d:%02d:%02d' % (h, m, s))
718*83d13c2eSAlexander Pyhalov        print('Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
719*83d13c2eSAlexander Pyhalov               float(Result.total)) * 100))
720*83d13c2eSAlexander Pyhalov        print('Log directory:\t%s' % self.outputdir)
72124bb1048SAlexander Pyhalov
72224bb1048SAlexander Pyhalov
72324bb1048SAlexander Pyhalovdef verify_file(pathname):
72424bb1048SAlexander Pyhalov    """
72524bb1048SAlexander Pyhalov    Verify that the supplied pathname is an executable regular file.
72624bb1048SAlexander Pyhalov    """
72724bb1048SAlexander Pyhalov    if os.path.isdir(pathname) or os.path.islink(pathname):
72824bb1048SAlexander Pyhalov        return False
72924bb1048SAlexander Pyhalov
73024bb1048SAlexander Pyhalov    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
73124bb1048SAlexander Pyhalov        return True
73224bb1048SAlexander Pyhalov
73324bb1048SAlexander Pyhalov    return False
73424bb1048SAlexander Pyhalov
73524bb1048SAlexander Pyhalov
73624bb1048SAlexander Pyhalovdef verify_user(user, logger):
73724bb1048SAlexander Pyhalov    """
73824bb1048SAlexander Pyhalov    Verify that the specified user exists on this system, and can execute
73924bb1048SAlexander Pyhalov    sudo without being prompted for a password.
74024bb1048SAlexander Pyhalov    """
74124bb1048SAlexander Pyhalov    testcmd = [SUDO, '-n', '-u', user, TRUE]
74224bb1048SAlexander Pyhalov    can_sudo = exists = True
74324bb1048SAlexander Pyhalov
74424bb1048SAlexander Pyhalov    if user in Cmd.verified_users:
74524bb1048SAlexander Pyhalov        return True
74624bb1048SAlexander Pyhalov
74724bb1048SAlexander Pyhalov    try:
74824bb1048SAlexander Pyhalov        _ = getpwnam(user)
74924bb1048SAlexander Pyhalov    except KeyError:
75024bb1048SAlexander Pyhalov        exists = False
75124bb1048SAlexander Pyhalov        logger.info("Warning: user '%s' does not exist.", user)
75224bb1048SAlexander Pyhalov        return False
75324bb1048SAlexander Pyhalov
75424bb1048SAlexander Pyhalov    p = Popen(testcmd)
75524bb1048SAlexander Pyhalov    p.wait()
75624bb1048SAlexander Pyhalov    if p.returncode is not 0:
75724bb1048SAlexander Pyhalov        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
75824bb1048SAlexander Pyhalov        return False
75924bb1048SAlexander Pyhalov    else:
76024bb1048SAlexander Pyhalov        Cmd.verified_users.append(user)
76124bb1048SAlexander Pyhalov
76224bb1048SAlexander Pyhalov    return True
76324bb1048SAlexander Pyhalov
76424bb1048SAlexander Pyhalov
76524bb1048SAlexander Pyhalovdef find_tests(testrun, options):
76624bb1048SAlexander Pyhalov    """
76724bb1048SAlexander Pyhalov    For the given list of pathnames, add files as Tests. For directories,
76824bb1048SAlexander Pyhalov    if do_groups is True, add the directory as a TestGroup. If False,
76924bb1048SAlexander Pyhalov    recursively search for executable files.
77024bb1048SAlexander Pyhalov    """
77124bb1048SAlexander Pyhalov
77224bb1048SAlexander Pyhalov    for p in sorted(options.pathnames):
77324bb1048SAlexander Pyhalov        if os.path.isdir(p):
77424bb1048SAlexander Pyhalov            for dirname, _, filenames in os.walk(p):
77524bb1048SAlexander Pyhalov                if options.do_groups:
77624bb1048SAlexander Pyhalov                    testrun.addtestgroup(dirname, filenames, options)
77724bb1048SAlexander Pyhalov                else:
77824bb1048SAlexander Pyhalov                    for f in sorted(filenames):
77924bb1048SAlexander Pyhalov                        testrun.addtest(os.path.join(dirname, f), options)
78024bb1048SAlexander Pyhalov        else:
78124bb1048SAlexander Pyhalov            testrun.addtest(p, options)
78224bb1048SAlexander Pyhalov
78324bb1048SAlexander Pyhalov
78424bb1048SAlexander Pyhalovdef fail(retstr, ret=1):
785*83d13c2eSAlexander Pyhalov    print('%s: %s' % (argv[0], retstr))
78624bb1048SAlexander Pyhalov    exit(ret)
78724bb1048SAlexander Pyhalov
78824bb1048SAlexander Pyhalov
78924bb1048SAlexander Pyhalovdef options_cb(option, opt_str, value, parser):
79024bb1048SAlexander Pyhalov    path_options = ['runfile', 'outputdir', 'template']
79124bb1048SAlexander Pyhalov
79224bb1048SAlexander Pyhalov    if option.dest is 'runfile' and '-w' in parser.rargs or \
79324bb1048SAlexander Pyhalov        option.dest is 'template' and '-c' in parser.rargs:
79424bb1048SAlexander Pyhalov        fail('-c and -w are mutually exclusive.')
79524bb1048SAlexander Pyhalov
79624bb1048SAlexander Pyhalov    if opt_str in parser.rargs:
79724bb1048SAlexander Pyhalov        fail('%s may only be specified once.' % opt_str)
79824bb1048SAlexander Pyhalov
79924bb1048SAlexander Pyhalov    if option.dest is 'runfile':
80024bb1048SAlexander Pyhalov        parser.values.cmd = 'rdconfig'
80124bb1048SAlexander Pyhalov    if option.dest is 'template':
80224bb1048SAlexander Pyhalov        parser.values.cmd = 'wrconfig'
80324bb1048SAlexander Pyhalov
80424bb1048SAlexander Pyhalov    setattr(parser.values, option.dest, value)
80524bb1048SAlexander Pyhalov    if option.dest in path_options:
80624bb1048SAlexander Pyhalov        setattr(parser.values, option.dest, os.path.abspath(value))
80724bb1048SAlexander Pyhalov
80824bb1048SAlexander Pyhalov
80924bb1048SAlexander Pyhalovdef parse_args():
81024bb1048SAlexander Pyhalov    parser = OptionParser()
81124bb1048SAlexander Pyhalov    parser.add_option('-c', action='callback', callback=options_cb,
81224bb1048SAlexander Pyhalov                      type='string', dest='runfile', metavar='runfile',
81324bb1048SAlexander Pyhalov                      help='Specify tests to run via config file.')
81424bb1048SAlexander Pyhalov    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
81524bb1048SAlexander Pyhalov                      help='Dry run. Print tests, but take no other action.')
81624bb1048SAlexander Pyhalov    parser.add_option('-g', action='store_true', default=False,
81724bb1048SAlexander Pyhalov                      dest='do_groups', help='Make directories TestGroups.')
81824bb1048SAlexander Pyhalov    parser.add_option('-o', action='callback', callback=options_cb,
81924bb1048SAlexander Pyhalov                      default=BASEDIR, dest='outputdir', type='string',
82024bb1048SAlexander Pyhalov                      metavar='outputdir', help='Specify an output directory.')
82124bb1048SAlexander Pyhalov    parser.add_option('-p', action='callback', callback=options_cb,
82224bb1048SAlexander Pyhalov                      default='', dest='pre', metavar='script',
82324bb1048SAlexander Pyhalov                      type='string', help='Specify a pre script.')
82424bb1048SAlexander Pyhalov    parser.add_option('-P', action='callback', callback=options_cb,
82524bb1048SAlexander Pyhalov                      default='', dest='post', metavar='script',
82624bb1048SAlexander Pyhalov                      type='string', help='Specify a post script.')
82724bb1048SAlexander Pyhalov    parser.add_option('-q', action='store_true', default=False, dest='quiet',
82824bb1048SAlexander Pyhalov                      help='Silence on the console during a test run.')
82924bb1048SAlexander Pyhalov    parser.add_option('-t', action='callback', callback=options_cb, default=60,
83024bb1048SAlexander Pyhalov                      dest='timeout', metavar='seconds', type='int',
83124bb1048SAlexander Pyhalov                      help='Timeout (in seconds) for an individual test.')
83224bb1048SAlexander Pyhalov    parser.add_option('-u', action='callback', callback=options_cb,
83324bb1048SAlexander Pyhalov                      default='', dest='user', metavar='user', type='string',
83424bb1048SAlexander Pyhalov                      help='Specify a different user name to run as.')
83524bb1048SAlexander Pyhalov    parser.add_option('-w', action='callback', callback=options_cb,
83624bb1048SAlexander Pyhalov                      default=None, dest='template', metavar='template',
83724bb1048SAlexander Pyhalov                      type='string', help='Create a new config file.')
83824bb1048SAlexander Pyhalov    parser.add_option('-x', action='callback', callback=options_cb, default='',
83924bb1048SAlexander Pyhalov                      dest='pre_user', metavar='pre_user', type='string',
84024bb1048SAlexander Pyhalov                      help='Specify a user to execute the pre script.')
84124bb1048SAlexander Pyhalov    parser.add_option('-X', action='callback', callback=options_cb, default='',
84224bb1048SAlexander Pyhalov                      dest='post_user', metavar='post_user', type='string',
84324bb1048SAlexander Pyhalov                      help='Specify a user to execute the post script.')
84424bb1048SAlexander Pyhalov    (options, pathnames) = parser.parse_args()
84524bb1048SAlexander Pyhalov
84624bb1048SAlexander Pyhalov    if not options.runfile and not options.template:
84724bb1048SAlexander Pyhalov        options.cmd = 'runtests'
84824bb1048SAlexander Pyhalov
84924bb1048SAlexander Pyhalov    if options.runfile and len(pathnames):
85024bb1048SAlexander Pyhalov        fail('Extraneous arguments.')
85124bb1048SAlexander Pyhalov
85224bb1048SAlexander Pyhalov    options.pathnames = [os.path.abspath(path) for path in pathnames]
85324bb1048SAlexander Pyhalov
85424bb1048SAlexander Pyhalov    return options
85524bb1048SAlexander Pyhalov
85624bb1048SAlexander Pyhalov
85724bb1048SAlexander Pyhalovdef main(args):
85824bb1048SAlexander Pyhalov    options = parse_args()
85924bb1048SAlexander Pyhalov    testrun = TestRun(options)
86024bb1048SAlexander Pyhalov
86124bb1048SAlexander Pyhalov    if options.cmd is 'runtests':
86224bb1048SAlexander Pyhalov        find_tests(testrun, options)
86324bb1048SAlexander Pyhalov    elif options.cmd is 'rdconfig':
86424bb1048SAlexander Pyhalov        testrun.read(testrun.logger, options)
86524bb1048SAlexander Pyhalov    elif options.cmd is 'wrconfig':
86624bb1048SAlexander Pyhalov        find_tests(testrun, options)
86724bb1048SAlexander Pyhalov        testrun.write(options)
86824bb1048SAlexander Pyhalov        exit(0)
86924bb1048SAlexander Pyhalov    else:
87024bb1048SAlexander Pyhalov        fail('Unknown command specified')
87124bb1048SAlexander Pyhalov
87224bb1048SAlexander Pyhalov    testrun.complete_outputdirs(options)
87324bb1048SAlexander Pyhalov    testrun.run(options)
87424bb1048SAlexander Pyhalov    testrun.summary()
87524bb1048SAlexander Pyhalov    exit(0)
87624bb1048SAlexander Pyhalov
87724bb1048SAlexander Pyhalov
87824bb1048SAlexander Pyhalovif __name__ == '__main__':
87924bb1048SAlexander Pyhalov    main(argv[1:])
880