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