xref: /illumos-gate/usr/src/test/test-runner/cmd/run (revision e581456dc8e63dfcc1866df3444fe7b59c5d0d1c)
19f923083SAlexander Pyhalov#!@PYTHON@
29f923083SAlexander Pyhalov
39f923083SAlexander Pyhalov#
49f923083SAlexander Pyhalov# This file and its contents are supplied under the terms of the
59f923083SAlexander Pyhalov# Common Development and Distribution License ("CDDL"), version 1.0.
69f923083SAlexander Pyhalov# You may only use this file in accordance with the terms of version
79f923083SAlexander Pyhalov# 1.0 of the CDDL.
89f923083SAlexander Pyhalov#
99f923083SAlexander Pyhalov# A full copy of the text of the CDDL should have accompanied this
109f923083SAlexander Pyhalov# source.  A copy of the CDDL is also available via the Internet at
119f923083SAlexander Pyhalov# http://www.illumos.org/license/CDDL.
129f923083SAlexander Pyhalov#
139f923083SAlexander Pyhalov
149f923083SAlexander Pyhalov#
15dfc11533SChris Williamson# Copyright (c) 2012, 2016 by Delphix. All rights reserved.
1664ee6612SChris Fraire# Copyright (c) 2017, Chris Fraire <cfraire@me.com>.
172491fc01SJohn Levon# Copyright 2019 Joyent, Inc.
181a2acdcdSAndy Fiddaman# Copyright 2020 OmniOS Community Edition (OmniOSce) Association.
199f923083SAlexander Pyhalov#
209f923083SAlexander Pyhalov
21b0858fdcSAlexander Pyhalovfrom __future__ import print_function
22b0858fdcSAlexander Pyhalovimport sys
23b0858fdcSAlexander PyhalovPY3 = sys.version_info[0] == 3
24b0858fdcSAlexander Pyhalov
25b0858fdcSAlexander Pyhalovif PY3:
26b0858fdcSAlexander Pyhalov    import configparser
27b0858fdcSAlexander Pyhalovelse:
28b0858fdcSAlexander Pyhalov    import ConfigParser as configparser
29b0858fdcSAlexander Pyhalov
30cdd7a662SAlexander Pyhalovimport io
319f923083SAlexander Pyhalovimport os
329f923083SAlexander Pyhalovimport logging
33a0955b86SJohn Levonimport platform
3478801af7SAndy Fiddamanimport re
359f923083SAlexander Pyhalovfrom logging.handlers import WatchedFileHandler
369f923083SAlexander Pyhalovfrom datetime import datetime
379f923083SAlexander Pyhalovfrom optparse import OptionParser
389f923083SAlexander Pyhalovfrom pwd import getpwnam
399f923083SAlexander Pyhalovfrom pwd import getpwuid
409f923083SAlexander Pyhalovfrom select import select
419f923083SAlexander Pyhalovfrom subprocess import PIPE
429f923083SAlexander Pyhalovfrom subprocess import Popen
439f923083SAlexander Pyhalovfrom sys import argv
44b0858fdcSAlexander Pyhalovfrom sys import exit
45b0858fdcSAlexander Pyhalovfrom sys import maxsize
469f923083SAlexander Pyhalovfrom threading import Timer
479f923083SAlexander Pyhalovfrom time import time
489f923083SAlexander Pyhalov
499f923083SAlexander PyhalovBASEDIR = '/var/tmp/test_results'
50b8052df9SRyan MoellerTESTDIR = '/opt/zfs-tests/'
519f923083SAlexander PyhalovKILL = '/usr/bin/kill'
529f923083SAlexander PyhalovTRUE = '/usr/bin/true'
539f923083SAlexander PyhalovSUDO = '/usr/bin/sudo'
549f923083SAlexander Pyhalov
552491fc01SJohn Levonretcode = 0
562491fc01SJohn Levon
579f923083SAlexander Pyhalov# Custom class to reopen the log file in case it is forcibly closed by a test.
589f923083SAlexander Pyhalovclass WatchedFileHandlerClosed(WatchedFileHandler):
599f923083SAlexander Pyhalov    """Watch files, including closed files.
609f923083SAlexander Pyhalov    Similar to (and inherits from) logging.handler.WatchedFileHandler,
619f923083SAlexander Pyhalov    except that IOErrors are handled by reopening the stream and retrying.
629f923083SAlexander Pyhalov    This will be retried up to a configurable number of times before
639f923083SAlexander Pyhalov    giving up, default 5.
649f923083SAlexander Pyhalov    """
659f923083SAlexander Pyhalov
66cdd7a662SAlexander Pyhalov    def __init__(self, filename, mode='a', encoding='utf-8', delay=0, max_tries=5):
679f923083SAlexander Pyhalov        self.max_tries = max_tries
689f923083SAlexander Pyhalov        self.tries = 0
699f923083SAlexander Pyhalov        WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
709f923083SAlexander Pyhalov
719f923083SAlexander Pyhalov    def emit(self, record):
729f923083SAlexander Pyhalov        while True:
739f923083SAlexander Pyhalov            try:
749f923083SAlexander Pyhalov                WatchedFileHandler.emit(self, record)
759f923083SAlexander Pyhalov                self.tries = 0
769f923083SAlexander Pyhalov                return
779f923083SAlexander Pyhalov            except IOError as err:
789f923083SAlexander Pyhalov                if self.tries == self.max_tries:
799f923083SAlexander Pyhalov                    raise
809f923083SAlexander Pyhalov                self.stream.close()
819f923083SAlexander Pyhalov                self.stream = self._open()
829f923083SAlexander Pyhalov                self.tries += 1
839f923083SAlexander Pyhalov
849f923083SAlexander Pyhalovclass Result(object):
859f923083SAlexander Pyhalov    total = 0
869f923083SAlexander Pyhalov    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
879f923083SAlexander Pyhalov
889f923083SAlexander Pyhalov    def __init__(self):
899f923083SAlexander Pyhalov        self.starttime = None
909f923083SAlexander Pyhalov        self.returncode = None
919f923083SAlexander Pyhalov        self.runtime = ''
929f923083SAlexander Pyhalov        self.stdout = []
939f923083SAlexander Pyhalov        self.stderr = []
949f923083SAlexander Pyhalov        self.result = ''
959f923083SAlexander Pyhalov
969f923083SAlexander Pyhalov    def done(self, proc, killed):
979f923083SAlexander Pyhalov        """
989f923083SAlexander Pyhalov        Finalize the results of this Cmd.
9996c8483aSYuri Pankov        Report SKIP for return codes 3,4 (NOTINUSE, UNSUPPORTED)
10096c8483aSYuri Pankov        as defined in ../stf/include/stf.shlib
1019f923083SAlexander Pyhalov        """
1022491fc01SJohn Levon        global retcode
1032491fc01SJohn Levon
1049f923083SAlexander Pyhalov        Result.total += 1
1059f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
1069f923083SAlexander Pyhalov        self.runtime = '%02d:%02d' % (m, s)
1079f923083SAlexander Pyhalov        self.returncode = proc.returncode
1089f923083SAlexander Pyhalov        if killed:
1099f923083SAlexander Pyhalov            self.result = 'KILLED'
1109f923083SAlexander Pyhalov            Result.runresults['KILLED'] += 1
1112491fc01SJohn Levon            retcode = 2;
1121a2acdcdSAndy Fiddaman        elif self.returncode == 0:
1139f923083SAlexander Pyhalov            self.result = 'PASS'
1149f923083SAlexander Pyhalov            Result.runresults['PASS'] += 1
1151a2acdcdSAndy Fiddaman        elif self.returncode == 3 or self.returncode == 4:
11696c8483aSYuri Pankov            self.result = 'SKIP'
11796c8483aSYuri Pankov            Result.runresults['SKIP'] += 1
1181a2acdcdSAndy Fiddaman        elif self.returncode != 0:
1199f923083SAlexander Pyhalov            self.result = 'FAIL'
1209f923083SAlexander Pyhalov            Result.runresults['FAIL'] += 1
1212491fc01SJohn Levon            retcode = 1;
1229f923083SAlexander Pyhalov
1239f923083SAlexander Pyhalov
1249f923083SAlexander Pyhalovclass Output(object):
1259f923083SAlexander Pyhalov    """
1269f923083SAlexander Pyhalov    This class is a slightly modified version of the 'Stream' class found
1279f923083SAlexander Pyhalov    here: http://goo.gl/aSGfv
1289f923083SAlexander Pyhalov    """
1299f923083SAlexander Pyhalov    def __init__(self, stream):
1309f923083SAlexander Pyhalov        self.stream = stream
1319f923083SAlexander Pyhalov        self._buf = ''
1329f923083SAlexander Pyhalov        self.lines = []
1339f923083SAlexander Pyhalov
1349f923083SAlexander Pyhalov    def fileno(self):
1359f923083SAlexander Pyhalov        return self.stream.fileno()
1369f923083SAlexander Pyhalov
1379f923083SAlexander Pyhalov    def read(self, drain=0):
1389f923083SAlexander Pyhalov        """
1399f923083SAlexander Pyhalov        Read from the file descriptor. If 'drain' set, read until EOF.
1409f923083SAlexander Pyhalov        """
1419f923083SAlexander Pyhalov        while self._read() is not None:
1429f923083SAlexander Pyhalov            if not drain:
1439f923083SAlexander Pyhalov                break
1449f923083SAlexander Pyhalov
1459f923083SAlexander Pyhalov    def _read(self):
1469f923083SAlexander Pyhalov        """
1479f923083SAlexander Pyhalov        Read up to 4k of data from this output stream. Collect the output
1489f923083SAlexander Pyhalov        up to the last newline, and append it to any leftover data from a
1499f923083SAlexander Pyhalov        previous call. The lines are stored as a (timestamp, data) tuple
1509f923083SAlexander Pyhalov        for easy sorting/merging later.
1519f923083SAlexander Pyhalov        """
1529f923083SAlexander Pyhalov        fd = self.fileno()
153cc37296fSAndy Fiddaman        buf = os.read(fd, 4096).decode('utf-8', errors='ignore')
1549f923083SAlexander Pyhalov        if not buf:
1559f923083SAlexander Pyhalov            return None
1569f923083SAlexander Pyhalov        if '\n' not in buf:
1579f923083SAlexander Pyhalov            self._buf += buf
1589f923083SAlexander Pyhalov            return []
1599f923083SAlexander Pyhalov
1609f923083SAlexander Pyhalov        buf = self._buf + buf
1619f923083SAlexander Pyhalov        tmp, rest = buf.rsplit('\n', 1)
1629f923083SAlexander Pyhalov        self._buf = rest
1639f923083SAlexander Pyhalov        now = datetime.now()
1649f923083SAlexander Pyhalov        rows = tmp.split('\n')
1659f923083SAlexander Pyhalov        self.lines += [(now, r) for r in rows]
1669f923083SAlexander Pyhalov
1679f923083SAlexander Pyhalov
1689f923083SAlexander Pyhalovclass Cmd(object):
1699f923083SAlexander Pyhalov    verified_users = []
1709f923083SAlexander Pyhalov
171b8052df9SRyan Moeller    def __init__(self, pathname, identifier=None, outputdir=None,
172b8052df9SRyan Moeller                 timeout=None, user=None, tags=None):
1739f923083SAlexander Pyhalov        self.pathname = pathname
174b8052df9SRyan Moeller        self.identifier = identifier
1759f923083SAlexander Pyhalov        self.outputdir = outputdir or 'BASEDIR'
1769f923083SAlexander Pyhalov        self.timeout = timeout
1779f923083SAlexander Pyhalov        self.user = user or ''
1789f923083SAlexander Pyhalov        self.killed = False
1799f923083SAlexander Pyhalov        self.result = Result()
1809f923083SAlexander Pyhalov
1819f923083SAlexander Pyhalov        if self.timeout is None:
1829f923083SAlexander Pyhalov            self.timeout = 60
1839f923083SAlexander Pyhalov
1849f923083SAlexander Pyhalov    def __str__(self):
185b8052df9SRyan Moeller        return '''\
186b8052df9SRyan MoellerPathname: %s
187b8052df9SRyan MoellerIdentifier: %s
188b8052df9SRyan MoellerOutputdir: %s
189b8052df9SRyan MoellerTimeout: %d
190b8052df9SRyan MoellerUser: %s
191b8052df9SRyan Moeller''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user)
1929f923083SAlexander Pyhalov
1939f923083SAlexander Pyhalov    def kill_cmd(self, proc):
1949f923083SAlexander Pyhalov        """
1959f923083SAlexander Pyhalov        Kill a running command due to timeout, or ^C from the keyboard. If
1969f923083SAlexander Pyhalov        sudo is required, this user was verified previously.
1979f923083SAlexander Pyhalov        """
1989f923083SAlexander Pyhalov        self.killed = True
1999f923083SAlexander Pyhalov        do_sudo = len(self.user) != 0
2009f923083SAlexander Pyhalov        signal = '-TERM'
2019f923083SAlexander Pyhalov
2029f923083SAlexander Pyhalov        cmd = [SUDO, KILL, signal, str(proc.pid)]
2039f923083SAlexander Pyhalov        if not do_sudo:
2049f923083SAlexander Pyhalov            del cmd[0]
2059f923083SAlexander Pyhalov
2069f923083SAlexander Pyhalov        try:
2079f923083SAlexander Pyhalov            kp = Popen(cmd)
2089f923083SAlexander Pyhalov            kp.wait()
2099f923083SAlexander Pyhalov        except:
2109f923083SAlexander Pyhalov            pass
2119f923083SAlexander Pyhalov
2129f923083SAlexander Pyhalov    def update_cmd_privs(self, cmd, user):
2139f923083SAlexander Pyhalov        """
2149f923083SAlexander Pyhalov        If a user has been specified to run this Cmd and we're not already
2159f923083SAlexander Pyhalov        running as that user, prepend the appropriate sudo command to run
2169f923083SAlexander Pyhalov        as that user.
2179f923083SAlexander Pyhalov        """
2189f923083SAlexander Pyhalov        me = getpwuid(os.getuid())
2199f923083SAlexander Pyhalov
220*e581456dSPatrick Mooney        if not user or user == me.pw_name:
2219f923083SAlexander Pyhalov            return cmd
2229f923083SAlexander Pyhalov
2239f923083SAlexander Pyhalov        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
2249f923083SAlexander Pyhalov        return ret.split(' ')
2259f923083SAlexander Pyhalov
2269f923083SAlexander Pyhalov    def collect_output(self, proc):
2279f923083SAlexander Pyhalov        """
2289f923083SAlexander Pyhalov        Read from stdout/stderr as data becomes available, until the
2299f923083SAlexander Pyhalov        process is no longer running. Return the lines from the stdout and
2309f923083SAlexander Pyhalov        stderr Output objects.
2319f923083SAlexander Pyhalov        """
2329f923083SAlexander Pyhalov        out = Output(proc.stdout)
2339f923083SAlexander Pyhalov        err = Output(proc.stderr)
2349f923083SAlexander Pyhalov        res = []
2359f923083SAlexander Pyhalov        while proc.returncode is None:
2369f923083SAlexander Pyhalov            proc.poll()
2379f923083SAlexander Pyhalov            res = select([out, err], [], [], .1)
2389f923083SAlexander Pyhalov            for fd in res[0]:
2399f923083SAlexander Pyhalov                fd.read()
2409f923083SAlexander Pyhalov        for fd in res[0]:
2419f923083SAlexander Pyhalov            fd.read(drain=1)
2429f923083SAlexander Pyhalov
2439f923083SAlexander Pyhalov        return out.lines, err.lines
2449f923083SAlexander Pyhalov
2459f923083SAlexander Pyhalov    def run(self, options):
2469f923083SAlexander Pyhalov        """
2479f923083SAlexander Pyhalov        This is the main function that runs each individual test.
2489f923083SAlexander Pyhalov        Determine whether or not the command requires sudo, and modify it
2499f923083SAlexander Pyhalov        if needed. Run the command, and update the result object.
2509f923083SAlexander Pyhalov        """
2519f923083SAlexander Pyhalov        if options.dryrun is True:
252b0858fdcSAlexander Pyhalov            print(self)
2539f923083SAlexander Pyhalov            return
2549f923083SAlexander Pyhalov
2559f923083SAlexander Pyhalov        privcmd = self.update_cmd_privs(self.pathname, self.user)
2569f923083SAlexander Pyhalov        try:
2579f923083SAlexander Pyhalov            old = os.umask(0)
2589f923083SAlexander Pyhalov            if not os.path.isdir(self.outputdir):
259b0858fdcSAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
2609f923083SAlexander Pyhalov            os.umask(old)
261b0858fdcSAlexander Pyhalov        except OSError as e:
2629f923083SAlexander Pyhalov            fail('%s' % e)
2639f923083SAlexander Pyhalov
2649f923083SAlexander Pyhalov        try:
2659f923083SAlexander Pyhalov            self.result.starttime = time()
266b0858fdcSAlexander Pyhalov            proc = Popen(privcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE,
267b0858fdcSAlexander Pyhalov                         universal_newlines=True)
2685cabbc6bSPrashanth Sreenivasa            proc.stdin.close()
2699f923083SAlexander Pyhalov
2709f923083SAlexander Pyhalov            # Allow a special timeout value of 0 to mean infinity
2719f923083SAlexander Pyhalov            if int(self.timeout) == 0:
272b0858fdcSAlexander Pyhalov                self.timeout = maxsize
2739f923083SAlexander Pyhalov            t = Timer(int(self.timeout), self.kill_cmd, [proc])
2749f923083SAlexander Pyhalov            t.start()
2759f923083SAlexander Pyhalov            self.result.stdout, self.result.stderr = self.collect_output(proc)
2769f923083SAlexander Pyhalov        except KeyboardInterrupt:
2779f923083SAlexander Pyhalov            self.kill_cmd(proc)
2789f923083SAlexander Pyhalov            fail('\nRun terminated at user request.')
2799f923083SAlexander Pyhalov        finally:
2809f923083SAlexander Pyhalov            t.cancel()
2819f923083SAlexander Pyhalov
2829f923083SAlexander Pyhalov        self.result.done(proc, self.killed)
2839f923083SAlexander Pyhalov
2849f923083SAlexander Pyhalov    def skip(self):
2859f923083SAlexander Pyhalov        """
2869f923083SAlexander Pyhalov        Initialize enough of the test result that we can log a skipped
2879f923083SAlexander Pyhalov        command.
2889f923083SAlexander Pyhalov        """
2899f923083SAlexander Pyhalov        Result.total += 1
2909f923083SAlexander Pyhalov        Result.runresults['SKIP'] += 1
2919f923083SAlexander Pyhalov        self.result.stdout = self.result.stderr = []
2929f923083SAlexander Pyhalov        self.result.starttime = time()
2939f923083SAlexander Pyhalov        m, s = divmod(time() - self.result.starttime, 60)
2949f923083SAlexander Pyhalov        self.result.runtime = '%02d:%02d' % (m, s)
2959f923083SAlexander Pyhalov        self.result.result = 'SKIP'
2969f923083SAlexander Pyhalov
2979f923083SAlexander Pyhalov    def log(self, logger, options):
2989f923083SAlexander Pyhalov        """
2999f923083SAlexander Pyhalov        This function is responsible for writing all output. This includes
3009f923083SAlexander Pyhalov        the console output, the logfile of all results (with timestamped
3019f923083SAlexander Pyhalov        merged stdout and stderr), and for each test, the unmodified
3029f923083SAlexander Pyhalov        stdout/stderr/merged in it's own file.
3039f923083SAlexander Pyhalov        """
3049f923083SAlexander Pyhalov        if logger is None:
3059f923083SAlexander Pyhalov            return
3069f923083SAlexander Pyhalov
3079f923083SAlexander Pyhalov        logname = getpwuid(os.getuid()).pw_name
3089f923083SAlexander Pyhalov        user = ' (run as %s)' % (self.user if len(self.user) else logname)
309b8052df9SRyan Moeller        if self.identifier:
310b8052df9SRyan Moeller            msga = 'Test (%s): %s%s ' % (self.identifier, self.pathname, user)
311b8052df9SRyan Moeller        else:
3129f923083SAlexander Pyhalov            msga = 'Test: %s%s ' % (self.pathname, user)
3139f923083SAlexander Pyhalov        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
3149f923083SAlexander Pyhalov        pad = ' ' * (80 - (len(msga) + len(msgb)))
3159f923083SAlexander Pyhalov
3169f923083SAlexander Pyhalov        # If -q is specified, only print a line for tests that didn't pass.
3179f923083SAlexander Pyhalov        # This means passing tests need to be logged as DEBUG, or the one
3189f923083SAlexander Pyhalov        # line summary will only be printed in the logfile for failures.
3199f923083SAlexander Pyhalov        if not options.quiet:
3209f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
3211a2acdcdSAndy Fiddaman        elif self.result.result != 'PASS':
3229f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
3239f923083SAlexander Pyhalov        else:
3249f923083SAlexander Pyhalov            logger.debug('%s%s%s' % (msga, pad, msgb))
3259f923083SAlexander Pyhalov
3269f923083SAlexander Pyhalov        lines = sorted(self.result.stdout + self.result.stderr,
327b0858fdcSAlexander Pyhalov                       key=lambda x: x[0])
3289f923083SAlexander Pyhalov
3299f923083SAlexander Pyhalov        for dt, line in lines:
3309f923083SAlexander Pyhalov            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
3319f923083SAlexander Pyhalov
3329f923083SAlexander Pyhalov        if len(self.result.stdout):
333cdd7a662SAlexander Pyhalov            with io.open(os.path.join(self.outputdir, 'stdout'),
334cdd7a662SAlexander Pyhalov                                   encoding='utf-8',
335cdd7a662SAlexander Pyhalov                                   errors='surrogateescape',
336cdd7a662SAlexander Pyhalov                                   mode='w') as out:
3379f923083SAlexander Pyhalov                for _, line in self.result.stdout:
338b0858fdcSAlexander Pyhalov                    out.write('%s\n' % line)
3399f923083SAlexander Pyhalov        if len(self.result.stderr):
340cdd7a662SAlexander Pyhalov            with io.open(os.path.join(self.outputdir, 'stderr'),
341cdd7a662SAlexander Pyhalov                                   encoding='utf-8',
342cdd7a662SAlexander Pyhalov                                   errors='surrogateescape',
343cdd7a662SAlexander Pyhalov                                   mode='w') as err:
3449f923083SAlexander Pyhalov                for _, line in self.result.stderr:
345b0858fdcSAlexander Pyhalov                    err.write('%s\n' % line)
3469f923083SAlexander Pyhalov        if len(self.result.stdout) and len(self.result.stderr):
347cdd7a662SAlexander Pyhalov            with io.open(os.path.join(self.outputdir, 'merged'),
348cdd7a662SAlexander Pyhalov                                   encoding='utf-8',
349cdd7a662SAlexander Pyhalov                                   errors='surrogateescape',
350cdd7a662SAlexander Pyhalov                                   mode='w') as merged:
3519f923083SAlexander Pyhalov                for _, line in lines:
352b0858fdcSAlexander Pyhalov                    merged.write('%s\n' % line)
3539f923083SAlexander Pyhalov
3549f923083SAlexander Pyhalov
3559f923083SAlexander Pyhalovclass Test(Cmd):
3569f923083SAlexander Pyhalov    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
357b8052df9SRyan Moeller             'post_user', 'tags']
3589f923083SAlexander Pyhalov
359b8052df9SRyan Moeller    def __init__(self, pathname,
360b8052df9SRyan Moeller                 pre=None, pre_user=None, post=None, post_user=None,
361b8052df9SRyan Moeller                 tags=None, **kwargs):
362b8052df9SRyan Moeller        super(Test, self).__init__(pathname, **kwargs)
3639f923083SAlexander Pyhalov        self.pre = pre or ''
3649f923083SAlexander Pyhalov        self.pre_user = pre_user or ''
3659f923083SAlexander Pyhalov        self.post = post or ''
3669f923083SAlexander Pyhalov        self.post_user = post_user or ''
367b8052df9SRyan Moeller        self.tags = tags or []
3689f923083SAlexander Pyhalov
3699f923083SAlexander Pyhalov    def __str__(self):
3709f923083SAlexander Pyhalov        post_user = pre_user = ''
3719f923083SAlexander Pyhalov        if len(self.pre_user):
3729f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
3739f923083SAlexander Pyhalov        if len(self.post_user):
3749f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
375b8052df9SRyan Moeller        return '''\
376b8052df9SRyan MoellerPathname: %s
377b8052df9SRyan MoellerIdentifier: %s
378b8052df9SRyan MoellerOutputdir: %s
379b8052df9SRyan MoellerTimeout: %d
380b8052df9SRyan MoellerUser: %s
381b8052df9SRyan MoellerPre: %s%s
382b8052df9SRyan MoellerPost: %s%s
383b8052df9SRyan MoellerTags: %s
384b8052df9SRyan Moeller''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user,
385b8052df9SRyan Moeller            self.pre, pre_user, self.post, post_user, self.tags)
3869f923083SAlexander Pyhalov
3879f923083SAlexander Pyhalov    def verify(self, logger):
3889f923083SAlexander Pyhalov        """
3899f923083SAlexander Pyhalov        Check the pre/post scripts, user and Test. Omit the Test from this
3909f923083SAlexander Pyhalov        run if there are any problems.
3919f923083SAlexander Pyhalov        """
3929f923083SAlexander Pyhalov        files = [self.pre, self.pathname, self.post]
3939f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
3949f923083SAlexander Pyhalov
3959f923083SAlexander Pyhalov        for f in [f for f in files if len(f)]:
3969f923083SAlexander Pyhalov            if not verify_file(f):
3979f923083SAlexander Pyhalov                logger.info("Warning: Test '%s' not added to this run because"
3989f923083SAlexander Pyhalov                            " it failed verification." % f)
3999f923083SAlexander Pyhalov                return False
4009f923083SAlexander Pyhalov
4019f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
4029f923083SAlexander Pyhalov            if not verify_user(user, logger):
4039f923083SAlexander Pyhalov                logger.info("Not adding Test '%s' to this run." %
4049f923083SAlexander Pyhalov                            self.pathname)
4059f923083SAlexander Pyhalov                return False
4069f923083SAlexander Pyhalov
4079f923083SAlexander Pyhalov        return True
4089f923083SAlexander Pyhalov
4099f923083SAlexander Pyhalov    def run(self, logger, options):
4109f923083SAlexander Pyhalov        """
4119f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
4129f923083SAlexander Pyhalov        doesn't pass, skip this Test. Run the post script regardless.
4139f923083SAlexander Pyhalov        """
4149f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
415b8052df9SRyan Moeller        pretest = Cmd(self.pre, identifier=self.identifier, outputdir=odir,
416b8052df9SRyan Moeller                      timeout=self.timeout, user=self.pre_user)
417b8052df9SRyan Moeller        test = Cmd(self.pathname, identifier=self.identifier,
418b8052df9SRyan Moeller                   outputdir=self.outputdir, timeout=self.timeout,
419b8052df9SRyan Moeller                   user=self.user)
4209f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
421b8052df9SRyan Moeller        posttest = Cmd(self.post, identifier=self.identifier, outputdir=odir,
422b8052df9SRyan Moeller                       timeout=self.timeout, user=self.post_user)
4239f923083SAlexander Pyhalov
4249f923083SAlexander Pyhalov        cont = True
4259f923083SAlexander Pyhalov        if len(pretest.pathname):
4269f923083SAlexander Pyhalov            pretest.run(options)
4271a2acdcdSAndy Fiddaman            cont = pretest.result.result == 'PASS'
4289f923083SAlexander Pyhalov            pretest.log(logger, options)
4299f923083SAlexander Pyhalov
4309f923083SAlexander Pyhalov        if cont:
4319f923083SAlexander Pyhalov            test.run(options)
4329f923083SAlexander Pyhalov        else:
4339f923083SAlexander Pyhalov            test.skip()
4349f923083SAlexander Pyhalov
4359f923083SAlexander Pyhalov        test.log(logger, options)
4369f923083SAlexander Pyhalov
4379f923083SAlexander Pyhalov        if len(posttest.pathname):
4389f923083SAlexander Pyhalov            posttest.run(options)
4399f923083SAlexander Pyhalov            posttest.log(logger, options)
4409f923083SAlexander Pyhalov
4419f923083SAlexander Pyhalov
4429f923083SAlexander Pyhalovclass TestGroup(Test):
4439f923083SAlexander Pyhalov    props = Test.props + ['tests']
4449f923083SAlexander Pyhalov
445b8052df9SRyan Moeller    def __init__(self, pathname, tests=None, **kwargs):
446b8052df9SRyan Moeller        super(TestGroup, self).__init__(pathname, **kwargs)
4479f923083SAlexander Pyhalov        self.tests = tests or []
4489f923083SAlexander Pyhalov
4499f923083SAlexander Pyhalov    def __str__(self):
4509f923083SAlexander Pyhalov        post_user = pre_user = ''
4519f923083SAlexander Pyhalov        if len(self.pre_user):
4529f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
4539f923083SAlexander Pyhalov        if len(self.post_user):
4549f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
455b8052df9SRyan Moeller        return '''\
456b8052df9SRyan MoellerPathname: %s
457b8052df9SRyan MoellerIdentifier: %s
458b8052df9SRyan MoellerOutputdir: %s
459b8052df9SRyan MoellerTests: %s
460b8052df9SRyan MoellerTimeout: %d
461b8052df9SRyan MoellerUser: %s
462b8052df9SRyan MoellerPre: %s%s
463b8052df9SRyan MoellerPost: %s%s
464b8052df9SRyan MoellerTags: %s
465b8052df9SRyan Moeller''' % (self.pathname, self.identifier, self.outputdir, self.tests,
466b8052df9SRyan Moeller            self.timeout, self.user, self.pre, pre_user, self.post, post_user,
467b8052df9SRyan Moeller            self.tags)
4689f923083SAlexander Pyhalov
46978801af7SAndy Fiddaman    def filter(self, keeplist):
47078801af7SAndy Fiddaman        self.tests = [ x for x in self.tests if x in keeplist ]
47178801af7SAndy Fiddaman
4729f923083SAlexander Pyhalov    def verify(self, logger):
4739f923083SAlexander Pyhalov        """
4749f923083SAlexander Pyhalov        Check the pre/post scripts, user and tests in this TestGroup. Omit
4759f923083SAlexander Pyhalov        the TestGroup entirely, or simply delete the relevant tests in the
4769f923083SAlexander Pyhalov        group, if that's all that's required.
4779f923083SAlexander Pyhalov        """
4789f923083SAlexander Pyhalov        # If the pre or post scripts are relative pathnames, convert to
4799f923083SAlexander Pyhalov        # absolute, so they stand a chance of passing verification.
4809f923083SAlexander Pyhalov        if len(self.pre) and not os.path.isabs(self.pre):
4819f923083SAlexander Pyhalov            self.pre = os.path.join(self.pathname, self.pre)
4829f923083SAlexander Pyhalov        if len(self.post) and not os.path.isabs(self.post):
4839f923083SAlexander Pyhalov            self.post = os.path.join(self.pathname, self.post)
4849f923083SAlexander Pyhalov
4859f923083SAlexander Pyhalov        auxfiles = [self.pre, self.post]
4869f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
4879f923083SAlexander Pyhalov
4889f923083SAlexander Pyhalov        for f in [f for f in auxfiles if len(f)]:
4899f923083SAlexander Pyhalov            if self.pathname != os.path.dirname(f):
4909f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4919f923083SAlexander Pyhalov                            "Auxiliary script '%s' exists in a different "
4929f923083SAlexander Pyhalov                            "directory." % (self.pathname, f))
4939f923083SAlexander Pyhalov                return False
4949f923083SAlexander Pyhalov
4959f923083SAlexander Pyhalov            if not verify_file(f):
4969f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
4979f923083SAlexander Pyhalov                            "Auxiliary script '%s' failed verification." %
4989f923083SAlexander Pyhalov                            (self.pathname, f))
4999f923083SAlexander Pyhalov                return False
5009f923083SAlexander Pyhalov
5019f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
5029f923083SAlexander Pyhalov            if not verify_user(user, logger):
5039f923083SAlexander Pyhalov                logger.info("Not adding TestGroup '%s' to this run." %
5049f923083SAlexander Pyhalov                            self.pathname)
5059f923083SAlexander Pyhalov                return False
5069f923083SAlexander Pyhalov
5079f923083SAlexander Pyhalov        # If one of the tests is invalid, delete it, log it, and drive on.
508dfc11533SChris Williamson        self.tests[:] = [f for f in self.tests if
509dfc11533SChris Williamson          verify_file(os.path.join(self.pathname, f))]
5109f923083SAlexander Pyhalov
5111a2acdcdSAndy Fiddaman        return len(self.tests) != 0
5129f923083SAlexander Pyhalov
5139f923083SAlexander Pyhalov    def run(self, logger, options):
5149f923083SAlexander Pyhalov        """
5159f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
5169f923083SAlexander Pyhalov        doesn't pass, skip all the tests in this TestGroup. Run the post
5179f923083SAlexander Pyhalov        script regardless.
5189f923083SAlexander Pyhalov        """
519b8052df9SRyan Moeller        # tags assigned to this test group also include the test names
520b8052df9SRyan Moeller        if options.tags and not set(self.tags).intersection(set(options.tags)):
521b8052df9SRyan Moeller            return
522b8052df9SRyan Moeller
5239f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
5249f923083SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
525b8052df9SRyan Moeller                      user=self.pre_user, identifier=self.identifier)
5269f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
5279f923083SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
528b8052df9SRyan Moeller                       user=self.post_user, identifier=self.identifier)
5299f923083SAlexander Pyhalov
5309f923083SAlexander Pyhalov        cont = True
5319f923083SAlexander Pyhalov        if len(pretest.pathname):
5329f923083SAlexander Pyhalov            pretest.run(options)
5331a2acdcdSAndy Fiddaman            cont = pretest.result.result == 'PASS'
5349f923083SAlexander Pyhalov            pretest.log(logger, options)
5359f923083SAlexander Pyhalov
5369f923083SAlexander Pyhalov        for fname in self.tests:
5379f923083SAlexander Pyhalov            test = Cmd(os.path.join(self.pathname, fname),
5389f923083SAlexander Pyhalov                       outputdir=os.path.join(self.outputdir, fname),
539b8052df9SRyan Moeller                       timeout=self.timeout, user=self.user,
540b8052df9SRyan Moeller                       identifier=self.identifier)
5419f923083SAlexander Pyhalov            if cont:
5429f923083SAlexander Pyhalov                test.run(options)
5439f923083SAlexander Pyhalov            else:
5449f923083SAlexander Pyhalov                test.skip()
5459f923083SAlexander Pyhalov
5469f923083SAlexander Pyhalov            test.log(logger, options)
5479f923083SAlexander Pyhalov
5489f923083SAlexander Pyhalov        if len(posttest.pathname):
5499f923083SAlexander Pyhalov            posttest.run(options)
5509f923083SAlexander Pyhalov            posttest.log(logger, options)
5519f923083SAlexander Pyhalov
5529f923083SAlexander Pyhalov
5539f923083SAlexander Pyhalovclass TestRun(object):
5549f923083SAlexander Pyhalov    props = ['quiet', 'outputdir']
5559f923083SAlexander Pyhalov
5569f923083SAlexander Pyhalov    def __init__(self, options):
5579f923083SAlexander Pyhalov        self.tests = {}
5589f923083SAlexander Pyhalov        self.testgroups = {}
5599f923083SAlexander Pyhalov        self.starttime = time()
5609f923083SAlexander Pyhalov        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
5619f923083SAlexander Pyhalov        self.outputdir = os.path.join(options.outputdir, self.timestamp)
5629f923083SAlexander Pyhalov        self.logger = self.setup_logging(options)
5639f923083SAlexander Pyhalov        self.defaults = [
5649f923083SAlexander Pyhalov            ('outputdir', BASEDIR),
5659f923083SAlexander Pyhalov            ('quiet', False),
5669f923083SAlexander Pyhalov            ('timeout', 60),
5679f923083SAlexander Pyhalov            ('user', ''),
5689f923083SAlexander Pyhalov            ('pre', ''),
5699f923083SAlexander Pyhalov            ('pre_user', ''),
5709f923083SAlexander Pyhalov            ('post', ''),
571b8052df9SRyan Moeller            ('post_user', ''),
572b8052df9SRyan Moeller            ('tags', [])
5739f923083SAlexander Pyhalov        ]
5749f923083SAlexander Pyhalov
5759f923083SAlexander Pyhalov    def __str__(self):
5769f923083SAlexander Pyhalov        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
5779f923083SAlexander Pyhalov        s += 'TESTS:\n'
5789f923083SAlexander Pyhalov        for key in sorted(self.tests.keys()):
5799f923083SAlexander Pyhalov            s += '%s%s' % (self.tests[key].__str__(), '\n')
5809f923083SAlexander Pyhalov        s += 'TESTGROUPS:\n'
5819f923083SAlexander Pyhalov        for key in sorted(self.testgroups.keys()):
5829f923083SAlexander Pyhalov            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
5839f923083SAlexander Pyhalov        return s
5849f923083SAlexander Pyhalov
5859f923083SAlexander Pyhalov    def addtest(self, pathname, options):
5869f923083SAlexander Pyhalov        """
5879f923083SAlexander Pyhalov        Create a new Test, and apply any properties that were passed in
5889f923083SAlexander Pyhalov        from the command line. If it passes verification, add it to the
5899f923083SAlexander Pyhalov        TestRun.
5909f923083SAlexander Pyhalov        """
5919f923083SAlexander Pyhalov        test = Test(pathname)
5929f923083SAlexander Pyhalov        for prop in Test.props:
5939f923083SAlexander Pyhalov            setattr(test, prop, getattr(options, prop))
5949f923083SAlexander Pyhalov
5959f923083SAlexander Pyhalov        if test.verify(self.logger):
5969f923083SAlexander Pyhalov            self.tests[pathname] = test
5979f923083SAlexander Pyhalov
5989f923083SAlexander Pyhalov    def addtestgroup(self, dirname, filenames, options):
5999f923083SAlexander Pyhalov        """
6009f923083SAlexander Pyhalov        Create a new TestGroup, and apply any properties that were passed
6019f923083SAlexander Pyhalov        in from the command line. If it passes verification, add it to the
6029f923083SAlexander Pyhalov        TestRun.
6039f923083SAlexander Pyhalov        """
6049f923083SAlexander Pyhalov        if dirname not in self.testgroups:
6059f923083SAlexander Pyhalov            testgroup = TestGroup(dirname)
6069f923083SAlexander Pyhalov            for prop in Test.props:
6079f923083SAlexander Pyhalov                setattr(testgroup, prop, getattr(options, prop))
6089f923083SAlexander Pyhalov
6099f923083SAlexander Pyhalov            # Prevent pre/post scripts from running as regular tests
6109f923083SAlexander Pyhalov            for f in [testgroup.pre, testgroup.post]:
6119f923083SAlexander Pyhalov                if f in filenames:
6129f923083SAlexander Pyhalov                    del filenames[filenames.index(f)]
6139f923083SAlexander Pyhalov
6149f923083SAlexander Pyhalov            self.testgroups[dirname] = testgroup
6159f923083SAlexander Pyhalov            self.testgroups[dirname].tests = sorted(filenames)
6169f923083SAlexander Pyhalov
6179f923083SAlexander Pyhalov            testgroup.verify(self.logger)
6189f923083SAlexander Pyhalov
61978801af7SAndy Fiddaman    def filter(self, keeplist):
62078801af7SAndy Fiddaman        for group in list(self.testgroups.keys()):
62178801af7SAndy Fiddaman            if group not in keeplist:
62278801af7SAndy Fiddaman                del self.testgroups[group]
62378801af7SAndy Fiddaman                continue
62478801af7SAndy Fiddaman
62578801af7SAndy Fiddaman            g = self.testgroups[group]
62678801af7SAndy Fiddaman
62778801af7SAndy Fiddaman            if g.pre and os.path.basename(g.pre) in keeplist[group]:
62878801af7SAndy Fiddaman                continue
62978801af7SAndy Fiddaman
63078801af7SAndy Fiddaman            g.filter(keeplist[group])
63178801af7SAndy Fiddaman
63278801af7SAndy Fiddaman        for test in list(self.tests.keys()):
63378801af7SAndy Fiddaman            directory, base = os.path.split(test)
63478801af7SAndy Fiddaman            if directory not in keeplist or base not in keeplist[directory]:
63578801af7SAndy Fiddaman                del self.tests[test]
63678801af7SAndy Fiddaman
6379f923083SAlexander Pyhalov    def read(self, logger, options):
6389f923083SAlexander Pyhalov        """
6399f923083SAlexander Pyhalov        Read in the specified runfile, and apply the TestRun properties
6409f923083SAlexander Pyhalov        listed in the 'DEFAULT' section to our TestRun. Then read each
6419f923083SAlexander Pyhalov        section, and apply the appropriate properties to the Test or
6429f923083SAlexander Pyhalov        TestGroup. Properties from individual sections override those set
6439f923083SAlexander Pyhalov        in the 'DEFAULT' section. If the Test or TestGroup passes
6449f923083SAlexander Pyhalov        verification, add it to the TestRun.
6459f923083SAlexander Pyhalov        """
646b0858fdcSAlexander Pyhalov        config = configparser.RawConfigParser()
647b8052df9SRyan Moeller        parsed = config.read(options.runfiles)
648b8052df9SRyan Moeller        failed = options.runfiles - set(parsed)
649b8052df9SRyan Moeller        if len(failed):
650b8052df9SRyan Moeller            files = ' '.join(sorted(failed))
651b8052df9SRyan Moeller            fail("Couldn't read config files: %s" % files)
6529f923083SAlexander Pyhalov
6539f923083SAlexander Pyhalov        for opt in TestRun.props:
6549f923083SAlexander Pyhalov            if config.has_option('DEFAULT', opt):
6559f923083SAlexander Pyhalov                setattr(self, opt, config.get('DEFAULT', opt))
6569f923083SAlexander Pyhalov        self.outputdir = os.path.join(self.outputdir, self.timestamp)
6579f923083SAlexander Pyhalov
658b8052df9SRyan Moeller        testdir = options.testdir
659b8052df9SRyan Moeller
6609f923083SAlexander Pyhalov        for section in config.sections():
661a0955b86SJohn Levon            if ('arch' in config.options(section) and
662a0955b86SJohn Levon                platform.machine() != config.get(section, 'arch')):
663a0955b86SJohn Levon                continue
664a0955b86SJohn Levon
665b8052df9SRyan Moeller            parts = section.split(':', 1)
666b8052df9SRyan Moeller            sectiondir = parts[0]
667b8052df9SRyan Moeller            identifier = parts[1] if len(parts) == 2 else None
668b8052df9SRyan Moeller            if os.path.isdir(sectiondir):
669b8052df9SRyan Moeller                pathname = sectiondir
670b8052df9SRyan Moeller            elif os.path.isdir(os.path.join(testdir, sectiondir)):
671b8052df9SRyan Moeller                pathname = os.path.join(testdir, sectiondir)
672b8052df9SRyan Moeller            else:
673b8052df9SRyan Moeller                pathname = sectiondir
674b8052df9SRyan Moeller
675b8052df9SRyan Moeller            testgroup = TestGroup(os.path.abspath(pathname),
676b8052df9SRyan Moeller                                  identifier=identifier)
6779f923083SAlexander Pyhalov            if 'tests' in config.options(section):
6789f923083SAlexander Pyhalov                for prop in TestGroup.props:
6799f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
6809f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
681b8052df9SRyan Moeller                            if prop == 'tags':
682b8052df9SRyan Moeller                                setattr(testgroup, prop,
683b8052df9SRyan Moeller                                        eval(config.get(sect, prop)))
684b8052df9SRyan Moeller                            else:
685b8052df9SRyan Moeller                                setattr(testgroup, prop,
686b8052df9SRyan Moeller                                        config.get(sect, prop))
6879f923083SAlexander Pyhalov
6889f923083SAlexander Pyhalov                # Repopulate tests using eval to convert the string to a list
6899f923083SAlexander Pyhalov                testgroup.tests = eval(config.get(section, 'tests'))
6909f923083SAlexander Pyhalov
6919f923083SAlexander Pyhalov                if testgroup.verify(logger):
6929f923083SAlexander Pyhalov                    self.testgroups[section] = testgroup
693dfc11533SChris Williamson
694dfc11533SChris Williamson            elif 'autotests' in config.options(section):
695dfc11533SChris Williamson                for prop in TestGroup.props:
696dfc11533SChris Williamson                    for sect in ['DEFAULT', section]:
697dfc11533SChris Williamson                        if config.has_option(sect, prop):
698dfc11533SChris Williamson                            setattr(testgroup, prop, config.get(sect, prop))
699dfc11533SChris Williamson
700b8052df9SRyan Moeller                filenames = os.listdir(pathname)
701dfc11533SChris Williamson                # only files starting with "tst." are considered tests
702dfc11533SChris Williamson                filenames = [f for f in filenames if f.startswith("tst.")]
703dfc11533SChris Williamson                testgroup.tests = sorted(filenames)
704dfc11533SChris Williamson
705dfc11533SChris Williamson                if testgroup.verify(logger):
706dfc11533SChris Williamson                    self.testgroups[section] = testgroup
7079f923083SAlexander Pyhalov            else:
7089f923083SAlexander Pyhalov                test = Test(section)
7099f923083SAlexander Pyhalov                for prop in Test.props:
7109f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
7119f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
7129f923083SAlexander Pyhalov                            setattr(test, prop, config.get(sect, prop))
7139f923083SAlexander Pyhalov
7149f923083SAlexander Pyhalov                if test.verify(logger):
7159f923083SAlexander Pyhalov                    self.tests[section] = test
7169f923083SAlexander Pyhalov
7179f923083SAlexander Pyhalov    def write(self, options):
7189f923083SAlexander Pyhalov        """
7199f923083SAlexander Pyhalov        Create a configuration file for editing and later use. The
7209f923083SAlexander Pyhalov        'DEFAULT' section of the config file is created from the
7219f923083SAlexander Pyhalov        properties that were specified on the command line. Tests are
7229f923083SAlexander Pyhalov        simply added as sections that inherit everything from the
7239f923083SAlexander Pyhalov        'DEFAULT' section. TestGroups are the same, except they get an
7249f923083SAlexander Pyhalov        option including all the tests to run in that directory.
7259f923083SAlexander Pyhalov        """
7269f923083SAlexander Pyhalov
7279f923083SAlexander Pyhalov        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
7289f923083SAlexander Pyhalov                         self.defaults])
729b0858fdcSAlexander Pyhalov        config = configparser.RawConfigParser(defaults)
7309f923083SAlexander Pyhalov
7319f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
7329f923083SAlexander Pyhalov            config.add_section(test)
73378801af7SAndy Fiddaman            for prop in Test.props:
73478801af7SAndy Fiddaman                if prop not in self.props:
73578801af7SAndy Fiddaman                    config.set(testgroup, prop,
73678801af7SAndy Fiddaman                        getattr(self.testgroups[testgroup], prop))
7379f923083SAlexander Pyhalov
7389f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
7399f923083SAlexander Pyhalov            config.add_section(testgroup)
7409f923083SAlexander Pyhalov            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
74178801af7SAndy Fiddaman            for prop in TestGroup.props:
74278801af7SAndy Fiddaman                if prop not in self.props:
74378801af7SAndy Fiddaman                    config.set(testgroup, prop,
74478801af7SAndy Fiddaman                        getattr(self.testgroups[testgroup], prop))
7459f923083SAlexander Pyhalov
7469f923083SAlexander Pyhalov        try:
7479f923083SAlexander Pyhalov            with open(options.template, 'w') as f:
7489f923083SAlexander Pyhalov                return config.write(f)
7499f923083SAlexander Pyhalov        except IOError:
7509f923083SAlexander Pyhalov            fail('Could not open \'%s\' for writing.' % options.template)
7519f923083SAlexander Pyhalov
7529f923083SAlexander Pyhalov    def complete_outputdirs(self):
7539f923083SAlexander Pyhalov        """
7549f923083SAlexander Pyhalov        Collect all the pathnames for Tests, and TestGroups. Work
7559f923083SAlexander Pyhalov        backwards one pathname component at a time, to create a unique
7569f923083SAlexander Pyhalov        directory name in which to deposit test output. Tests will be able
7579f923083SAlexander Pyhalov        to write output files directly in the newly modified outputdir.
7589f923083SAlexander Pyhalov        TestGroups will be able to create one subdirectory per test in the
7599f923083SAlexander Pyhalov        outputdir, and are guaranteed uniqueness because a group can only
7609f923083SAlexander Pyhalov        contain files in one directory. Pre and post tests will create a
7619f923083SAlexander Pyhalov        directory rooted at the outputdir of the Test or TestGroup in
7629f923083SAlexander Pyhalov        question for their output.
7639f923083SAlexander Pyhalov        """
7649f923083SAlexander Pyhalov        done = False
7659f923083SAlexander Pyhalov        components = 0
766b0858fdcSAlexander Pyhalov        tmp_dict = dict(list(self.tests.items()) + list(self.testgroups.items()))
7679f923083SAlexander Pyhalov        total = len(tmp_dict)
7689f923083SAlexander Pyhalov        base = self.outputdir
7699f923083SAlexander Pyhalov
7709f923083SAlexander Pyhalov        while not done:
7719f923083SAlexander Pyhalov            l = []
7729f923083SAlexander Pyhalov            components -= 1
773b0858fdcSAlexander Pyhalov            for testfile in list(tmp_dict.keys()):
7749f923083SAlexander Pyhalov                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
7759f923083SAlexander Pyhalov                if uniq not in l:
7769f923083SAlexander Pyhalov                    l.append(uniq)
7779f923083SAlexander Pyhalov                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
7789f923083SAlexander Pyhalov                else:
7799f923083SAlexander Pyhalov                    break
7809f923083SAlexander Pyhalov            done = total == len(l)
7819f923083SAlexander Pyhalov
7829f923083SAlexander Pyhalov    def setup_logging(self, options):
7839f923083SAlexander Pyhalov        """
7849f923083SAlexander Pyhalov        Two loggers are set up here. The first is for the logfile which
7859f923083SAlexander Pyhalov        will contain one line summarizing the test, including the test
7869f923083SAlexander Pyhalov        name, result, and running time. This logger will also capture the
7879f923083SAlexander Pyhalov        timestamped combined stdout and stderr of each run. The second
7889f923083SAlexander Pyhalov        logger is optional console output, which will contain only the one
7899f923083SAlexander Pyhalov        line summary. The loggers are initialized at two different levels
7909f923083SAlexander Pyhalov        to facilitate segregating the output.
7919f923083SAlexander Pyhalov        """
7929f923083SAlexander Pyhalov        if options.dryrun is True:
7939f923083SAlexander Pyhalov            return
7949f923083SAlexander Pyhalov
7959f923083SAlexander Pyhalov        testlogger = logging.getLogger(__name__)
7969f923083SAlexander Pyhalov        testlogger.setLevel(logging.DEBUG)
7979f923083SAlexander Pyhalov
79878801af7SAndy Fiddaman        if not options.template:
7999f923083SAlexander Pyhalov            try:
8009f923083SAlexander Pyhalov                old = os.umask(0)
801b0858fdcSAlexander Pyhalov                os.makedirs(self.outputdir, mode=0o777)
8029f923083SAlexander Pyhalov                os.umask(old)
803b0858fdcSAlexander Pyhalov            except OSError as e:
8049f923083SAlexander Pyhalov                fail('%s' % e)
8059f923083SAlexander Pyhalov            filename = os.path.join(self.outputdir, 'log')
8069f923083SAlexander Pyhalov
8079f923083SAlexander Pyhalov            logfile = WatchedFileHandlerClosed(filename)
8089f923083SAlexander Pyhalov            logfile.setLevel(logging.DEBUG)
8099f923083SAlexander Pyhalov            logfilefmt = logging.Formatter('%(message)s')
8109f923083SAlexander Pyhalov            logfile.setFormatter(logfilefmt)
8119f923083SAlexander Pyhalov            testlogger.addHandler(logfile)
8129f923083SAlexander Pyhalov
8139f923083SAlexander Pyhalov        cons = logging.StreamHandler()
8149f923083SAlexander Pyhalov        cons.setLevel(logging.INFO)
8159f923083SAlexander Pyhalov        consfmt = logging.Formatter('%(message)s')
8169f923083SAlexander Pyhalov        cons.setFormatter(consfmt)
8179f923083SAlexander Pyhalov        testlogger.addHandler(cons)
8189f923083SAlexander Pyhalov
8199f923083SAlexander Pyhalov        return testlogger
8209f923083SAlexander Pyhalov
8219f923083SAlexander Pyhalov    def run(self, options):
8229f923083SAlexander Pyhalov        """
8239f923083SAlexander Pyhalov        Walk through all the Tests and TestGroups, calling run().
8249f923083SAlexander Pyhalov        """
82564ee6612SChris Fraire        if not options.dryrun:
8269f923083SAlexander Pyhalov            try:
8279f923083SAlexander Pyhalov                os.chdir(self.outputdir)
8289f923083SAlexander Pyhalov            except OSError:
8299f923083SAlexander Pyhalov                fail('Could not change to directory %s' % self.outputdir)
8309f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
8319f923083SAlexander Pyhalov            self.tests[test].run(self.logger, options)
8329f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
8339f923083SAlexander Pyhalov            self.testgroups[testgroup].run(self.logger, options)
8349f923083SAlexander Pyhalov
8359f923083SAlexander Pyhalov    def summary(self):
8361a2acdcdSAndy Fiddaman        if Result.total == 0:
83746593baaSToomas Soome            print('No tests to run')
8389f923083SAlexander Pyhalov            return
8399f923083SAlexander Pyhalov
840b0858fdcSAlexander Pyhalov        print('\nResults Summary')
841b0858fdcSAlexander Pyhalov        for key in list(Result.runresults.keys()):
8421a2acdcdSAndy Fiddaman            if Result.runresults[key] != 0:
843b0858fdcSAlexander Pyhalov                print('%s\t% 4d' % (key, Result.runresults[key]))
8449f923083SAlexander Pyhalov
8459f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
8469f923083SAlexander Pyhalov        h, m = divmod(m, 60)
847b0858fdcSAlexander Pyhalov        print('\nRunning Time:\t%02d:%02d:%02d' % (h, m, s))
848b0858fdcSAlexander Pyhalov        print('Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
849b0858fdcSAlexander Pyhalov                                            float(Result.total)) * 100))
850b0858fdcSAlexander Pyhalov        print('Log directory:\t%s' % self.outputdir)
8519f923083SAlexander Pyhalov
8529f923083SAlexander Pyhalov
8539f923083SAlexander Pyhalovdef verify_file(pathname):
8549f923083SAlexander Pyhalov    """
8559f923083SAlexander Pyhalov    Verify that the supplied pathname is an executable regular file.
8569f923083SAlexander Pyhalov    """
8579f923083SAlexander Pyhalov    if os.path.isdir(pathname) or os.path.islink(pathname):
8589f923083SAlexander Pyhalov        return False
8599f923083SAlexander Pyhalov
8609f923083SAlexander Pyhalov    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
8619f923083SAlexander Pyhalov        return True
8629f923083SAlexander Pyhalov
8639f923083SAlexander Pyhalov    return False
8649f923083SAlexander Pyhalov
8659f923083SAlexander Pyhalov
8669f923083SAlexander Pyhalovdef verify_user(user, logger):
8679f923083SAlexander Pyhalov    """
8689f923083SAlexander Pyhalov    Verify that the specified user exists on this system, and can execute
8699f923083SAlexander Pyhalov    sudo without being prompted for a password.
8709f923083SAlexander Pyhalov    """
8719f923083SAlexander Pyhalov    testcmd = [SUDO, '-n', '-u', user, TRUE]
8729f923083SAlexander Pyhalov
8739f923083SAlexander Pyhalov    if user in Cmd.verified_users:
8749f923083SAlexander Pyhalov        return True
8759f923083SAlexander Pyhalov
8769f923083SAlexander Pyhalov    try:
8779f923083SAlexander Pyhalov        _ = getpwnam(user)
8789f923083SAlexander Pyhalov    except KeyError:
8799f923083SAlexander Pyhalov        logger.info("Warning: user '%s' does not exist.", user)
8809f923083SAlexander Pyhalov        return False
8819f923083SAlexander Pyhalov
8829f923083SAlexander Pyhalov    p = Popen(testcmd)
8839f923083SAlexander Pyhalov    p.wait()
8841a2acdcdSAndy Fiddaman    if p.returncode != 0:
8859f923083SAlexander Pyhalov        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
8869f923083SAlexander Pyhalov        return False
8879f923083SAlexander Pyhalov    else:
8889f923083SAlexander Pyhalov        Cmd.verified_users.append(user)
8899f923083SAlexander Pyhalov
8909f923083SAlexander Pyhalov    return True
8919f923083SAlexander Pyhalov
8929f923083SAlexander Pyhalov
8939f923083SAlexander Pyhalovdef find_tests(testrun, options):
8949f923083SAlexander Pyhalov    """
8959f923083SAlexander Pyhalov    For the given list of pathnames, add files as Tests. For directories,
8969f923083SAlexander Pyhalov    if do_groups is True, add the directory as a TestGroup. If False,
8979f923083SAlexander Pyhalov    recursively search for executable files.
8989f923083SAlexander Pyhalov    """
8999f923083SAlexander Pyhalov
9009f923083SAlexander Pyhalov    for p in sorted(options.pathnames):
9019f923083SAlexander Pyhalov        if os.path.isdir(p):
9029f923083SAlexander Pyhalov            for dirname, _, filenames in os.walk(p):
9039f923083SAlexander Pyhalov                if options.do_groups:
9049f923083SAlexander Pyhalov                    testrun.addtestgroup(dirname, filenames, options)
9059f923083SAlexander Pyhalov                else:
9069f923083SAlexander Pyhalov                    for f in sorted(filenames):
9079f923083SAlexander Pyhalov                        testrun.addtest(os.path.join(dirname, f), options)
9089f923083SAlexander Pyhalov        else:
9099f923083SAlexander Pyhalov            testrun.addtest(p, options)
9109f923083SAlexander Pyhalov
9119f923083SAlexander Pyhalov
91278801af7SAndy Fiddamandef filter_tests(testrun, options):
91378801af7SAndy Fiddaman    try:
9144947898cSToomas Soome        fh = open(options.logfile, "r", errors='replace')
91578801af7SAndy Fiddaman    except Exception as e:
91678801af7SAndy Fiddaman        fail('%s' % e)
91778801af7SAndy Fiddaman
91878801af7SAndy Fiddaman    failed = {}
91978801af7SAndy Fiddaman    while True:
92078801af7SAndy Fiddaman            line = fh.readline()
92178801af7SAndy Fiddaman            if not line:
92278801af7SAndy Fiddaman                 break
92378801af7SAndy Fiddaman            m = re.match(r'Test: (.*)/(\S+).*\[FAIL\]', line)
92478801af7SAndy Fiddaman            if not m:
92578801af7SAndy Fiddaman                continue
92678801af7SAndy Fiddaman            group, test = m.group(1, 2)
927f4d2cf74SToomas Soome            m = re.match(re.escape(options.testdir) + r'(.*)', group)
928f4d2cf74SToomas Soome            if m:
929f4d2cf74SToomas Soome                group = m.group(1)
93078801af7SAndy Fiddaman            try:
93178801af7SAndy Fiddaman                failed[group].append(test)
93278801af7SAndy Fiddaman            except KeyError:
93378801af7SAndy Fiddaman                failed[group] = [ test ]
93478801af7SAndy Fiddaman    fh.close()
93578801af7SAndy Fiddaman
93678801af7SAndy Fiddaman    testrun.filter(failed)
93778801af7SAndy Fiddaman
93878801af7SAndy Fiddaman
9399f923083SAlexander Pyhalovdef fail(retstr, ret=1):
940b0858fdcSAlexander Pyhalov    print('%s: %s' % (argv[0], retstr))
9419f923083SAlexander Pyhalov    exit(ret)
9429f923083SAlexander Pyhalov
9439f923083SAlexander Pyhalov
9449f923083SAlexander Pyhalovdef options_cb(option, opt_str, value, parser):
945b8052df9SRyan Moeller    path_options = ['outputdir', 'template', 'testdir', 'logfile']
9469f923083SAlexander Pyhalov
9479f923083SAlexander Pyhalov    if opt_str in parser.rargs:
9489f923083SAlexander Pyhalov        fail('%s may only be specified once.' % opt_str)
9499f923083SAlexander Pyhalov
950b8052df9SRyan Moeller    if option.dest == 'runfiles':
951b8052df9SRyan Moeller        parser.values.cmd = 'rdconfig'
952b8052df9SRyan Moeller        value = set(os.path.abspath(p) for p in value.split(','))
953b8052df9SRyan Moeller    if option.dest == 'tags':
954b8052df9SRyan Moeller        value = [x.strip() for x in value.split(',')]
955b8052df9SRyan Moeller
9569f923083SAlexander Pyhalov    if option.dest in path_options:
9579f923083SAlexander Pyhalov        setattr(parser.values, option.dest, os.path.abspath(value))
95878801af7SAndy Fiddaman    else:
95978801af7SAndy Fiddaman        setattr(parser.values, option.dest, value)
9609f923083SAlexander Pyhalov
9619f923083SAlexander Pyhalov
9629f923083SAlexander Pyhalovdef parse_args():
9639f923083SAlexander Pyhalov    parser = OptionParser()
9649f923083SAlexander Pyhalov    parser.add_option('-c', action='callback', callback=options_cb,
965b8052df9SRyan Moeller                      type='string', dest='runfiles', metavar='runfiles',
966b8052df9SRyan Moeller                      help='Specify tests to run via config files.')
9679f923083SAlexander Pyhalov    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
9689f923083SAlexander Pyhalov                      help='Dry run. Print tests, but take no other action.')
96978801af7SAndy Fiddaman    parser.add_option('-l', action='callback', callback=options_cb,
97078801af7SAndy Fiddaman                      default=None, dest='logfile', metavar='logfile',
97178801af7SAndy Fiddaman                      type='string',
97278801af7SAndy Fiddaman                      help='Read logfile and re-run tests which failed.')
9739f923083SAlexander Pyhalov    parser.add_option('-g', action='store_true', default=False,
9749f923083SAlexander Pyhalov                      dest='do_groups', help='Make directories TestGroups.')
9759f923083SAlexander Pyhalov    parser.add_option('-o', action='callback', callback=options_cb,
9769f923083SAlexander Pyhalov                      default=BASEDIR, dest='outputdir', type='string',
9779f923083SAlexander Pyhalov                      metavar='outputdir', help='Specify an output directory.')
978b8052df9SRyan Moeller    parser.add_option('-i', action='callback', callback=options_cb,
979b8052df9SRyan Moeller                      default=TESTDIR, dest='testdir', type='string',
980b8052df9SRyan Moeller                      metavar='testdir', help='Specify a test directory.')
9819f923083SAlexander Pyhalov    parser.add_option('-p', action='callback', callback=options_cb,
9829f923083SAlexander Pyhalov                      default='', dest='pre', metavar='script',
9839f923083SAlexander Pyhalov                      type='string', help='Specify a pre script.')
9849f923083SAlexander Pyhalov    parser.add_option('-P', action='callback', callback=options_cb,
9859f923083SAlexander Pyhalov                      default='', dest='post', metavar='script',
9869f923083SAlexander Pyhalov                      type='string', help='Specify a post script.')
9879f923083SAlexander Pyhalov    parser.add_option('-q', action='store_true', default=False, dest='quiet',
9889f923083SAlexander Pyhalov                      help='Silence on the console during a test run.')
9899f923083SAlexander Pyhalov    parser.add_option('-t', action='callback', callback=options_cb, default=60,
9909f923083SAlexander Pyhalov                      dest='timeout', metavar='seconds', type='int',
9919f923083SAlexander Pyhalov                      help='Timeout (in seconds) for an individual test.')
9929f923083SAlexander Pyhalov    parser.add_option('-u', action='callback', callback=options_cb,
9939f923083SAlexander Pyhalov                      default='', dest='user', metavar='user', type='string',
9949f923083SAlexander Pyhalov                      help='Specify a different user name to run as.')
9959f923083SAlexander Pyhalov    parser.add_option('-w', action='callback', callback=options_cb,
9969f923083SAlexander Pyhalov                      default=None, dest='template', metavar='template',
9979f923083SAlexander Pyhalov                      type='string', help='Create a new config file.')
9989f923083SAlexander Pyhalov    parser.add_option('-x', action='callback', callback=options_cb, default='',
9999f923083SAlexander Pyhalov                      dest='pre_user', metavar='pre_user', type='string',
10009f923083SAlexander Pyhalov                      help='Specify a user to execute the pre script.')
10019f923083SAlexander Pyhalov    parser.add_option('-X', action='callback', callback=options_cb, default='',
10029f923083SAlexander Pyhalov                      dest='post_user', metavar='post_user', type='string',
10039f923083SAlexander Pyhalov                      help='Specify a user to execute the post script.')
1004b8052df9SRyan Moeller    parser.add_option('-T', action='callback', callback=options_cb, default='',
1005b8052df9SRyan Moeller                      dest='tags', metavar='tags', type='string',
1006b8052df9SRyan Moeller                      help='Specify tags to execute specific test groups.')
10079f923083SAlexander Pyhalov    (options, pathnames) = parser.parse_args()
10089f923083SAlexander Pyhalov
1009b8052df9SRyan Moeller    if options.runfiles and len(pathnames):
10109f923083SAlexander Pyhalov        fail('Extraneous arguments.')
10119f923083SAlexander Pyhalov
10129f923083SAlexander Pyhalov    options.pathnames = [os.path.abspath(path) for path in pathnames]
10139f923083SAlexander Pyhalov
10149f923083SAlexander Pyhalov    return options
10159f923083SAlexander Pyhalov
10169f923083SAlexander Pyhalov
10179f923083SAlexander Pyhalovdef main():
10189f923083SAlexander Pyhalov    options = parse_args()
101978801af7SAndy Fiddaman
10209f923083SAlexander Pyhalov    testrun = TestRun(options)
10219f923083SAlexander Pyhalov
1022b8052df9SRyan Moeller    if options.runfiles:
10239f923083SAlexander Pyhalov        testrun.read(testrun.logger, options)
102478801af7SAndy Fiddaman    else:
10259f923083SAlexander Pyhalov        find_tests(testrun, options)
102678801af7SAndy Fiddaman
102778801af7SAndy Fiddaman    if options.logfile:
102878801af7SAndy Fiddaman        filter_tests(testrun, options)
102978801af7SAndy Fiddaman
103078801af7SAndy Fiddaman    if options.template:
10319f923083SAlexander Pyhalov        testrun.write(options)
10329f923083SAlexander Pyhalov        exit(0)
10339f923083SAlexander Pyhalov
10349f923083SAlexander Pyhalov    testrun.complete_outputdirs()
10359f923083SAlexander Pyhalov    testrun.run(options)
10369f923083SAlexander Pyhalov    testrun.summary()
10372491fc01SJohn Levon    exit(retcode)
10389f923083SAlexander Pyhalov
10399f923083SAlexander Pyhalov
10409f923083SAlexander Pyhalovif __name__ == '__main__':
10419f923083SAlexander Pyhalov    main()
1042