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