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 # Copyright 2018 Joyent, Inc.
18 #
19
20 import ConfigParser
21 import os
22 import logging
23 import platform
24 from logging.handlers import WatchedFileHandler
25 from datetime import datetime
26 from optparse import OptionParser
27 from pwd import getpwnam
28 from pwd import getpwuid
29 from select import select
30 from subprocess import PIPE
31 from subprocess import Popen
32 from sys import argv
33 from sys import maxint
34 from threading import Timer
35 from time import time
36
37 BASEDIR = '/var/tmp/test_results'
38 KILL = '/usr/bin/kill'
39 TRUE = '/usr/bin/true'
40 SUDO = '/usr/bin/sudo'
41
42 # Custom class to reopen the log file in case it is forcibly closed by a test.
43 class WatchedFileHandlerClosed(WatchedFileHandler):
44 """Watch files, including closed files.
45 Similar to (and inherits from) logging.handler.WatchedFileHandler,
46 except that IOErrors are handled by reopening the stream and retrying.
47 This will be retried up to a configurable number of times before
48 giving up, default 5.
49 """
50
51 def __init__(self, filename, mode='a', encoding=None, delay=0, max_tries=5):
52 self.max_tries = max_tries
53 self.tries = 0
54 WatchedFileHandler.__init__(self, filename, mode, encoding, delay)
55
56 def emit(self, record):
57 while True:
58 try:
59 WatchedFileHandler.emit(self, record)
60 self.tries = 0
61 return
62 except IOError as err:
63 if self.tries == self.max_tries:
64 raise
65 self.stream.close()
66 self.stream = self._open()
67 self.tries += 1
68
69 class Result(object):
70 total = 0
71 runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0}
72
73 def __init__(self):
74 self.starttime = None
75 self.returncode = None
76 self.runtime = ''
77 self.stdout = []
78 self.stderr = []
79 self.result = ''
80
81 def done(self, proc, killed):
82 """
83 Finalize the results of this Cmd.
84 """
85 Result.total += 1
86 m, s = divmod(time() - self.starttime, 60)
87 self.runtime = '%02d:%02d' % (m, s)
88 self.returncode = proc.returncode
89 if killed:
90 self.result = 'KILLED'
91 Result.runresults['KILLED'] += 1
92 elif self.returncode is 0:
93 self.result = 'PASS'
94 Result.runresults['PASS'] += 1
95 elif self.returncode is not 0:
96 self.result = 'FAIL'
97 Result.runresults['FAIL'] += 1
98
99
100 class Output(object):
101 """
102 This class is a slightly modified version of the 'Stream' class found
103 here: http://goo.gl/aSGfv
104 """
105 def __init__(self, stream):
106 self.stream = stream
107 self._buf = ''
108 self.lines = []
109
110 def fileno(self):
111 return self.stream.fileno()
112
113 def read(self, drain=0):
114 """
115 Read from the file descriptor. If 'drain' set, read until EOF.
116 """
117 while self._read() is not None:
118 if not drain:
119 break
120
121 def _read(self):
122 """
123 Read up to 4k of data from this output stream. Collect the output
124 up to the last newline, and append it to any leftover data from a
125 previous call. The lines are stored as a (timestamp, data) tuple
126 for easy sorting/merging later.
127 """
128 fd = self.fileno()
129 buf = os.read(fd, 4096)
130 if not buf:
131 return None
132 if '\n' not in buf:
133 self._buf += buf
134 return []
135
136 buf = self._buf + buf
137 tmp, rest = buf.rsplit('\n', 1)
138 self._buf = rest
139 now = datetime.now()
140 rows = tmp.split('\n')
141 self.lines += [(now, r) for r in rows]
142
143
144 class Cmd(object):
145 verified_users = []
146
147 def __init__(self, pathname, outputdir=None, timeout=None, user=None):
148 self.pathname = pathname
149 self.outputdir = outputdir or 'BASEDIR'
150 self.timeout = timeout
151 self.user = user or ''
152 self.killed = False
153 self.result = Result()
154
155 if self.timeout is None:
156 self.timeout = 60
157
158 def __str__(self):
159 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % \
160 (self.pathname, self.outputdir, self.timeout, self.user)
161
162 def kill_cmd(self, proc):
163 """
164 Kill a running command due to timeout, or ^C from the keyboard. If
165 sudo is required, this user was verified previously.
166 """
167 self.killed = True
168 do_sudo = len(self.user) != 0
169 signal = '-TERM'
170
171 cmd = [SUDO, KILL, signal, str(proc.pid)]
172 if not do_sudo:
173 del cmd[0]
174
175 try:
176 kp = Popen(cmd)
177 kp.wait()
178 except:
179 pass
180
181 def update_cmd_privs(self, cmd, user):
182 """
183 If a user has been specified to run this Cmd and we're not already
184 running as that user, prepend the appropriate sudo command to run
185 as that user.
186 """
187 me = getpwuid(os.getuid())
188
189 if not user or user is me:
190 return cmd
191
192 ret = '%s -E -u %s %s' % (SUDO, user, cmd)
193 return ret.split(' ')
194
195 def collect_output(self, proc):
196 """
197 Read from stdout/stderr as data becomes available, until the
198 process is no longer running. Return the lines from the stdout and
199 stderr Output objects.
200 """
201 out = Output(proc.stdout)
202 err = Output(proc.stderr)
203 res = []
204 while proc.returncode is None:
205 proc.poll()
206 res = select([out, err], [], [], .1)
207 for fd in res[0]:
208 fd.read()
209 for fd in res[0]:
210 fd.read(drain=1)
211
212 return out.lines, err.lines
213
214 def run(self, options):
215 """
216 This is the main function that runs each individual test.
217 Determine whether or not the command requires sudo, and modify it
218 if needed. Run the command, and update the result object.
219 """
220 if options.dryrun is True:
221 print self
222 return
223
224 privcmd = self.update_cmd_privs(self.pathname, self.user)
225 try:
226 old = os.umask(0)
227 if not os.path.isdir(self.outputdir):
228 os.makedirs(self.outputdir, mode=0777)
229 os.umask(old)
230 except OSError, e:
231 fail('%s' % e)
232
233 try:
234 self.result.starttime = time()
235 proc = Popen(privcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE)
236 proc.stdin.close()
237
238 # Allow a special timeout value of 0 to mean infinity
239 if int(self.timeout) == 0:
240 self.timeout = maxint
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 is not '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 cmp=lambda x, y: cmp(x[0], y[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 os.write(out.fileno(), '%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 os.write(err.fileno(), '%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 os.write(merged.fileno(), '%s\n' % line)
309
310
311 class 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: %d\nPre: %s%s\nPost: " \
330 "%s%s\nUser: %s\n" % \
331 (self.pathname, self.outputdir, self.timeout, self.pre,
332 pre_user, self.post, post_user, 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 odir = os.path.join(self.outputdir, os.path.basename(self.pre))
362 pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
363 user=self.pre_user)
364 test = Cmd(self.pathname, outputdir=self.outputdir,
365 timeout=self.timeout, user=self.user)
366 odir = os.path.join(self.outputdir, os.path.basename(self.post))
367 posttest = Cmd(self.post, outputdir=odir, 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 is '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
388 class 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: %d\n" \
405 "Pre: %s%s\nPost: %s%s\nUser: %s\n" % \
406 (self.pathname, self.outputdir, self.tests, self.timeout,
407 self.pre, pre_user, 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 self.tests[:] = [f for f in self.tests if
446 verify_file(os.path.join(self.pathname, f))]
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
485 class 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 ('arch' in config.options(section) and
570 platform.machine() != config.get(section, 'arch')):
571 continue
572
573 if 'tests' in config.options(section):
574 testgroup = TestGroup(section)
575 for prop in TestGroup.props:
576 for sect in ['DEFAULT', section]:
577 if config.has_option(sect, prop):
578 setattr(testgroup, prop, config.get(sect, prop))
579
580 # Repopulate tests using eval to convert the string to a list
581 testgroup.tests = eval(config.get(section, 'tests'))
582
583 if testgroup.verify(logger):
584 self.testgroups[section] = testgroup
585
586 elif 'autotests' in config.options(section):
587 testgroup = TestGroup(section)
588 for prop in TestGroup.props:
589 for sect in ['DEFAULT', section]:
590 if config.has_option(sect, prop):
591 setattr(testgroup, prop, config.get(sect, prop))
592
593 filenames = os.listdir(section)
594 # only files starting with "tst." are considered tests
595 filenames = [f for f in filenames if f.startswith("tst.")]
596 testgroup.tests = sorted(filenames)
597
598 if testgroup.verify(logger):
599 self.testgroups[section] = testgroup
600
601 else:
602 test = Test(section)
603 for prop in Test.props:
604 for sect in ['DEFAULT', section]:
605 if config.has_option(sect, prop):
606 setattr(test, prop, config.get(sect, prop))
607
608 if test.verify(logger):
609 self.tests[section] = test
610
611 def write(self, options):
612 """
613 Create a configuration file for editing and later use. The
614 'DEFAULT' section of the config file is created from the
615 properties that were specified on the command line. Tests are
616 simply added as sections that inherit everything from the
617 'DEFAULT' section. TestGroups are the same, except they get an
618 option including all the tests to run in that directory.
619 """
620
621 defaults = dict([(prop, getattr(options, prop)) for prop, _ in
622 self.defaults])
623 config = ConfigParser.RawConfigParser(defaults)
624
625 for test in sorted(self.tests.keys()):
626 config.add_section(test)
627
628 for testgroup in sorted(self.testgroups.keys()):
629 config.add_section(testgroup)
630 config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
631
632 try:
633 with open(options.template, 'w') as f:
634 return config.write(f)
635 except IOError:
636 fail('Could not open \'%s\' for writing.' % options.template)
637
638 def complete_outputdirs(self):
639 """
640 Collect all the pathnames for Tests, and TestGroups. Work
641 backwards one pathname component at a time, to create a unique
642 directory name in which to deposit test output. Tests will be able
643 to write output files directly in the newly modified outputdir.
644 TestGroups will be able to create one subdirectory per test in the
645 outputdir, and are guaranteed uniqueness because a group can only
646 contain files in one directory. Pre and post tests will create a
647 directory rooted at the outputdir of the Test or TestGroup in
648 question for their output.
649 """
650 done = False
651 components = 0
652 tmp_dict = dict(self.tests.items() + self.testgroups.items())
653 total = len(tmp_dict)
654 base = self.outputdir
655
656 while not done:
657 l = []
658 components -= 1
659 for testfile in tmp_dict.keys():
660 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
661 if uniq not in l:
662 l.append(uniq)
663 tmp_dict[testfile].outputdir = os.path.join(base, uniq)
664 else:
665 break
666 done = total == len(l)
667
668 def setup_logging(self, options):
669 """
670 Two loggers are set up here. The first is for the logfile which
671 will contain one line summarizing the test, including the test
672 name, result, and running time. This logger will also capture the
673 timestamped combined stdout and stderr of each run. The second
674 logger is optional console output, which will contain only the one
675 line summary. The loggers are initialized at two different levels
676 to facilitate segregating the output.
677 """
678 if options.dryrun is True:
679 return
680
681 testlogger = logging.getLogger(__name__)
682 testlogger.setLevel(logging.DEBUG)
683
684 if options.cmd is not 'wrconfig':
685 try:
686 old = os.umask(0)
687 os.makedirs(self.outputdir, mode=0777)
688 os.umask(old)
689 except OSError, e:
690 fail('%s' % e)
691 filename = os.path.join(self.outputdir, 'log')
692
693 logfile = WatchedFileHandlerClosed(filename)
694 logfile.setLevel(logging.DEBUG)
695 logfilefmt = logging.Formatter('%(message)s')
696 logfile.setFormatter(logfilefmt)
697 testlogger.addHandler(logfile)
698
699 cons = logging.StreamHandler()
700 cons.setLevel(logging.INFO)
701 consfmt = logging.Formatter('%(message)s')
702 cons.setFormatter(consfmt)
703 testlogger.addHandler(cons)
704
705 return testlogger
706
707 def run(self, options):
708 """
709 Walk through all the Tests and TestGroups, calling run().
710 """
711 if not options.dryrun:
712 try:
713 os.chdir(self.outputdir)
714 except OSError:
715 fail('Could not change to directory %s' % self.outputdir)
716 for test in sorted(self.tests.keys()):
717 self.tests[test].run(self.logger, options)
718 for testgroup in sorted(self.testgroups.keys()):
719 self.testgroups[testgroup].run(self.logger, options)
720
721 def summary(self):
722 if Result.total is 0:
723 return
724
725 print '\nResults Summary'
726 for key in Result.runresults.keys():
727 if Result.runresults[key] is not 0:
728 print '%s\t% 4d' % (key, Result.runresults[key])
729
730 m, s = divmod(time() - self.starttime, 60)
731 h, m = divmod(m, 60)
732 print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
733 print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
734 float(Result.total)) * 100)
735 print 'Log directory:\t%s' % self.outputdir
736
737
738 def verify_file(pathname):
739 """
740 Verify that the supplied pathname is an executable regular file.
741 """
742 if os.path.isdir(pathname) or os.path.islink(pathname):
743 return False
744
745 if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
746 return True
747
748 return False
749
750
751 def verify_user(user, logger):
752 """
753 Verify that the specified user exists on this system, and can execute
754 sudo without being prompted for a password.
755 """
756 testcmd = [SUDO, '-n', '-u', user, TRUE]
757
758 if user in Cmd.verified_users:
759 return True
760
761 try:
762 _ = getpwnam(user)
763 except KeyError:
764 logger.info("Warning: user '%s' does not exist.", user)
765 return False
766
767 p = Popen(testcmd)
768 p.wait()
769 if p.returncode is not 0:
770 logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
771 return False
772 else:
773 Cmd.verified_users.append(user)
774
775 return True
776
777
778 def find_tests(testrun, options):
779 """
780 For the given list of pathnames, add files as Tests. For directories,
781 if do_groups is True, add the directory as a TestGroup. If False,
782 recursively search for executable files.
783 """
784
785 for p in sorted(options.pathnames):
786 if os.path.isdir(p):
787 for dirname, _, filenames in os.walk(p):
788 if options.do_groups:
789 testrun.addtestgroup(dirname, filenames, options)
790 else:
791 for f in sorted(filenames):
792 testrun.addtest(os.path.join(dirname, f), options)
793 else:
794 testrun.addtest(p, options)
795
796
797 def fail(retstr, ret=1):
798 print '%s: %s' % (argv[0], retstr)
799 exit(ret)
800
801
802 def options_cb(option, opt_str, value, parser):
803 path_options = ['runfile', 'outputdir', 'template']
804
805 if option.dest is 'runfile' and '-w' in parser.rargs or \
806 option.dest is 'template' and '-c' in parser.rargs:
807 fail('-c and -w are mutually exclusive.')
808
809 if opt_str in parser.rargs:
810 fail('%s may only be specified once.' % opt_str)
811
812 if option.dest is 'runfile':
813 parser.values.cmd = 'rdconfig'
814 if option.dest is 'template':
815 parser.values.cmd = 'wrconfig'
816
817 setattr(parser.values, option.dest, value)
818 if option.dest in path_options:
819 setattr(parser.values, option.dest, os.path.abspath(value))
820
821
822 def parse_args():
823 parser = OptionParser()
824 parser.add_option('-c', action='callback', callback=options_cb,
825 type='string', dest='runfile', metavar='runfile',
826 help='Specify tests to run via config file.')
827 parser.add_option('-d', action='store_true', default=False, dest='dryrun',
828 help='Dry run. Print tests, but take no other action.')
829 parser.add_option('-g', action='store_true', default=False,
830 dest='do_groups', help='Make directories TestGroups.')
831 parser.add_option('-o', action='callback', callback=options_cb,
832 default=BASEDIR, dest='outputdir', type='string',
833 metavar='outputdir', help='Specify an output directory.')
834 parser.add_option('-p', action='callback', callback=options_cb,
835 default='', dest='pre', metavar='script',
836 type='string', help='Specify a pre script.')
837 parser.add_option('-P', action='callback', callback=options_cb,
838 default='', dest='post', metavar='script',
839 type='string', help='Specify a post script.')
840 parser.add_option('-q', action='store_true', default=False, dest='quiet',
841 help='Silence on the console during a test run.')
842 parser.add_option('-t', action='callback', callback=options_cb, default=60,
843 dest='timeout', metavar='seconds', type='int',
844 help='Timeout (in seconds) for an individual test.')
845 parser.add_option('-u', action='callback', callback=options_cb,
846 default='', dest='user', metavar='user', type='string',
847 help='Specify a different user name to run as.')
848 parser.add_option('-w', action='callback', callback=options_cb,
849 default=None, dest='template', metavar='template',
850 type='string', help='Create a new config file.')
851 parser.add_option('-x', action='callback', callback=options_cb, default='',
852 dest='pre_user', metavar='pre_user', type='string',
853 help='Specify a user to execute the pre script.')
854 parser.add_option('-X', action='callback', callback=options_cb, default='',
855 dest='post_user', metavar='post_user', type='string',
856 help='Specify a user to execute the post script.')
857 (options, pathnames) = parser.parse_args()
858
859 if not options.runfile and not options.template:
860 options.cmd = 'runtests'
861
862 if options.runfile and len(pathnames):
863 fail('Extraneous arguments.')
864
865 options.pathnames = [os.path.abspath(path) for path in pathnames]
866
867 return options
868
869
870 def main():
871 options = parse_args()
872 testrun = TestRun(options)
873
874 if options.cmd is 'runtests':
875 find_tests(testrun, options)
876 elif options.cmd is 'rdconfig':
877 testrun.read(testrun.logger, options)
878 elif options.cmd is 'wrconfig':
879 find_tests(testrun, options)
880 testrun.write(options)
881 exit(0)
882 else:
883 fail('Unknown command specified')
884
885 testrun.complete_outputdirs()
886 testrun.run(options)
887 testrun.summary()
888 exit(0)
889
890
891 if __name__ == '__main__':
892 main()