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