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