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