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