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) 2013 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 logname = getpwuid(os.getuid()).pw_name
238 user = ' (run as %s)' % (self.user if len(self.user) else logname)
239 msga = 'Test: %s%s ' % (self.pathname, user)
240 msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
241 pad = ' ' * (80 - (len(msga) + len(msgb)))
242
243 # If -q is specified, only print a line for tests that didn't pass.
244 # This means passing tests need to be logged as DEBUG, or the one
245 # line summary will only be printed in the logfile for failures.
246 if not options.quiet:
247 logger.info('%s%s%s' % (msga, pad, msgb))
248 elif self.result.result is not 'PASS':
249 logger.info('%s%s%s' % (msga, pad, msgb))
250 else:
251 logger.debug('%s%s%s' % (msga, pad, msgb))
252
253 lines = self.result.stdout + self.result.stderr
254 for dt, line in sorted(lines):
255 logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
256
257 if len(self.result.stdout):
258 with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
259 for _, line in self.result.stdout:
260 os.write(out.fileno(), '%s\n' % line)
261 if len(self.result.stderr):
262 with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
263 for _, line in self.result.stderr:
264 os.write(err.fileno(), '%s\n' % line)
265 if len(self.result.stdout) and len(self.result.stderr):
266 with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
267 for _, line in sorted(lines):
268 os.write(merged.fileno(), '%s\n' % line)
269
270
271 class Test(Cmd):
272 props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
273 'post_user']
274
275 def __init__(self, pathname, outputdir=None, timeout=None, user=None,
276 pre=None, pre_user=None, post=None, post_user=None):
277 super(Test, self).__init__(pathname, outputdir, timeout, user)
278 self.pre = pre or ''
279 self.pre_user = pre_user or ''
280 self.post = post or ''
281 self.post_user = post_user or ''
282
283 def __str__(self):
284 post_user = pre_user = ''
285 if len(self.pre_user):
286 pre_user = ' (as %s)' % (self.pre_user)
287 if len(self.post_user):
288 post_user = ' (as %s)' % (self.post_user)
289 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \
290 "%s%s\nUser: %s\n" % (self.pathname, self.outputdir,
291 self.timeout, self.pre, pre_user, self.post, post_user,
292 self.user)
293
294 def verify(self, logger):
295 """
296 Check the pre/post scripts, user and Test. Omit the Test from this
297 run if there are any problems.
298 """
299 files = [self.pre, self.pathname, self.post]
300 users = [self.pre_user, self.user, self.post_user]
301
302 for f in [f for f in files if len(f)]:
303 if not verify_file(f):
304 logger.info("Warning: Test '%s' not added to this run because"
305 " it failed verification." % f)
306 return False
307
308 for user in [user for user in users if len(user)]:
309 if not verify_user(user, logger):
310 logger.info("Not adding Test '%s' to this run." %
311 self.pathname)
312 return False
313
314 return True
315
316 def run(self, logger, options):
317 """
318 Create Cmd instances for the pre/post scripts. If the pre script
319 doesn't pass, skip this Test. Run the post script regardless.
320 """
321 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
322 os.path.basename(self.pre)), timeout=self.timeout,
323 user=self.pre_user)
324 test = Cmd(self.pathname, outputdir=self.outputdir,
325 timeout=self.timeout, user=self.user)
326 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
327 os.path.basename(self.post)), timeout=self.timeout,
328 user=self.post_user)
329
330 cont = True
331 if len(pretest.pathname):
332 pretest.run(options)
333 cont = pretest.result.result is 'PASS'
334 pretest.log(logger, options)
335
336 if cont:
337 test.run(options)
338 else:
339 test.skip()
340
341 test.log(logger, options)
342
343 if len(posttest.pathname):
344 posttest.run(options)
345 posttest.log(logger, options)
346
347
348 class TestGroup(Test):
349 props = Test.props + ['tests']
350
351 def __init__(self, pathname, outputdir=None, timeout=None, user=None,
352 pre=None, pre_user=None, post=None, post_user=None,
353 tests=None):
354 super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
355 pre, pre_user, post, post_user)
356 self.tests = tests or []
357
358 def __str__(self):
359 post_user = pre_user = ''
360 if len(self.pre_user):
361 pre_user = ' (as %s)' % (self.pre_user)
362 if len(self.post_user):
363 post_user = ' (as %s)' % (self.post_user)
364 return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \
365 "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname,
366 self.outputdir, self.tests, self.timeout, self.pre, pre_user,
367 self.post, post_user, self.user)
368
369 def verify(self, logger):
370 """
371 Check the pre/post scripts, user and tests in this TestGroup. Omit
372 the TestGroup entirely, or simply delete the relevant tests in the
373 group, if that's all that's required.
374 """
375 # If the pre or post scripts are relative pathnames, convert to
376 # absolute, so they stand a chance of passing verification.
377 if len(self.pre) and not os.path.isabs(self.pre):
378 self.pre = os.path.join(self.pathname, self.pre)
379 if len(self.post) and not os.path.isabs(self.post):
380 self.post = os.path.join(self.pathname, self.post)
381
382 auxfiles = [self.pre, self.post]
383 users = [self.pre_user, self.user, self.post_user]
384
385 for f in [f for f in auxfiles if len(f)]:
386 if self.pathname != os.path.dirname(f):
387 logger.info("Warning: TestGroup '%s' not added to this run. "
388 "Auxiliary script '%s' exists in a different "
389 "directory." % (self.pathname, f))
390 return False
391
392 if not verify_file(f):
393 logger.info("Warning: TestGroup '%s' not added to this run. "
394 "Auxiliary script '%s' failed verification." %
395 (self.pathname, f))
396 return False
397
398 for user in [user for user in users if len(user)]:
399 if not verify_user(user, logger):
400 logger.info("Not adding TestGroup '%s' to this run." %
401 self.pathname)
402 return False
403
404 # If one of the tests is invalid, delete it, log it, and drive on.
405 for test in self.tests:
406 if not verify_file(os.path.join(self.pathname, test)):
407 del self.tests[self.tests.index(test)]
408 logger.info("Warning: Test '%s' removed from TestGroup '%s' "
409 "because it failed verification." % (test,
410 self.pathname))
411
412 return len(self.tests) is not 0
413
414 def run(self, logger, options):
415 """
416 Create Cmd instances for the pre/post scripts. If the pre script
417 doesn't pass, skip all the tests in this TestGroup. Run the post
418 script regardless.
419 """
420 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
421 os.path.basename(self.pre)), timeout=self.timeout,
422 user=self.pre_user)
423 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
424 os.path.basename(self.post)), timeout=self.timeout,
425 user=self.post_user)
426
427 cont = True
428 if len(pretest.pathname):
429 pretest.run(options)
430 cont = pretest.result.result is 'PASS'
431 pretest.log(logger, options)
432
433 for fname in self.tests:
434 test = Cmd(os.path.join(self.pathname, fname),
435 outputdir=os.path.join(self.outputdir, fname),
436 timeout=self.timeout, user=self.user)
437 if cont:
438 test.run(options)
439 else:
440 test.skip()
441
442 test.log(logger, options)
443
444 if len(posttest.pathname):
445 posttest.run(options)
446 posttest.log(logger, options)
447
448
449 class TestRun(object):
450 props = ['quiet', 'outputdir']
451
452 def __init__(self, options):
453 self.tests = {}
454 self.testgroups = {}
455 self.starttime = time()
456 self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
457 self.outputdir = os.path.join(options.outputdir, self.timestamp)
458 self.logger = self.setup_logging(options)
459 self.defaults = [
460 ('outputdir', BASEDIR),
461 ('quiet', False),
462 ('timeout', 60),
463 ('user', ''),
464 ('pre', ''),
465 ('pre_user', ''),
466 ('post', ''),
467 ('post_user', '')
468 ]
469
470 def __str__(self):
471 s = 'TestRun:\n outputdir: %s\n' % self.outputdir
472 s += 'TESTS:\n'
473 for key in sorted(self.tests.keys()):
474 s += '%s%s' % (self.tests[key].__str__(), '\n')
475 s += 'TESTGROUPS:\n'
476 for key in sorted(self.testgroups.keys()):
477 s += '%s%s' % (self.testgroups[key].__str__(), '\n')
478 return s
479
480 def addtest(self, pathname, options):
481 """
482 Create a new Test, and apply any properties that were passed in
483 from the command line. If it passes verification, add it to the
484 TestRun.
485 """
486 test = Test(pathname)
487 for prop in Test.props:
488 setattr(test, prop, getattr(options, prop))
489
490 if test.verify(self.logger):
491 self.tests[pathname] = test
492
493 def addtestgroup(self, dirname, filenames, options):
494 """
495 Create a new TestGroup, and apply any properties that were passed
496 in from the command line. If it passes verification, add it to the
497 TestRun.
498 """
499 if dirname not in self.testgroups:
500 testgroup = TestGroup(dirname)
501 for prop in Test.props:
502 setattr(testgroup, prop, getattr(options, prop))
503
504 # Prevent pre/post scripts from running as regular tests
505 for f in [testgroup.pre, testgroup.post]:
506 if f in filenames:
507 del filenames[filenames.index(f)]
508
509 self.testgroups[dirname] = testgroup
510 self.testgroups[dirname].tests = sorted(filenames)
511
512 testgroup.verify(self.logger)
513
514 def read(self, logger, options):
515 """
516 Read in the specified runfile, and apply the TestRun properties
517 listed in the 'DEFAULT' section to our TestRun. Then read each
518 section, and apply the appropriate properties to the Test or
519 TestGroup. Properties from individual sections override those set
520 in the 'DEFAULT' section. If the Test or TestGroup passes
521 verification, add it to the TestRun.
522 """
523 config = ConfigParser.RawConfigParser()
524 if not len(config.read(options.runfile)):
525 fail("Coulnd't read config file %s" % options.runfile)
526
527 for opt in TestRun.props:
528 if config.has_option('DEFAULT', opt):
529 setattr(self, opt, config.get('DEFAULT', opt))
530 self.outputdir = os.path.join(self.outputdir, self.timestamp)
531
532 for section in config.sections():
533 if 'tests' in config.options(section):
534 testgroup = TestGroup(section)
535 for prop in TestGroup.props:
536 try:
537 setattr(testgroup, prop, config.get('DEFAULT', prop))
538 setattr(testgroup, prop, config.get(section, prop))
539 except ConfigParser.NoOptionError:
540 pass
541
542 # Repopulate tests using eval to convert the string to a list
543 testgroup.tests = eval(config.get(section, 'tests'))
544
545 if testgroup.verify(logger):
546 self.testgroups[section] = testgroup
547 else:
548 test = Test(section)
549 for prop in Test.props:
550 try:
551 setattr(test, prop, config.get('DEFAULT', prop))
552 setattr(test, prop, config.get(section, prop))
553 except ConfigParser.NoOptionError:
554 pass
555 if test.verify(logger):
556 self.tests[section] = test
557
558 def write(self, options):
559 """
560 Create a configuration file for editing and later use. The
561 'DEFAULT' section of the config file is created from the
562 properties that were specified on the command line. Tests are
563 simply added as sections that inherit everything from the
564 'DEFAULT' section. TestGroups are the same, except they get an
565 option including all the tests to run in that directory.
566 """
567
568 defaults = dict([(prop, getattr(options, prop)) for prop, _ in
569 self.defaults])
570 config = ConfigParser.RawConfigParser(defaults)
571
572 for test in sorted(self.tests.keys()):
573 config.add_section(test)
574
575 for testgroup in sorted(self.testgroups.keys()):
576 config.add_section(testgroup)
577 config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
578
579 try:
580 with open(options.template, 'w') as f:
581 return config.write(f)
582 except IOError:
583 fail('Could not open \'%s\' for writing.' % options.template)
584
585 def complete_outputdirs(self, options):
586 """
587 Collect all the pathnames for Tests, and TestGroups. Work
588 backwards one pathname component at a time, to create a unique
589 directory name in which to deposit test output. Tests will be able
590 to write output files directly in the newly modified outputdir.
591 TestGroups will be able to create one subdirectory per test in the
592 outputdir, and are guaranteed uniqueness because a group can only
593 contain files in one directory. Pre and post tests will create a
594 directory rooted at the outputdir of the Test or TestGroup in
595 question for their output.
596 """
597 done = False
598 components = 0
599 tmp_dict = dict(self.tests.items() + self.testgroups.items())
600 total = len(tmp_dict)
601 base = self.outputdir
602
603 while not done:
604 l = []
605 components -= 1
606 for testfile in tmp_dict.keys():
607 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
608 if not uniq in l:
609 l.append(uniq)
610 tmp_dict[testfile].outputdir = os.path.join(base, uniq)
611 else:
612 break
613 done = total == len(l)
614
615 def setup_logging(self, options):
616 """
617 Two loggers are set up here. The first is for the logfile which
618 will contain one line summarizing the test, including the test
619 name, result, and running time. This logger will also capture the
620 timestamped combined stdout and stderr of each run. The second
621 logger is optional console output, which will contain only the one
622 line summary. The loggers are initialized at two different levels
623 to facilitate segregating the output.
624 """
625 if options.dryrun is True:
626 return
627
628 testlogger = logging.getLogger(__name__)
629 testlogger.setLevel(logging.DEBUG)
630
631 if options.cmd is not 'wrconfig':
632 try:
633 old = os.umask(0)
634 os.makedirs(self.outputdir, mode=0777)
635 os.umask(old)
636 except OSError, e:
637 fail('%s' % e)
638 filename = os.path.join(self.outputdir, 'log')
639
640 logfile = logging.FileHandler(filename)
641 logfile.setLevel(logging.DEBUG)
642 logfilefmt = logging.Formatter('%(message)s')
643 logfile.setFormatter(logfilefmt)
644 testlogger.addHandler(logfile)
645
646 cons = logging.StreamHandler()
647 cons.setLevel(logging.INFO)
648 consfmt = logging.Formatter('%(message)s')
649 cons.setFormatter(consfmt)
650 testlogger.addHandler(cons)
651
652 return testlogger
653
654 def run(self, options):
655 """
656 Walk through all the Tests and TestGroups, calling run().
657 """
658 try:
659 os.chdir(self.outputdir)
660 except OSError:
661 fail('Could not change to directory %s' % self.outputdir)
662 for test in sorted(self.tests.keys()):
663 self.tests[test].run(self.logger, options)
664 for testgroup in sorted(self.testgroups.keys()):
665 self.testgroups[testgroup].run(self.logger, options)
666
667 def summary(self):
668 if Result.total is 0:
669 return
670
671 print '\nResults Summary'
672 for key in Result.runresults.keys():
673 if Result.runresults[key] is not 0:
674 print '%s\t% 4d' % (key, Result.runresults[key])
675
676 m, s = divmod(time() - self.starttime, 60)
677 h, m = divmod(m, 60)
678 print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
679 print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
680 float(Result.total)) * 100)
681 print 'Log directory:\t%s' % self.outputdir
682
683
684 def verify_file(pathname):
685 """
686 Verify that the supplied pathname is an executable regular file.
687 """
688 if os.path.isdir(pathname) or os.path.islink(pathname):
689 return False
690
691 if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
692 return True
693
694 return False
695
696
697 def verify_user(user, logger):
698 """
699 Verify that the specified user exists on this system, and can execute
700 sudo without being prompted for a password.
701 """
702 testcmd = [SUDO, '-n', '-u', user, TRUE]
703 can_sudo = exists = True
704
705 if user in Cmd.verified_users:
706 return True
707
708 try:
709 _ = getpwnam(user)
710 except KeyError:
711 exists = False
712 logger.info("Warning: user '%s' does not exist.", user)
713 return False
714
715 p = Popen(testcmd)
716 p.wait()
717 if p.returncode is not 0:
718 logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
719 return False
720 else:
721 Cmd.verified_users.append(user)
722
723 return True
724
725
726 def find_tests(testrun, options):
727 """
728 For the given list of pathnames, add files as Tests. For directories,
729 if do_groups is True, add the directory as a TestGroup. If False,
730 recursively search for executable files.
731 """
732
733 for p in sorted(options.pathnames):
734 if os.path.isdir(p):
735 for dirname, _, filenames in os.walk(p):
736 if options.do_groups:
737 testrun.addtestgroup(dirname, filenames, options)
738 else:
739 for f in sorted(filenames):
740 testrun.addtest(os.path.join(dirname, f), options)
741 else:
742 testrun.addtest(p, options)
743
744
745 def fail(retstr, ret=1):
746 print '%s: %s' % (argv[0], retstr)
747 exit(ret)
748
749
750 def options_cb(option, opt_str, value, parser):
751 path_options = ['runfile', 'outputdir', 'template']
752
753 if option.dest is 'runfile' and '-w' in parser.rargs or \
754 option.dest is 'template' and '-c' in parser.rargs:
755 fail('-c and -w are mutually exclusive.')
756
757 if opt_str in parser.rargs:
758 fail('%s may only be specified once.' % opt_str)
759
760 if option.dest is 'runfile':
761 parser.values.cmd = 'rdconfig'
762 if option.dest is 'template':
763 parser.values.cmd = 'wrconfig'
764
765 setattr(parser.values, option.dest, value)
766 if option.dest in path_options:
767 setattr(parser.values, option.dest, os.path.abspath(value))
768
769
770 def parse_args():
771 parser = OptionParser()
772 parser.add_option('-c', action='callback', callback=options_cb,
773 type='string', dest='runfile', metavar='runfile',
774 help='Specify tests to run via config file.')
775 parser.add_option('-d', action='store_true', default=False, dest='dryrun',
776 help='Dry run. Print tests, but take no other action.')
777 parser.add_option('-g', action='store_true', default=False,
778 dest='do_groups', help='Make directories TestGroups.')
779 parser.add_option('-o', action='callback', callback=options_cb,
780 default=BASEDIR, dest='outputdir', type='string',
781 metavar='outputdir', help='Specify an output directory.')
782 parser.add_option('-p', action='callback', callback=options_cb,
783 default='', dest='pre', metavar='script',
784 type='string', help='Specify a pre script.')
785 parser.add_option('-P', action='callback', callback=options_cb,
786 default='', dest='post', metavar='script',
787 type='string', help='Specify a post script.')
788 parser.add_option('-q', action='store_true', default=False, dest='quiet',
789 help='Silence on the console during a test run.')
790 parser.add_option('-t', action='callback', callback=options_cb, default=60,
791 dest='timeout', metavar='seconds', type='int',
792 help='Timeout (in seconds) for an individual test.')
793 parser.add_option('-u', action='callback', callback=options_cb,
794 default='', dest='user', metavar='user', type='string',
795 help='Specify a different user name to run as.')
796 parser.add_option('-w', action='callback', callback=options_cb,
797 default=None, dest='template', metavar='template',
798 type='string', help='Create a new config file.')
799 parser.add_option('-x', action='callback', callback=options_cb, default='',
800 dest='pre_user', metavar='pre_user', type='string',
801 help='Specify a user to execute the pre script.')
802 parser.add_option('-X', action='callback', callback=options_cb, default='',
803 dest='post_user', metavar='post_user', type='string',
804 help='Specify a user to execute the post script.')
805 (options, pathnames) = parser.parse_args()
806
807 if not options.runfile and not options.template:
808 options.cmd = 'runtests'
809
810 if options.runfile and len(pathnames):
811 fail('Extraneous arguments.')
812
813 options.pathnames = [os.path.abspath(path) for path in pathnames]
814
815 return options
816
817
818 def main(args):
819 options = parse_args()
820 testrun = TestRun(options)
821
822 if options.cmd is 'runtests':
823 find_tests(testrun, options)
824 elif options.cmd is 'rdconfig':
825 testrun.read(testrun.logger, options)
826 elif options.cmd is 'wrconfig':
827 find_tests(testrun, options)
828 testrun.write(options)
829 exit(0)
830 else:
831 fail('Unknown command specified')
832
833 testrun.complete_outputdirs(options)
834 testrun.run(options)
835 testrun.summary()
836 exit(0)
837
838
839 if __name__ == '__main__':
840 main(argv[1:])