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