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