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