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