xref: /illumos-gate/usr/src/test/test-runner/cmd/run (revision 9f9230833b50b8271840dc2c12bd1e94d9df7d12)
1*9f923083SAlexander Pyhalov#!@PYTHON@
2*9f923083SAlexander Pyhalov
3*9f923083SAlexander Pyhalov#
4*9f923083SAlexander Pyhalov# This file and its contents are supplied under the terms of the
5*9f923083SAlexander Pyhalov# Common Development and Distribution License ("CDDL"), version 1.0.
6*9f923083SAlexander Pyhalov# You may only use this file in accordance with the terms of version
7*9f923083SAlexander Pyhalov# 1.0 of the CDDL.
8*9f923083SAlexander Pyhalov#
9*9f923083SAlexander Pyhalov# A full copy of the text of the CDDL should have accompanied this
10*9f923083SAlexander Pyhalov# source.  A copy of the CDDL is also available via the Internet at
11*9f923083SAlexander Pyhalov# http://www.illumos.org/license/CDDL.
12*9f923083SAlexander Pyhalov#
13*9f923083SAlexander Pyhalov
14*9f923083SAlexander Pyhalov#
15*9f923083SAlexander Pyhalov# Copyright (c) 2012, 2015 by Delphix. All rights reserved.
16*9f923083SAlexander Pyhalov#
17*9f923083SAlexander Pyhalov
18*9f923083SAlexander Pyhalovimport ConfigParser
19*9f923083SAlexander Pyhalovimport os
20*9f923083SAlexander Pyhalovimport logging
21*9f923083SAlexander Pyhalovfrom logging.handlers import WatchedFileHandler
22*9f923083SAlexander Pyhalovfrom datetime import datetime
23*9f923083SAlexander Pyhalovfrom optparse import OptionParser
24*9f923083SAlexander Pyhalovfrom pwd import getpwnam
25*9f923083SAlexander Pyhalovfrom pwd import getpwuid
26*9f923083SAlexander Pyhalovfrom select import select
27*9f923083SAlexander Pyhalovfrom subprocess import PIPE
28*9f923083SAlexander Pyhalovfrom subprocess import Popen
29*9f923083SAlexander Pyhalovfrom sys import argv
30*9f923083SAlexander Pyhalovfrom sys import maxint
31*9f923083SAlexander Pyhalovfrom threading import Timer
32*9f923083SAlexander Pyhalovfrom time import time
33*9f923083SAlexander Pyhalov
34*9f923083SAlexander PyhalovBASEDIR = '/var/tmp/test_results'
35*9f923083SAlexander PyhalovKILL = '/usr/bin/kill'
36*9f923083SAlexander PyhalovTRUE = '/usr/bin/true'
37*9f923083SAlexander PyhalovSUDO = '/usr/bin/sudo'
38*9f923083SAlexander Pyhalov
39*9f923083SAlexander Pyhalov# Custom class to reopen the log file in case it is forcibly closed by a test.
40*9f923083SAlexander Pyhalovclass WatchedFileHandlerClosed(WatchedFileHandler):
41*9f923083SAlexander Pyhalov    """Watch files, including closed files.
42*9f923083SAlexander Pyhalov    Similar to (and inherits from) logging.handler.WatchedFileHandler,
43*9f923083SAlexander Pyhalov    except that IOErrors are handled by reopening the stream and retrying.
44*9f923083SAlexander Pyhalov    This will be retried up to a configurable number of times before
45*9f923083SAlexander Pyhalov    giving up, default 5.
46*9f923083SAlexander Pyhalov    """
47*9f923083SAlexander Pyhalov
48*9f923083SAlexander Pyhalov    def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
49*9f923083SAlexander Pyhalov        self.max_tries = max_tries
50*9f923083SAlexander Pyhalov        self.tries = 0
51*9f923083SAlexander Pyhalov        WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
52*9f923083SAlexander Pyhalov
53*9f923083SAlexander Pyhalov    def emit(self, record):
54*9f923083SAlexander Pyhalov        while True:
55*9f923083SAlexander Pyhalov            try:
56*9f923083SAlexander Pyhalov                WatchedFileHandler.emit(self, record)
57*9f923083SAlexander Pyhalov                self.tries = 0
58*9f923083SAlexander Pyhalov                return
59*9f923083SAlexander Pyhalov            except IOError as err:
60*9f923083SAlexander Pyhalov                if self.tries == self.max_tries:
61*9f923083SAlexander Pyhalov                    raise
62*9f923083SAlexander Pyhalov                self.stream.close()
63*9f923083SAlexander Pyhalov                self.stream = self._open()
64*9f923083SAlexander Pyhalov                self.tries += 1
65*9f923083SAlexander Pyhalov
66*9f923083SAlexander Pyhalovclass Result(object):
67*9f923083SAlexander Pyhalov    total = 0
68*9f923083SAlexander Pyhalov    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
69*9f923083SAlexander Pyhalov
70*9f923083SAlexander Pyhalov    def __init__(self):
71*9f923083SAlexander Pyhalov        self.starttime = None
72*9f923083SAlexander Pyhalov        self.returncode = None
73*9f923083SAlexander Pyhalov        self.runtime = ''
74*9f923083SAlexander Pyhalov        self.stdout = []
75*9f923083SAlexander Pyhalov        self.stderr = []
76*9f923083SAlexander Pyhalov        self.result = ''
77*9f923083SAlexander Pyhalov
78*9f923083SAlexander Pyhalov    def done(self, proc, killed):
79*9f923083SAlexander Pyhalov        """
80*9f923083SAlexander Pyhalov        Finalize the results of this Cmd.
81*9f923083SAlexander Pyhalov        """
82*9f923083SAlexander Pyhalov        Result.total += 1
83*9f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
84*9f923083SAlexander Pyhalov        self.runtime = '%02d:%02d' % (m, s)
85*9f923083SAlexander Pyhalov        self.returncode = proc.returncode
86*9f923083SAlexander Pyhalov        if killed:
87*9f923083SAlexander Pyhalov            self.result = 'KILLED'
88*9f923083SAlexander Pyhalov            Result.runresults['KILLED'] += 1
89*9f923083SAlexander Pyhalov        elif self.returncode is 0:
90*9f923083SAlexander Pyhalov            self.result = 'PASS'
91*9f923083SAlexander Pyhalov            Result.runresults['PASS'] += 1
92*9f923083SAlexander Pyhalov        elif self.returncode is not 0:
93*9f923083SAlexander Pyhalov            self.result = 'FAIL'
94*9f923083SAlexander Pyhalov            Result.runresults['FAIL'] += 1
95*9f923083SAlexander Pyhalov
96*9f923083SAlexander Pyhalov
97*9f923083SAlexander Pyhalovclass Output(object):
98*9f923083SAlexander Pyhalov    """
99*9f923083SAlexander Pyhalov    This class is a slightly modified version of the 'Stream' class found
100*9f923083SAlexander Pyhalov    here: http://goo.gl/aSGfv
101*9f923083SAlexander Pyhalov    """
102*9f923083SAlexander Pyhalov    def __init__(self, stream):
103*9f923083SAlexander Pyhalov        self.stream = stream
104*9f923083SAlexander Pyhalov        self._buf = ''
105*9f923083SAlexander Pyhalov        self.lines = []
106*9f923083SAlexander Pyhalov
107*9f923083SAlexander Pyhalov    def fileno(self):
108*9f923083SAlexander Pyhalov        return self.stream.fileno()
109*9f923083SAlexander Pyhalov
110*9f923083SAlexander Pyhalov    def read(self, drain=0):
111*9f923083SAlexander Pyhalov        """
112*9f923083SAlexander Pyhalov        Read from the file descriptor. If 'drain' set, read until EOF.
113*9f923083SAlexander Pyhalov        """
114*9f923083SAlexander Pyhalov        while self._read() is not None:
115*9f923083SAlexander Pyhalov            if not drain:
116*9f923083SAlexander Pyhalov                break
117*9f923083SAlexander Pyhalov
118*9f923083SAlexander Pyhalov    def _read(self):
119*9f923083SAlexander Pyhalov        """
120*9f923083SAlexander Pyhalov        Read up to 4k of data from this output stream. Collect the output
121*9f923083SAlexander Pyhalov        up to the last newline, and append it to any leftover data from a
122*9f923083SAlexander Pyhalov        previous call. The lines are stored as a (timestamp, data) tuple
123*9f923083SAlexander Pyhalov        for easy sorting/merging later.
124*9f923083SAlexander Pyhalov        """
125*9f923083SAlexander Pyhalov        fd = self.fileno()
126*9f923083SAlexander Pyhalov        buf = os.read(fd, 4096)
127*9f923083SAlexander Pyhalov        if not buf:
128*9f923083SAlexander Pyhalov            return None
129*9f923083SAlexander Pyhalov        if '\n' not in buf:
130*9f923083SAlexander Pyhalov            self._buf += buf
131*9f923083SAlexander Pyhalov            return []
132*9f923083SAlexander Pyhalov
133*9f923083SAlexander Pyhalov        buf = self._buf + buf
134*9f923083SAlexander Pyhalov        tmp, rest = buf.rsplit('\n', 1)
135*9f923083SAlexander Pyhalov        self._buf = rest
136*9f923083SAlexander Pyhalov        now = datetime.now()
137*9f923083SAlexander Pyhalov        rows = tmp.split('\n')
138*9f923083SAlexander Pyhalov        self.lines += [(now, r) for r in rows]
139*9f923083SAlexander Pyhalov
140*9f923083SAlexander Pyhalov
141*9f923083SAlexander Pyhalovclass Cmd(object):
142*9f923083SAlexander Pyhalov    verified_users = []
143*9f923083SAlexander Pyhalov
144*9f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None):
145*9f923083SAlexander Pyhalov        self.pathname = pathname
146*9f923083SAlexander Pyhalov        self.outputdir = outputdir or 'BASEDIR'
147*9f923083SAlexander Pyhalov        self.timeout = timeout
148*9f923083SAlexander Pyhalov        self.user = user or ''
149*9f923083SAlexander Pyhalov        self.killed = False
150*9f923083SAlexander Pyhalov        self.result = Result()
151*9f923083SAlexander Pyhalov
152*9f923083SAlexander Pyhalov        if self.timeout is None:
153*9f923083SAlexander Pyhalov            self.timeout = 60
154*9f923083SAlexander Pyhalov
155*9f923083SAlexander Pyhalov    def __str__(self):
156*9f923083SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %d\nUser: %s\n" % \
157*9f923083SAlexander Pyhalov            (self.pathname, self.outputdir, self.timeout, self.user)
158*9f923083SAlexander Pyhalov
159*9f923083SAlexander Pyhalov    def kill_cmd(self, proc):
160*9f923083SAlexander Pyhalov        """
161*9f923083SAlexander Pyhalov        Kill a running command due to timeout, or ^C from the keyboard. If
162*9f923083SAlexander Pyhalov        sudo is required, this user was verified previously.
163*9f923083SAlexander Pyhalov        """
164*9f923083SAlexander Pyhalov        self.killed = True
165*9f923083SAlexander Pyhalov        do_sudo = len(self.user) != 0
166*9f923083SAlexander Pyhalov        signal = '-TERM'
167*9f923083SAlexander Pyhalov
168*9f923083SAlexander Pyhalov        cmd = [SUDO, KILL, signal, str(proc.pid)]
169*9f923083SAlexander Pyhalov        if not do_sudo:
170*9f923083SAlexander Pyhalov            del cmd[0]
171*9f923083SAlexander Pyhalov
172*9f923083SAlexander Pyhalov        try:
173*9f923083SAlexander Pyhalov            kp = Popen(cmd)
174*9f923083SAlexander Pyhalov            kp.wait()
175*9f923083SAlexander Pyhalov        except:
176*9f923083SAlexander Pyhalov            pass
177*9f923083SAlexander Pyhalov
178*9f923083SAlexander Pyhalov    def update_cmd_privs(self, cmd, user):
179*9f923083SAlexander Pyhalov        """
180*9f923083SAlexander Pyhalov        If a user has been specified to run this Cmd and we're not already
181*9f923083SAlexander Pyhalov        running as that user, prepend the appropriate sudo command to run
182*9f923083SAlexander Pyhalov        as that user.
183*9f923083SAlexander Pyhalov        """
184*9f923083SAlexander Pyhalov        me = getpwuid(os.getuid())
185*9f923083SAlexander Pyhalov
186*9f923083SAlexander Pyhalov        if not user or user is me:
187*9f923083SAlexander Pyhalov            return cmd
188*9f923083SAlexander Pyhalov
189*9f923083SAlexander Pyhalov        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
190*9f923083SAlexander Pyhalov        return ret.split(' ')
191*9f923083SAlexander Pyhalov
192*9f923083SAlexander Pyhalov    def collect_output(self, proc):
193*9f923083SAlexander Pyhalov        """
194*9f923083SAlexander Pyhalov        Read from stdout/stderr as data becomes available, until the
195*9f923083SAlexander Pyhalov        process is no longer running. Return the lines from the stdout and
196*9f923083SAlexander Pyhalov        stderr Output objects.
197*9f923083SAlexander Pyhalov        """
198*9f923083SAlexander Pyhalov        out = Output(proc.stdout)
199*9f923083SAlexander Pyhalov        err = Output(proc.stderr)
200*9f923083SAlexander Pyhalov        res = []
201*9f923083SAlexander Pyhalov        while proc.returncode is None:
202*9f923083SAlexander Pyhalov            proc.poll()
203*9f923083SAlexander Pyhalov            res = select([out, err], [], [], .1)
204*9f923083SAlexander Pyhalov            for fd in res[0]:
205*9f923083SAlexander Pyhalov                fd.read()
206*9f923083SAlexander Pyhalov        for fd in res[0]:
207*9f923083SAlexander Pyhalov            fd.read(drain=1)
208*9f923083SAlexander Pyhalov
209*9f923083SAlexander Pyhalov        return out.lines, err.lines
210*9f923083SAlexander Pyhalov
211*9f923083SAlexander Pyhalov    def run(self, options):
212*9f923083SAlexander Pyhalov        """
213*9f923083SAlexander Pyhalov        This is the main function that runs each individual test.
214*9f923083SAlexander Pyhalov        Determine whether or not the command requires sudo, and modify it
215*9f923083SAlexander Pyhalov        if needed. Run the command, and update the result object.
216*9f923083SAlexander Pyhalov        """
217*9f923083SAlexander Pyhalov        if options.dryrun is True:
218*9f923083SAlexander Pyhalov            print self
219*9f923083SAlexander Pyhalov            return
220*9f923083SAlexander Pyhalov
221*9f923083SAlexander Pyhalov        privcmd = self.update_cmd_privs(self.pathname, self.user)
222*9f923083SAlexander Pyhalov        try:
223*9f923083SAlexander Pyhalov            old = os.umask(0)
224*9f923083SAlexander Pyhalov            if not os.path.isdir(self.outputdir):
225*9f923083SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0777)
226*9f923083SAlexander Pyhalov            os.umask(old)
227*9f923083SAlexander Pyhalov        except OSError, e:
228*9f923083SAlexander Pyhalov            fail('%s' % e)
229*9f923083SAlexander Pyhalov
230*9f923083SAlexander Pyhalov        try:
231*9f923083SAlexander Pyhalov            self.result.starttime = time()
232*9f923083SAlexander Pyhalov            proc = Popen(privcmd, stdout=PIPE, stderr=PIPE)
233*9f923083SAlexander Pyhalov
234*9f923083SAlexander Pyhalov            # Allow a special timeout value of 0 to mean infinity
235*9f923083SAlexander Pyhalov            if int(self.timeout) == 0:
236*9f923083SAlexander Pyhalov                self.timeout = maxint
237*9f923083SAlexander Pyhalov            t = Timer(int(self.timeout), self.kill_cmd, [proc])
238*9f923083SAlexander Pyhalov            t.start()
239*9f923083SAlexander Pyhalov            self.result.stdout, self.result.stderr = self.collect_output(proc)
240*9f923083SAlexander Pyhalov        except KeyboardInterrupt:
241*9f923083SAlexander Pyhalov            self.kill_cmd(proc)
242*9f923083SAlexander Pyhalov            fail('\nRun terminated at user request.')
243*9f923083SAlexander Pyhalov        finally:
244*9f923083SAlexander Pyhalov            t.cancel()
245*9f923083SAlexander Pyhalov
246*9f923083SAlexander Pyhalov        self.result.done(proc, self.killed)
247*9f923083SAlexander Pyhalov
248*9f923083SAlexander Pyhalov    def skip(self):
249*9f923083SAlexander Pyhalov        """
250*9f923083SAlexander Pyhalov        Initialize enough of the test result that we can log a skipped
251*9f923083SAlexander Pyhalov        command.
252*9f923083SAlexander Pyhalov        """
253*9f923083SAlexander Pyhalov        Result.total += 1
254*9f923083SAlexander Pyhalov        Result.runresults['SKIP'] += 1
255*9f923083SAlexander Pyhalov        self.result.stdout = self.result.stderr = []
256*9f923083SAlexander Pyhalov        self.result.starttime = time()
257*9f923083SAlexander Pyhalov        m, s = divmod(time() - self.result.starttime, 60)
258*9f923083SAlexander Pyhalov        self.result.runtime = '%02d:%02d' % (m, s)
259*9f923083SAlexander Pyhalov        self.result.result = 'SKIP'
260*9f923083SAlexander Pyhalov
261*9f923083SAlexander Pyhalov    def log(self, logger, options):
262*9f923083SAlexander Pyhalov        """
263*9f923083SAlexander Pyhalov        This function is responsible for writing all output. This includes
264*9f923083SAlexander Pyhalov        the console output, the logfile of all results (with timestamped
265*9f923083SAlexander Pyhalov        merged stdout and stderr), and for each test, the unmodified
266*9f923083SAlexander Pyhalov        stdout/stderr/merged in it's own file.
267*9f923083SAlexander Pyhalov        """
268*9f923083SAlexander Pyhalov        if logger is None:
269*9f923083SAlexander Pyhalov            return
270*9f923083SAlexander Pyhalov
271*9f923083SAlexander Pyhalov        logname = getpwuid(os.getuid()).pw_name
272*9f923083SAlexander Pyhalov        user = ' (run as %s)' % (self.user if len(self.user) else logname)
273*9f923083SAlexander Pyhalov        msga = 'Test: %s%s ' % (self.pathname, user)
274*9f923083SAlexander Pyhalov        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
275*9f923083SAlexander Pyhalov        pad = ' ' * (80 - (len(msga) + len(msgb)))
276*9f923083SAlexander Pyhalov
277*9f923083SAlexander Pyhalov        # If -q is specified, only print a line for tests that didn't pass.
278*9f923083SAlexander Pyhalov        # This means passing tests need to be logged as DEBUG, or the one
279*9f923083SAlexander Pyhalov        # line summary will only be printed in the logfile for failures.
280*9f923083SAlexander Pyhalov        if not options.quiet:
281*9f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
282*9f923083SAlexander Pyhalov        elif self.result.result is not 'PASS':
283*9f923083SAlexander Pyhalov            logger.info('%s%s%s' % (msga, pad, msgb))
284*9f923083SAlexander Pyhalov        else:
285*9f923083SAlexander Pyhalov            logger.debug('%s%s%s' % (msga, pad, msgb))
286*9f923083SAlexander Pyhalov
287*9f923083SAlexander Pyhalov        lines = sorted(self.result.stdout + self.result.stderr,
288*9f923083SAlexander Pyhalov                       cmp=lambda x, y: cmp(x[0], y[0]))
289*9f923083SAlexander Pyhalov
290*9f923083SAlexander Pyhalov        for dt, line in lines:
291*9f923083SAlexander Pyhalov            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
292*9f923083SAlexander Pyhalov
293*9f923083SAlexander Pyhalov        if len(self.result.stdout):
294*9f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
295*9f923083SAlexander Pyhalov                for _, line in self.result.stdout:
296*9f923083SAlexander Pyhalov                    os.write(out.fileno(), '%s\n' % line)
297*9f923083SAlexander Pyhalov        if len(self.result.stderr):
298*9f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
299*9f923083SAlexander Pyhalov                for _, line in self.result.stderr:
300*9f923083SAlexander Pyhalov                    os.write(err.fileno(), '%s\n' % line)
301*9f923083SAlexander Pyhalov        if len(self.result.stdout) and len(self.result.stderr):
302*9f923083SAlexander Pyhalov            with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
303*9f923083SAlexander Pyhalov                for _, line in lines:
304*9f923083SAlexander Pyhalov                    os.write(merged.fileno(), '%s\n' % line)
305*9f923083SAlexander Pyhalov
306*9f923083SAlexander Pyhalov
307*9f923083SAlexander Pyhalovclass Test(Cmd):
308*9f923083SAlexander Pyhalov    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
309*9f923083SAlexander Pyhalov             'post_user']
310*9f923083SAlexander Pyhalov
311*9f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
312*9f923083SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None):
313*9f923083SAlexander Pyhalov        super(Test, self).__init__(pathname, outputdir, timeout, user)
314*9f923083SAlexander Pyhalov        self.pre = pre or ''
315*9f923083SAlexander Pyhalov        self.pre_user = pre_user or ''
316*9f923083SAlexander Pyhalov        self.post = post or ''
317*9f923083SAlexander Pyhalov        self.post_user = post_user or ''
318*9f923083SAlexander Pyhalov
319*9f923083SAlexander Pyhalov    def __str__(self):
320*9f923083SAlexander Pyhalov        post_user = pre_user = ''
321*9f923083SAlexander Pyhalov        if len(self.pre_user):
322*9f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
323*9f923083SAlexander Pyhalov        if len(self.post_user):
324*9f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
325*9f923083SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTimeout: %d\nPre: %s%s\nPost: " \
326*9f923083SAlexander Pyhalov               "%s%s\nUser: %s\n" % \
327*9f923083SAlexander Pyhalov               (self.pathname, self.outputdir, self.timeout, self.pre,
328*9f923083SAlexander Pyhalov                pre_user, self.post, post_user, self.user)
329*9f923083SAlexander Pyhalov
330*9f923083SAlexander Pyhalov    def verify(self, logger):
331*9f923083SAlexander Pyhalov        """
332*9f923083SAlexander Pyhalov        Check the pre/post scripts, user and Test. Omit the Test from this
333*9f923083SAlexander Pyhalov        run if there are any problems.
334*9f923083SAlexander Pyhalov        """
335*9f923083SAlexander Pyhalov        files = [self.pre, self.pathname, self.post]
336*9f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
337*9f923083SAlexander Pyhalov
338*9f923083SAlexander Pyhalov        for f in [f for f in files if len(f)]:
339*9f923083SAlexander Pyhalov            if not verify_file(f):
340*9f923083SAlexander Pyhalov                logger.info("Warning: Test '%s' not added to this run because"
341*9f923083SAlexander Pyhalov                            " it failed verification." % f)
342*9f923083SAlexander Pyhalov                return False
343*9f923083SAlexander Pyhalov
344*9f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
345*9f923083SAlexander Pyhalov            if not verify_user(user, logger):
346*9f923083SAlexander Pyhalov                logger.info("Not adding Test '%s' to this run." %
347*9f923083SAlexander Pyhalov                            self.pathname)
348*9f923083SAlexander Pyhalov                return False
349*9f923083SAlexander Pyhalov
350*9f923083SAlexander Pyhalov        return True
351*9f923083SAlexander Pyhalov
352*9f923083SAlexander Pyhalov    def run(self, logger, options):
353*9f923083SAlexander Pyhalov        """
354*9f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
355*9f923083SAlexander Pyhalov        doesn't pass, skip this Test. Run the post script regardless.
356*9f923083SAlexander Pyhalov        """
357*9f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
358*9f923083SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
359*9f923083SAlexander Pyhalov                      user=self.pre_user)
360*9f923083SAlexander Pyhalov        test = Cmd(self.pathname, outputdir=self.outputdir,
361*9f923083SAlexander Pyhalov                   timeout=self.timeout, user=self.user)
362*9f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
363*9f923083SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
364*9f923083SAlexander Pyhalov                       user=self.post_user)
365*9f923083SAlexander Pyhalov
366*9f923083SAlexander Pyhalov        cont = True
367*9f923083SAlexander Pyhalov        if len(pretest.pathname):
368*9f923083SAlexander Pyhalov            pretest.run(options)
369*9f923083SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
370*9f923083SAlexander Pyhalov            pretest.log(logger, options)
371*9f923083SAlexander Pyhalov
372*9f923083SAlexander Pyhalov        if cont:
373*9f923083SAlexander Pyhalov            test.run(options)
374*9f923083SAlexander Pyhalov        else:
375*9f923083SAlexander Pyhalov            test.skip()
376*9f923083SAlexander Pyhalov
377*9f923083SAlexander Pyhalov        test.log(logger, options)
378*9f923083SAlexander Pyhalov
379*9f923083SAlexander Pyhalov        if len(posttest.pathname):
380*9f923083SAlexander Pyhalov            posttest.run(options)
381*9f923083SAlexander Pyhalov            posttest.log(logger, options)
382*9f923083SAlexander Pyhalov
383*9f923083SAlexander Pyhalov
384*9f923083SAlexander Pyhalovclass TestGroup(Test):
385*9f923083SAlexander Pyhalov    props = Test.props + ['tests']
386*9f923083SAlexander Pyhalov
387*9f923083SAlexander Pyhalov    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
388*9f923083SAlexander Pyhalov                 pre=None, pre_user=None, post=None, post_user=None,
389*9f923083SAlexander Pyhalov                 tests=None):
390*9f923083SAlexander Pyhalov        super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
391*9f923083SAlexander Pyhalov                                        pre, pre_user, post, post_user)
392*9f923083SAlexander Pyhalov        self.tests = tests or []
393*9f923083SAlexander Pyhalov
394*9f923083SAlexander Pyhalov    def __str__(self):
395*9f923083SAlexander Pyhalov        post_user = pre_user = ''
396*9f923083SAlexander Pyhalov        if len(self.pre_user):
397*9f923083SAlexander Pyhalov            pre_user = ' (as %s)' % (self.pre_user)
398*9f923083SAlexander Pyhalov        if len(self.post_user):
399*9f923083SAlexander Pyhalov            post_user = ' (as %s)' % (self.post_user)
400*9f923083SAlexander Pyhalov        return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %d\n" \
401*9f923083SAlexander Pyhalov               "Pre: %s%s\nPost: %s%s\nUser: %s\n" % \
402*9f923083SAlexander Pyhalov               (self.pathname, self.outputdir, self.tests, self.timeout,
403*9f923083SAlexander Pyhalov                self.pre, pre_user, self.post, post_user, self.user)
404*9f923083SAlexander Pyhalov
405*9f923083SAlexander Pyhalov    def verify(self, logger):
406*9f923083SAlexander Pyhalov        """
407*9f923083SAlexander Pyhalov        Check the pre/post scripts, user and tests in this TestGroup. Omit
408*9f923083SAlexander Pyhalov        the TestGroup entirely, or simply delete the relevant tests in the
409*9f923083SAlexander Pyhalov        group, if that's all that's required.
410*9f923083SAlexander Pyhalov        """
411*9f923083SAlexander Pyhalov        # If the pre or post scripts are relative pathnames, convert to
412*9f923083SAlexander Pyhalov        # absolute, so they stand a chance of passing verification.
413*9f923083SAlexander Pyhalov        if len(self.pre) and not os.path.isabs(self.pre):
414*9f923083SAlexander Pyhalov            self.pre = os.path.join(self.pathname, self.pre)
415*9f923083SAlexander Pyhalov        if len(self.post) and not os.path.isabs(self.post):
416*9f923083SAlexander Pyhalov            self.post = os.path.join(self.pathname, self.post)
417*9f923083SAlexander Pyhalov
418*9f923083SAlexander Pyhalov        auxfiles = [self.pre, self.post]
419*9f923083SAlexander Pyhalov        users = [self.pre_user, self.user, self.post_user]
420*9f923083SAlexander Pyhalov
421*9f923083SAlexander Pyhalov        for f in [f for f in auxfiles if len(f)]:
422*9f923083SAlexander Pyhalov            if self.pathname != os.path.dirname(f):
423*9f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
424*9f923083SAlexander Pyhalov                            "Auxiliary script '%s' exists in a different "
425*9f923083SAlexander Pyhalov                            "directory." % (self.pathname, f))
426*9f923083SAlexander Pyhalov                return False
427*9f923083SAlexander Pyhalov
428*9f923083SAlexander Pyhalov            if not verify_file(f):
429*9f923083SAlexander Pyhalov                logger.info("Warning: TestGroup '%s' not added to this run. "
430*9f923083SAlexander Pyhalov                            "Auxiliary script '%s' failed verification." %
431*9f923083SAlexander Pyhalov                            (self.pathname, f))
432*9f923083SAlexander Pyhalov                return False
433*9f923083SAlexander Pyhalov
434*9f923083SAlexander Pyhalov        for user in [user for user in users if len(user)]:
435*9f923083SAlexander Pyhalov            if not verify_user(user, logger):
436*9f923083SAlexander Pyhalov                logger.info("Not adding TestGroup '%s' to this run." %
437*9f923083SAlexander Pyhalov                            self.pathname)
438*9f923083SAlexander Pyhalov                return False
439*9f923083SAlexander Pyhalov
440*9f923083SAlexander Pyhalov        # If one of the tests is invalid, delete it, log it, and drive on.
441*9f923083SAlexander Pyhalov        for test in self.tests:
442*9f923083SAlexander Pyhalov            if not verify_file(os.path.join(self.pathname, test)):
443*9f923083SAlexander Pyhalov                del self.tests[self.tests.index(test)]
444*9f923083SAlexander Pyhalov                logger.info("Warning: Test '%s' removed from TestGroup '%s' "
445*9f923083SAlexander Pyhalov                            "because it failed verification." %
446*9f923083SAlexander Pyhalov                            (test, self.pathname))
447*9f923083SAlexander Pyhalov
448*9f923083SAlexander Pyhalov        return len(self.tests) is not 0
449*9f923083SAlexander Pyhalov
450*9f923083SAlexander Pyhalov    def run(self, logger, options):
451*9f923083SAlexander Pyhalov        """
452*9f923083SAlexander Pyhalov        Create Cmd instances for the pre/post scripts. If the pre script
453*9f923083SAlexander Pyhalov        doesn't pass, skip all the tests in this TestGroup. Run the post
454*9f923083SAlexander Pyhalov        script regardless.
455*9f923083SAlexander Pyhalov        """
456*9f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
457*9f923083SAlexander Pyhalov        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
458*9f923083SAlexander Pyhalov                      user=self.pre_user)
459*9f923083SAlexander Pyhalov        odir = os.path.join(self.outputdir, os.path.basename(self.post))
460*9f923083SAlexander Pyhalov        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
461*9f923083SAlexander Pyhalov                       user=self.post_user)
462*9f923083SAlexander Pyhalov
463*9f923083SAlexander Pyhalov        cont = True
464*9f923083SAlexander Pyhalov        if len(pretest.pathname):
465*9f923083SAlexander Pyhalov            pretest.run(options)
466*9f923083SAlexander Pyhalov            cont = pretest.result.result is 'PASS'
467*9f923083SAlexander Pyhalov            pretest.log(logger, options)
468*9f923083SAlexander Pyhalov
469*9f923083SAlexander Pyhalov        for fname in self.tests:
470*9f923083SAlexander Pyhalov            test = Cmd(os.path.join(self.pathname, fname),
471*9f923083SAlexander Pyhalov                       outputdir=os.path.join(self.outputdir, fname),
472*9f923083SAlexander Pyhalov                       timeout=self.timeout, user=self.user)
473*9f923083SAlexander Pyhalov            if cont:
474*9f923083SAlexander Pyhalov                test.run(options)
475*9f923083SAlexander Pyhalov            else:
476*9f923083SAlexander Pyhalov                test.skip()
477*9f923083SAlexander Pyhalov
478*9f923083SAlexander Pyhalov            test.log(logger, options)
479*9f923083SAlexander Pyhalov
480*9f923083SAlexander Pyhalov        if len(posttest.pathname):
481*9f923083SAlexander Pyhalov            posttest.run(options)
482*9f923083SAlexander Pyhalov            posttest.log(logger, options)
483*9f923083SAlexander Pyhalov
484*9f923083SAlexander Pyhalov
485*9f923083SAlexander Pyhalovclass TestRun(object):
486*9f923083SAlexander Pyhalov    props = ['quiet', 'outputdir']
487*9f923083SAlexander Pyhalov
488*9f923083SAlexander Pyhalov    def __init__(self, options):
489*9f923083SAlexander Pyhalov        self.tests = {}
490*9f923083SAlexander Pyhalov        self.testgroups = {}
491*9f923083SAlexander Pyhalov        self.starttime = time()
492*9f923083SAlexander Pyhalov        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
493*9f923083SAlexander Pyhalov        self.outputdir = os.path.join(options.outputdir, self.timestamp)
494*9f923083SAlexander Pyhalov        self.logger = self.setup_logging(options)
495*9f923083SAlexander Pyhalov        self.defaults = [
496*9f923083SAlexander Pyhalov            ('outputdir', BASEDIR),
497*9f923083SAlexander Pyhalov            ('quiet', False),
498*9f923083SAlexander Pyhalov            ('timeout', 60),
499*9f923083SAlexander Pyhalov            ('user', ''),
500*9f923083SAlexander Pyhalov            ('pre', ''),
501*9f923083SAlexander Pyhalov            ('pre_user', ''),
502*9f923083SAlexander Pyhalov            ('post', ''),
503*9f923083SAlexander Pyhalov            ('post_user', '')
504*9f923083SAlexander Pyhalov        ]
505*9f923083SAlexander Pyhalov
506*9f923083SAlexander Pyhalov    def __str__(self):
507*9f923083SAlexander Pyhalov        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
508*9f923083SAlexander Pyhalov        s += 'TESTS:\n'
509*9f923083SAlexander Pyhalov        for key in sorted(self.tests.keys()):
510*9f923083SAlexander Pyhalov            s += '%s%s' % (self.tests[key].__str__(), '\n')
511*9f923083SAlexander Pyhalov        s += 'TESTGROUPS:\n'
512*9f923083SAlexander Pyhalov        for key in sorted(self.testgroups.keys()):
513*9f923083SAlexander Pyhalov            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
514*9f923083SAlexander Pyhalov        return s
515*9f923083SAlexander Pyhalov
516*9f923083SAlexander Pyhalov    def addtest(self, pathname, options):
517*9f923083SAlexander Pyhalov        """
518*9f923083SAlexander Pyhalov        Create a new Test, and apply any properties that were passed in
519*9f923083SAlexander Pyhalov        from the command line. If it passes verification, add it to the
520*9f923083SAlexander Pyhalov        TestRun.
521*9f923083SAlexander Pyhalov        """
522*9f923083SAlexander Pyhalov        test = Test(pathname)
523*9f923083SAlexander Pyhalov        for prop in Test.props:
524*9f923083SAlexander Pyhalov            setattr(test, prop, getattr(options, prop))
525*9f923083SAlexander Pyhalov
526*9f923083SAlexander Pyhalov        if test.verify(self.logger):
527*9f923083SAlexander Pyhalov            self.tests[pathname] = test
528*9f923083SAlexander Pyhalov
529*9f923083SAlexander Pyhalov    def addtestgroup(self, dirname, filenames, options):
530*9f923083SAlexander Pyhalov        """
531*9f923083SAlexander Pyhalov        Create a new TestGroup, and apply any properties that were passed
532*9f923083SAlexander Pyhalov        in from the command line. If it passes verification, add it to the
533*9f923083SAlexander Pyhalov        TestRun.
534*9f923083SAlexander Pyhalov        """
535*9f923083SAlexander Pyhalov        if dirname not in self.testgroups:
536*9f923083SAlexander Pyhalov            testgroup = TestGroup(dirname)
537*9f923083SAlexander Pyhalov            for prop in Test.props:
538*9f923083SAlexander Pyhalov                setattr(testgroup, prop, getattr(options, prop))
539*9f923083SAlexander Pyhalov
540*9f923083SAlexander Pyhalov            # Prevent pre/post scripts from running as regular tests
541*9f923083SAlexander Pyhalov            for f in [testgroup.pre, testgroup.post]:
542*9f923083SAlexander Pyhalov                if f in filenames:
543*9f923083SAlexander Pyhalov                    del filenames[filenames.index(f)]
544*9f923083SAlexander Pyhalov
545*9f923083SAlexander Pyhalov            self.testgroups[dirname] = testgroup
546*9f923083SAlexander Pyhalov            self.testgroups[dirname].tests = sorted(filenames)
547*9f923083SAlexander Pyhalov
548*9f923083SAlexander Pyhalov            testgroup.verify(self.logger)
549*9f923083SAlexander Pyhalov
550*9f923083SAlexander Pyhalov    def read(self, logger, options):
551*9f923083SAlexander Pyhalov        """
552*9f923083SAlexander Pyhalov        Read in the specified runfile, and apply the TestRun properties
553*9f923083SAlexander Pyhalov        listed in the 'DEFAULT' section to our TestRun. Then read each
554*9f923083SAlexander Pyhalov        section, and apply the appropriate properties to the Test or
555*9f923083SAlexander Pyhalov        TestGroup. Properties from individual sections override those set
556*9f923083SAlexander Pyhalov        in the 'DEFAULT' section. If the Test or TestGroup passes
557*9f923083SAlexander Pyhalov        verification, add it to the TestRun.
558*9f923083SAlexander Pyhalov        """
559*9f923083SAlexander Pyhalov        config = ConfigParser.RawConfigParser()
560*9f923083SAlexander Pyhalov        if not len(config.read(options.runfile)):
561*9f923083SAlexander Pyhalov            fail("Coulnd't read config file %s" % options.runfile)
562*9f923083SAlexander Pyhalov
563*9f923083SAlexander Pyhalov        for opt in TestRun.props:
564*9f923083SAlexander Pyhalov            if config.has_option('DEFAULT', opt):
565*9f923083SAlexander Pyhalov                setattr(self, opt, config.get('DEFAULT', opt))
566*9f923083SAlexander Pyhalov        self.outputdir = os.path.join(self.outputdir, self.timestamp)
567*9f923083SAlexander Pyhalov
568*9f923083SAlexander Pyhalov        for section in config.sections():
569*9f923083SAlexander Pyhalov            if 'tests' in config.options(section):
570*9f923083SAlexander Pyhalov                testgroup = TestGroup(section)
571*9f923083SAlexander Pyhalov                for prop in TestGroup.props:
572*9f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
573*9f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
574*9f923083SAlexander Pyhalov                            setattr(testgroup, prop, config.get(sect, prop))
575*9f923083SAlexander Pyhalov
576*9f923083SAlexander Pyhalov                # Repopulate tests using eval to convert the string to a list
577*9f923083SAlexander Pyhalov                testgroup.tests = eval(config.get(section, 'tests'))
578*9f923083SAlexander Pyhalov
579*9f923083SAlexander Pyhalov                if testgroup.verify(logger):
580*9f923083SAlexander Pyhalov                    self.testgroups[section] = testgroup
581*9f923083SAlexander Pyhalov            else:
582*9f923083SAlexander Pyhalov                test = Test(section)
583*9f923083SAlexander Pyhalov                for prop in Test.props:
584*9f923083SAlexander Pyhalov                    for sect in ['DEFAULT', section]:
585*9f923083SAlexander Pyhalov                        if config.has_option(sect, prop):
586*9f923083SAlexander Pyhalov                            setattr(test, prop, config.get(sect, prop))
587*9f923083SAlexander Pyhalov
588*9f923083SAlexander Pyhalov                if test.verify(logger):
589*9f923083SAlexander Pyhalov                    self.tests[section] = test
590*9f923083SAlexander Pyhalov
591*9f923083SAlexander Pyhalov    def write(self, options):
592*9f923083SAlexander Pyhalov        """
593*9f923083SAlexander Pyhalov        Create a configuration file for editing and later use. The
594*9f923083SAlexander Pyhalov        'DEFAULT' section of the config file is created from the
595*9f923083SAlexander Pyhalov        properties that were specified on the command line. Tests are
596*9f923083SAlexander Pyhalov        simply added as sections that inherit everything from the
597*9f923083SAlexander Pyhalov        'DEFAULT' section. TestGroups are the same, except they get an
598*9f923083SAlexander Pyhalov        option including all the tests to run in that directory.
599*9f923083SAlexander Pyhalov        """
600*9f923083SAlexander Pyhalov
601*9f923083SAlexander Pyhalov        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
602*9f923083SAlexander Pyhalov                         self.defaults])
603*9f923083SAlexander Pyhalov        config = ConfigParser.RawConfigParser(defaults)
604*9f923083SAlexander Pyhalov
605*9f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
606*9f923083SAlexander Pyhalov            config.add_section(test)
607*9f923083SAlexander Pyhalov
608*9f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
609*9f923083SAlexander Pyhalov            config.add_section(testgroup)
610*9f923083SAlexander Pyhalov            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
611*9f923083SAlexander Pyhalov
612*9f923083SAlexander Pyhalov        try:
613*9f923083SAlexander Pyhalov            with open(options.template, 'w') as f:
614*9f923083SAlexander Pyhalov                return config.write(f)
615*9f923083SAlexander Pyhalov        except IOError:
616*9f923083SAlexander Pyhalov            fail('Could not open \'%s\' for writing.' % options.template)
617*9f923083SAlexander Pyhalov
618*9f923083SAlexander Pyhalov    def complete_outputdirs(self):
619*9f923083SAlexander Pyhalov        """
620*9f923083SAlexander Pyhalov        Collect all the pathnames for Tests, and TestGroups. Work
621*9f923083SAlexander Pyhalov        backwards one pathname component at a time, to create a unique
622*9f923083SAlexander Pyhalov        directory name in which to deposit test output. Tests will be able
623*9f923083SAlexander Pyhalov        to write output files directly in the newly modified outputdir.
624*9f923083SAlexander Pyhalov        TestGroups will be able to create one subdirectory per test in the
625*9f923083SAlexander Pyhalov        outputdir, and are guaranteed uniqueness because a group can only
626*9f923083SAlexander Pyhalov        contain files in one directory. Pre and post tests will create a
627*9f923083SAlexander Pyhalov        directory rooted at the outputdir of the Test or TestGroup in
628*9f923083SAlexander Pyhalov        question for their output.
629*9f923083SAlexander Pyhalov        """
630*9f923083SAlexander Pyhalov        done = False
631*9f923083SAlexander Pyhalov        components = 0
632*9f923083SAlexander Pyhalov        tmp_dict = dict(self.tests.items() + self.testgroups.items())
633*9f923083SAlexander Pyhalov        total = len(tmp_dict)
634*9f923083SAlexander Pyhalov        base = self.outputdir
635*9f923083SAlexander Pyhalov
636*9f923083SAlexander Pyhalov        while not done:
637*9f923083SAlexander Pyhalov            l = []
638*9f923083SAlexander Pyhalov            components -= 1
639*9f923083SAlexander Pyhalov            for testfile in tmp_dict.keys():
640*9f923083SAlexander Pyhalov                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
641*9f923083SAlexander Pyhalov                if uniq not in l:
642*9f923083SAlexander Pyhalov                    l.append(uniq)
643*9f923083SAlexander Pyhalov                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
644*9f923083SAlexander Pyhalov                else:
645*9f923083SAlexander Pyhalov                    break
646*9f923083SAlexander Pyhalov            done = total == len(l)
647*9f923083SAlexander Pyhalov
648*9f923083SAlexander Pyhalov    def setup_logging(self, options):
649*9f923083SAlexander Pyhalov        """
650*9f923083SAlexander Pyhalov        Two loggers are set up here. The first is for the logfile which
651*9f923083SAlexander Pyhalov        will contain one line summarizing the test, including the test
652*9f923083SAlexander Pyhalov        name, result, and running time. This logger will also capture the
653*9f923083SAlexander Pyhalov        timestamped combined stdout and stderr of each run. The second
654*9f923083SAlexander Pyhalov        logger is optional console output, which will contain only the one
655*9f923083SAlexander Pyhalov        line summary. The loggers are initialized at two different levels
656*9f923083SAlexander Pyhalov        to facilitate segregating the output.
657*9f923083SAlexander Pyhalov        """
658*9f923083SAlexander Pyhalov        if options.dryrun is True:
659*9f923083SAlexander Pyhalov            return
660*9f923083SAlexander Pyhalov
661*9f923083SAlexander Pyhalov        testlogger = logging.getLogger(__name__)
662*9f923083SAlexander Pyhalov        testlogger.setLevel(logging.DEBUG)
663*9f923083SAlexander Pyhalov
664*9f923083SAlexander Pyhalov        if options.cmd is not 'wrconfig':
665*9f923083SAlexander Pyhalov            try:
666*9f923083SAlexander Pyhalov                old = os.umask(0)
667*9f923083SAlexander Pyhalov                os.makedirs(self.outputdir, mode=0777)
668*9f923083SAlexander Pyhalov                os.umask(old)
669*9f923083SAlexander Pyhalov            except OSError, e:
670*9f923083SAlexander Pyhalov                fail('%s' % e)
671*9f923083SAlexander Pyhalov            filename = os.path.join(self.outputdir, 'log')
672*9f923083SAlexander Pyhalov
673*9f923083SAlexander Pyhalov            logfile = WatchedFileHandlerClosed(filename)
674*9f923083SAlexander Pyhalov            logfile.setLevel(logging.DEBUG)
675*9f923083SAlexander Pyhalov            logfilefmt = logging.Formatter('%(message)s')
676*9f923083SAlexander Pyhalov            logfile.setFormatter(logfilefmt)
677*9f923083SAlexander Pyhalov            testlogger.addHandler(logfile)
678*9f923083SAlexander Pyhalov
679*9f923083SAlexander Pyhalov        cons = logging.StreamHandler()
680*9f923083SAlexander Pyhalov        cons.setLevel(logging.INFO)
681*9f923083SAlexander Pyhalov        consfmt = logging.Formatter('%(message)s')
682*9f923083SAlexander Pyhalov        cons.setFormatter(consfmt)
683*9f923083SAlexander Pyhalov        testlogger.addHandler(cons)
684*9f923083SAlexander Pyhalov
685*9f923083SAlexander Pyhalov        return testlogger
686*9f923083SAlexander Pyhalov
687*9f923083SAlexander Pyhalov    def run(self, options):
688*9f923083SAlexander Pyhalov        """
689*9f923083SAlexander Pyhalov        Walk through all the Tests and TestGroups, calling run().
690*9f923083SAlexander Pyhalov        """
691*9f923083SAlexander Pyhalov        try:
692*9f923083SAlexander Pyhalov            os.chdir(self.outputdir)
693*9f923083SAlexander Pyhalov        except OSError:
694*9f923083SAlexander Pyhalov            fail('Could not change to directory %s' % self.outputdir)
695*9f923083SAlexander Pyhalov        for test in sorted(self.tests.keys()):
696*9f923083SAlexander Pyhalov            self.tests[test].run(self.logger, options)
697*9f923083SAlexander Pyhalov        for testgroup in sorted(self.testgroups.keys()):
698*9f923083SAlexander Pyhalov            self.testgroups[testgroup].run(self.logger, options)
699*9f923083SAlexander Pyhalov
700*9f923083SAlexander Pyhalov    def summary(self):
701*9f923083SAlexander Pyhalov        if Result.total is 0:
702*9f923083SAlexander Pyhalov            return
703*9f923083SAlexander Pyhalov
704*9f923083SAlexander Pyhalov        print '\nResults Summary'
705*9f923083SAlexander Pyhalov        for key in Result.runresults.keys():
706*9f923083SAlexander Pyhalov            if Result.runresults[key] is not 0:
707*9f923083SAlexander Pyhalov                print '%s\t% 4d' % (key, Result.runresults[key])
708*9f923083SAlexander Pyhalov
709*9f923083SAlexander Pyhalov        m, s = divmod(time() - self.starttime, 60)
710*9f923083SAlexander Pyhalov        h, m = divmod(m, 60)
711*9f923083SAlexander Pyhalov        print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
712*9f923083SAlexander Pyhalov        print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
713*9f923083SAlexander Pyhalov                                            float(Result.total)) * 100)
714*9f923083SAlexander Pyhalov        print 'Log directory:\t%s' % self.outputdir
715*9f923083SAlexander Pyhalov
716*9f923083SAlexander Pyhalov
717*9f923083SAlexander Pyhalovdef verify_file(pathname):
718*9f923083SAlexander Pyhalov    """
719*9f923083SAlexander Pyhalov    Verify that the supplied pathname is an executable regular file.
720*9f923083SAlexander Pyhalov    """
721*9f923083SAlexander Pyhalov    if os.path.isdir(pathname) or os.path.islink(pathname):
722*9f923083SAlexander Pyhalov        return False
723*9f923083SAlexander Pyhalov
724*9f923083SAlexander Pyhalov    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
725*9f923083SAlexander Pyhalov        return True
726*9f923083SAlexander Pyhalov
727*9f923083SAlexander Pyhalov    return False
728*9f923083SAlexander Pyhalov
729*9f923083SAlexander Pyhalov
730*9f923083SAlexander Pyhalovdef verify_user(user, logger):
731*9f923083SAlexander Pyhalov    """
732*9f923083SAlexander Pyhalov    Verify that the specified user exists on this system, and can execute
733*9f923083SAlexander Pyhalov    sudo without being prompted for a password.
734*9f923083SAlexander Pyhalov    """
735*9f923083SAlexander Pyhalov    testcmd = [SUDO, '-n', '-u', user, TRUE]
736*9f923083SAlexander Pyhalov
737*9f923083SAlexander Pyhalov    if user in Cmd.verified_users:
738*9f923083SAlexander Pyhalov        return True
739*9f923083SAlexander Pyhalov
740*9f923083SAlexander Pyhalov    try:
741*9f923083SAlexander Pyhalov        _ = getpwnam(user)
742*9f923083SAlexander Pyhalov    except KeyError:
743*9f923083SAlexander Pyhalov        logger.info("Warning: user '%s' does not exist.", user)
744*9f923083SAlexander Pyhalov        return False
745*9f923083SAlexander Pyhalov
746*9f923083SAlexander Pyhalov    p = Popen(testcmd)
747*9f923083SAlexander Pyhalov    p.wait()
748*9f923083SAlexander Pyhalov    if p.returncode is not 0:
749*9f923083SAlexander Pyhalov        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
750*9f923083SAlexander Pyhalov        return False
751*9f923083SAlexander Pyhalov    else:
752*9f923083SAlexander Pyhalov        Cmd.verified_users.append(user)
753*9f923083SAlexander Pyhalov
754*9f923083SAlexander Pyhalov    return True
755*9f923083SAlexander Pyhalov
756*9f923083SAlexander Pyhalov
757*9f923083SAlexander Pyhalovdef find_tests(testrun, options):
758*9f923083SAlexander Pyhalov    """
759*9f923083SAlexander Pyhalov    For the given list of pathnames, add files as Tests. For directories,
760*9f923083SAlexander Pyhalov    if do_groups is True, add the directory as a TestGroup. If False,
761*9f923083SAlexander Pyhalov    recursively search for executable files.
762*9f923083SAlexander Pyhalov    """
763*9f923083SAlexander Pyhalov
764*9f923083SAlexander Pyhalov    for p in sorted(options.pathnames):
765*9f923083SAlexander Pyhalov        if os.path.isdir(p):
766*9f923083SAlexander Pyhalov            for dirname, _, filenames in os.walk(p):
767*9f923083SAlexander Pyhalov                if options.do_groups:
768*9f923083SAlexander Pyhalov                    testrun.addtestgroup(dirname, filenames, options)
769*9f923083SAlexander Pyhalov                else:
770*9f923083SAlexander Pyhalov                    for f in sorted(filenames):
771*9f923083SAlexander Pyhalov                        testrun.addtest(os.path.join(dirname, f), options)
772*9f923083SAlexander Pyhalov        else:
773*9f923083SAlexander Pyhalov            testrun.addtest(p, options)
774*9f923083SAlexander Pyhalov
775*9f923083SAlexander Pyhalov
776*9f923083SAlexander Pyhalovdef fail(retstr, ret=1):
777*9f923083SAlexander Pyhalov    print '%s: %s' % (argv[0], retstr)
778*9f923083SAlexander Pyhalov    exit(ret)
779*9f923083SAlexander Pyhalov
780*9f923083SAlexander Pyhalov
781*9f923083SAlexander Pyhalovdef options_cb(option, opt_str, value, parser):
782*9f923083SAlexander Pyhalov    path_options = ['runfile', 'outputdir', 'template']
783*9f923083SAlexander Pyhalov
784*9f923083SAlexander Pyhalov    if option.dest is 'runfile' and '-w' in parser.rargs or \
785*9f923083SAlexander Pyhalov            option.dest is 'template' and '-c' in parser.rargs:
786*9f923083SAlexander Pyhalov        fail('-c and -w are mutually exclusive.')
787*9f923083SAlexander Pyhalov
788*9f923083SAlexander Pyhalov    if opt_str in parser.rargs:
789*9f923083SAlexander Pyhalov        fail('%s may only be specified once.' % opt_str)
790*9f923083SAlexander Pyhalov
791*9f923083SAlexander Pyhalov    if option.dest is 'runfile':
792*9f923083SAlexander Pyhalov        parser.values.cmd = 'rdconfig'
793*9f923083SAlexander Pyhalov    if option.dest is 'template':
794*9f923083SAlexander Pyhalov        parser.values.cmd = 'wrconfig'
795*9f923083SAlexander Pyhalov
796*9f923083SAlexander Pyhalov    setattr(parser.values, option.dest, value)
797*9f923083SAlexander Pyhalov    if option.dest in path_options:
798*9f923083SAlexander Pyhalov        setattr(parser.values, option.dest, os.path.abspath(value))
799*9f923083SAlexander Pyhalov
800*9f923083SAlexander Pyhalov
801*9f923083SAlexander Pyhalovdef parse_args():
802*9f923083SAlexander Pyhalov    parser = OptionParser()
803*9f923083SAlexander Pyhalov    parser.add_option('-c', action='callback', callback=options_cb,
804*9f923083SAlexander Pyhalov                      type='string', dest='runfile', metavar='runfile',
805*9f923083SAlexander Pyhalov                      help='Specify tests to run via config file.')
806*9f923083SAlexander Pyhalov    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
807*9f923083SAlexander Pyhalov                      help='Dry run. Print tests, but take no other action.')
808*9f923083SAlexander Pyhalov    parser.add_option('-g', action='store_true', default=False,
809*9f923083SAlexander Pyhalov                      dest='do_groups', help='Make directories TestGroups.')
810*9f923083SAlexander Pyhalov    parser.add_option('-o', action='callback', callback=options_cb,
811*9f923083SAlexander Pyhalov                      default=BASEDIR, dest='outputdir', type='string',
812*9f923083SAlexander Pyhalov                      metavar='outputdir', help='Specify an output directory.')
813*9f923083SAlexander Pyhalov    parser.add_option('-p', action='callback', callback=options_cb,
814*9f923083SAlexander Pyhalov                      default='', dest='pre', metavar='script',
815*9f923083SAlexander Pyhalov                      type='string', help='Specify a pre script.')
816*9f923083SAlexander Pyhalov    parser.add_option('-P', action='callback', callback=options_cb,
817*9f923083SAlexander Pyhalov                      default='', dest='post', metavar='script',
818*9f923083SAlexander Pyhalov                      type='string', help='Specify a post script.')
819*9f923083SAlexander Pyhalov    parser.add_option('-q', action='store_true', default=False, dest='quiet',
820*9f923083SAlexander Pyhalov                      help='Silence on the console during a test run.')
821*9f923083SAlexander Pyhalov    parser.add_option('-t', action='callback', callback=options_cb, default=60,
822*9f923083SAlexander Pyhalov                      dest='timeout', metavar='seconds', type='int',
823*9f923083SAlexander Pyhalov                      help='Timeout (in seconds) for an individual test.')
824*9f923083SAlexander Pyhalov    parser.add_option('-u', action='callback', callback=options_cb,
825*9f923083SAlexander Pyhalov                      default='', dest='user', metavar='user', type='string',
826*9f923083SAlexander Pyhalov                      help='Specify a different user name to run as.')
827*9f923083SAlexander Pyhalov    parser.add_option('-w', action='callback', callback=options_cb,
828*9f923083SAlexander Pyhalov                      default=None, dest='template', metavar='template',
829*9f923083SAlexander Pyhalov                      type='string', help='Create a new config file.')
830*9f923083SAlexander Pyhalov    parser.add_option('-x', action='callback', callback=options_cb, default='',
831*9f923083SAlexander Pyhalov                      dest='pre_user', metavar='pre_user', type='string',
832*9f923083SAlexander Pyhalov                      help='Specify a user to execute the pre script.')
833*9f923083SAlexander Pyhalov    parser.add_option('-X', action='callback', callback=options_cb, default='',
834*9f923083SAlexander Pyhalov                      dest='post_user', metavar='post_user', type='string',
835*9f923083SAlexander Pyhalov                      help='Specify a user to execute the post script.')
836*9f923083SAlexander Pyhalov    (options, pathnames) = parser.parse_args()
837*9f923083SAlexander Pyhalov
838*9f923083SAlexander Pyhalov    if not options.runfile and not options.template:
839*9f923083SAlexander Pyhalov        options.cmd = 'runtests'
840*9f923083SAlexander Pyhalov
841*9f923083SAlexander Pyhalov    if options.runfile and len(pathnames):
842*9f923083SAlexander Pyhalov        fail('Extraneous arguments.')
843*9f923083SAlexander Pyhalov
844*9f923083SAlexander Pyhalov    options.pathnames = [os.path.abspath(path) for path in pathnames]
845*9f923083SAlexander Pyhalov
846*9f923083SAlexander Pyhalov    return options
847*9f923083SAlexander Pyhalov
848*9f923083SAlexander Pyhalov
849*9f923083SAlexander Pyhalovdef main():
850*9f923083SAlexander Pyhalov    options = parse_args()
851*9f923083SAlexander Pyhalov    testrun = TestRun(options)
852*9f923083SAlexander Pyhalov
853*9f923083SAlexander Pyhalov    if options.cmd is 'runtests':
854*9f923083SAlexander Pyhalov        find_tests(testrun, options)
855*9f923083SAlexander Pyhalov    elif options.cmd is 'rdconfig':
856*9f923083SAlexander Pyhalov        testrun.read(testrun.logger, options)
857*9f923083SAlexander Pyhalov    elif options.cmd is 'wrconfig':
858*9f923083SAlexander Pyhalov        find_tests(testrun, options)
859*9f923083SAlexander Pyhalov        testrun.write(options)
860*9f923083SAlexander Pyhalov        exit(0)
861*9f923083SAlexander Pyhalov    else:
862*9f923083SAlexander Pyhalov        fail('Unknown command specified')
863*9f923083SAlexander Pyhalov
864*9f923083SAlexander Pyhalov    testrun.complete_outputdirs()
865*9f923083SAlexander Pyhalov    testrun.run(options)
866*9f923083SAlexander Pyhalov    testrun.summary()
867*9f923083SAlexander Pyhalov    exit(0)
868*9f923083SAlexander Pyhalov
869*9f923083SAlexander Pyhalov
870*9f923083SAlexander Pyhalovif __name__ == '__main__':
871*9f923083SAlexander Pyhalov    main()
872