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