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