1#!@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, 2016 by Delphix. All rights reserved. 16# Copyright (c) 2017, Chris Fraire <cfraire@me.com>. 17# Copyright 2019 Joyent, Inc. 18# Copyright 2020 OmniOS Community Edition (OmniOSce) Association. 19# Copyright 2026 Gordon W. Ross 20# 21 22from __future__ import print_function 23import sys 24PY3 = sys.version_info[0] == 3 25 26if PY3: 27 import configparser 28else: 29 import ConfigParser as configparser 30 31import io 32import os 33import logging 34import platform 35import re 36from logging.handlers import WatchedFileHandler 37from datetime import datetime 38from optparse import OptionParser 39from pwd import getpwnam 40from pwd import getpwuid 41from select import select 42from subprocess import PIPE 43from subprocess import Popen 44from sys import argv 45from sys import exit 46from sys import maxsize 47from threading import Timer 48from time import time 49 50BASEDIR = '/var/tmp/test_results' 51TESTDIR = '/opt/zfs-tests/' 52KILL = '/usr/bin/kill' 53TRUE = '/usr/bin/true' 54SUDO = '/usr/bin/sudo' 55 56# Exit codes as defined in stf/include/stf.shlib 57STF_PASS = 0 58STF_FAIL = 1 59STF_UNRESOLVED = 2 60STF_NOTINUSE = 3 61STF_UNSUPPORTED = 4 62STF_UNTESTED = 5 63STF_UNINITIATED = 6 64STF_NORESULT = 7 65STF_WARNING = 8 66STF_TIMED_OUT = 9 67STF_ABORTED = 10 68STF_OTHER = 11 69 70retcode = 0 71 72# Custom class to reopen the log file in case it is forcibly closed by a test. 73class WatchedFileHandlerClosed(WatchedFileHandler): 74 """Watch files, including closed files. 75 Similar to (and inherits from) logging.handler.WatchedFileHandler, 76 except that IOErrors are handled by reopening the stream and retrying. 77 This will be retried up to a configurable number of times before 78 giving up, default 5. 79 """ 80 81 def __init__(self, filename, mode='a', encoding='utf-8', delay=0, max_tries=5): 82 self.max_tries = max_tries 83 self.tries = 0 84 WatchedFileHandler.__init__(self, filename, mode, encoding, delay) 85 86 def emit(self, record): 87 while True: 88 try: 89 WatchedFileHandler.emit(self, record) 90 self.tries = 0 91 return 92 except IOError as err: 93 if self.tries == self.max_tries: 94 raise 95 self.stream.close() 96 self.stream = self._open() 97 self.tries += 1 98 99 100class RunAborted(Exception): 101 pass 102 103 104class Result(object): 105 total = 0 106 runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0} 107 108 def __init__(self): 109 self.starttime = None 110 self.returncode = None 111 self.runtime = '' 112 self.stdout = [] 113 self.stderr = [] 114 self.result = '' 115 116 def done(self, proc, killed): 117 """ 118 Finalize the results of this Cmd, mapping exit codes to a 119 result code, one of: (PASS, FAIL, SKIP, KILLED, ABORTED) 120 and increment one of the four runresults[] counters. 121 """ 122 global retcode 123 124 Result.total += 1 125 m, s = divmod(time() - self.starttime, 60) 126 self.runtime = '%02d:%02d' % (m, s) 127 self.returncode = proc.returncode 128 if killed: 129 self.result = 'KILLED' 130 Result.runresults['KILLED'] += 1 131 retcode = 2; 132 elif self.returncode == 0: 133 self.result = 'PASS' 134 Result.runresults['PASS'] += 1 135 elif self.returncode in (STF_NOTINUSE, STF_UNSUPPORTED): 136 self.result = 'SKIP' 137 Result.runresults['SKIP'] += 1 138 elif self.returncode == STF_ABORTED: 139 self.result = 'ABORTED' 140 Result.runresults['FAIL'] += 1 141 retcode = 1 142 elif self.returncode != 0: 143 self.result = 'FAIL' 144 Result.runresults['FAIL'] += 1 145 retcode = 1; 146 147 148class Output(object): 149 """ 150 This class is a slightly modified version of the 'Stream' class found 151 here: http://goo.gl/aSGfv 152 """ 153 def __init__(self, stream): 154 self.stream = stream 155 self._buf = '' 156 self.lines = [] 157 158 def fileno(self): 159 return self.stream.fileno() 160 161 def read(self, drain=0): 162 """ 163 Read from the file descriptor. If 'drain' set, read until EOF. 164 """ 165 while self._read() is not None: 166 if not drain: 167 break 168 169 def _read(self): 170 """ 171 Read up to 4k of data from this output stream. Collect the output 172 up to the last newline, and append it to any leftover data from a 173 previous call. The lines are stored as a (timestamp, data) tuple 174 for easy sorting/merging later. 175 """ 176 fd = self.fileno() 177 buf = os.read(fd, 4096).decode('utf-8', errors='ignore') 178 if not buf: 179 return None 180 if '\n' not in buf: 181 self._buf += buf 182 return [] 183 184 buf = self._buf + buf 185 tmp, rest = buf.rsplit('\n', 1) 186 self._buf = rest 187 now = datetime.now() 188 rows = tmp.split('\n') 189 self.lines += [(now, r) for r in rows] 190 191 192class Cmd(object): 193 verified_users = [] 194 195 def __init__(self, pathname, identifier=None, outputdir=None, 196 timeout=None, user=None, tags=None): 197 self.pathname = pathname 198 self.identifier = identifier 199 self.outputdir = outputdir or 'BASEDIR' 200 self.timeout = timeout 201 self.user = user or '' 202 self.killed = False 203 self.result = Result() 204 205 if self.timeout is None: 206 self.timeout = 60 207 208 def __str__(self): 209 return '''\ 210Pathname: %s 211Identifier: %s 212Outputdir: %s 213Timeout: %d 214User: %s 215''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user) 216 217 def ensure_outputdir(self): 218 try: 219 old = os.umask(0) 220 if not os.path.isdir(self.outputdir): 221 os.makedirs(self.outputdir, mode=0o777) 222 os.umask(old) 223 except OSError as e: 224 fail('%s' % e) 225 226 def kill_cmd(self, proc): 227 """ 228 Kill a running command due to timeout, or ^C from the keyboard. If 229 sudo is required, this user was verified previously. We kill the 230 entire process group so that SIGTERM reaches the test script and 231 its children (e.g. when launched via sudo), allowing EXIT traps 232 and cleanup functions to run. 233 """ 234 self.killed = True 235 do_sudo = len(self.user) != 0 236 signal = '-TERM' 237 238 # Negative PID sends signal to the entire process group. 239 cmd = [SUDO, KILL, signal, '-%s' % str(proc.pid)] 240 if not do_sudo: 241 del cmd[0] 242 243 try: 244 kp = Popen(cmd) 245 kp.wait() 246 except: 247 pass 248 249 def update_cmd_privs(self, cmd, user): 250 """ 251 If a user has been specified to run this Cmd and we're not already 252 running as that user, prepend the appropriate sudo command to run 253 as that user. 254 """ 255 me = getpwuid(os.getuid()) 256 257 if not user or user == me.pw_name: 258 return cmd 259 260 ret = '%s -E -u %s %s' % (SUDO, user, cmd) 261 return ret.split(' ') 262 263 def collect_output(self, proc): 264 """ 265 Read from stdout/stderr as data becomes available, until the 266 process is no longer running. Return the lines from the stdout and 267 stderr Output objects. 268 """ 269 out = Output(proc.stdout) 270 err = Output(proc.stderr) 271 res = [] 272 while proc.returncode is None: 273 proc.poll() 274 res = select([out, err], [], [], .1) 275 for fd in res[0]: 276 fd.read() 277 for fd in res[0]: 278 fd.read(drain=1) 279 280 return out.lines, err.lines 281 282 def run(self, options): 283 """ 284 This is the main function that runs each individual test. 285 Determine whether or not the command requires sudo, and modify it 286 if needed. Run the command, and update the result object. 287 """ 288 if options.dryrun is True: 289 print(self) 290 return 291 292 privcmd = self.update_cmd_privs(self.pathname, self.user) 293 self.ensure_outputdir() 294 295 try: 296 self.result.starttime = time() 297 proc = Popen(privcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, 298 universal_newlines=True, start_new_session=True) 299 proc.stdin.close() 300 301 # Allow a special timeout value of 0 to mean infinity 302 if int(self.timeout) == 0: 303 self.timeout = maxsize 304 t = Timer(int(self.timeout), self.kill_cmd, [proc]) 305 t.start() 306 self.result.stdout, self.result.stderr = self.collect_output(proc) 307 except KeyboardInterrupt: 308 self.kill_cmd(proc) 309 raise 310 finally: 311 t.cancel() 312 313 self.result.done(proc, self.killed) 314 315 def skip(self): 316 """ 317 Initialize enough of the test result that we can log a skipped 318 command. 319 """ 320 Result.total += 1 321 Result.runresults['SKIP'] += 1 322 self.result.stdout = self.result.stderr = [] 323 self.result.starttime = time() 324 m, s = divmod(time() - self.result.starttime, 60) 325 self.result.runtime = '%02d:%02d' % (m, s) 326 self.result.result = 'SKIP' 327 328 def not_found(self): 329 """ 330 Initialize enough of the test result that we can log a test whose 331 file was not found on disk. Reported as a failure. 332 """ 333 global retcode 334 self.ensure_outputdir() 335 Result.total += 1 336 Result.runresults['FAIL'] += 1 337 retcode = 1 338 self.result.stdout = [] 339 self.result.stderr = [(datetime.now(), 340 'Test file not found: %s' % self.pathname)] 341 self.result.starttime = time() 342 m, s = divmod(time() - self.result.starttime, 60) 343 self.result.runtime = '%02d:%02d' % (m, s) 344 self.result.result = 'FAIL' 345 346 def log(self, logger, options): 347 """ 348 This function is responsible for writing all output. This includes 349 the console output, the logfile of all results (with timestamped 350 merged stdout and stderr), and for each test, the unmodified 351 stdout/stderr/merged in it's own file. 352 """ 353 if logger is None: 354 return 355 356 logname = getpwuid(os.getuid()).pw_name 357 user = ' (run as %s)' % (self.user if len(self.user) else logname) 358 if self.identifier: 359 msga = 'Test (%s): %s%s ' % (self.identifier, self.pathname, user) 360 else: 361 msga = 'Test: %s%s ' % (self.pathname, user) 362 msgb = '[%s] [%s]' % (self.result.runtime, self.result.result) 363 pad = ' ' * (80 - (len(msga) + len(msgb))) 364 365 # If -q is specified, only print a line for tests that didn't pass. 366 # This means passing tests need to be logged as DEBUG, or the one 367 # line summary will only be printed in the logfile for failures. 368 if not options.quiet: 369 logger.info('%s%s%s' % (msga, pad, msgb)) 370 elif self.result.result != 'PASS': 371 logger.info('%s%s%s' % (msga, pad, msgb)) 372 else: 373 logger.debug('%s%s%s' % (msga, pad, msgb)) 374 375 lines = sorted(self.result.stdout + self.result.stderr, 376 key=lambda x: x[0]) 377 378 for dt, line in lines: 379 logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line)) 380 381 if len(self.result.stdout): 382 with io.open(os.path.join(self.outputdir, 'stdout'), 383 encoding='utf-8', 384 errors='surrogateescape', 385 mode='w') as out: 386 for _, line in self.result.stdout: 387 out.write('%s\n' % line) 388 if len(self.result.stderr): 389 with io.open(os.path.join(self.outputdir, 'stderr'), 390 encoding='utf-8', 391 errors='surrogateescape', 392 mode='w') as err: 393 for _, line in self.result.stderr: 394 err.write('%s\n' % line) 395 if len(self.result.stdout) and len(self.result.stderr): 396 with io.open(os.path.join(self.outputdir, 'merged'), 397 encoding='utf-8', 398 errors='surrogateescape', 399 mode='w') as merged: 400 for _, line in lines: 401 merged.write('%s\n' % line) 402 403 404class Test(Cmd): 405 props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post', 406 'post_user', 'tags'] 407 408 def __init__(self, pathname, 409 pre=None, pre_user=None, post=None, post_user=None, 410 tags=None, **kwargs): 411 super(Test, self).__init__(pathname, **kwargs) 412 self.pre = pre or '' 413 self.pre_user = pre_user or '' 414 self.post = post or '' 415 self.post_user = post_user or '' 416 self.tags = tags or [] 417 418 def __str__(self): 419 post_user = pre_user = '' 420 if len(self.pre_user): 421 pre_user = ' (as %s)' % (self.pre_user) 422 if len(self.post_user): 423 post_user = ' (as %s)' % (self.post_user) 424 return '''\ 425Pathname: %s 426Identifier: %s 427Outputdir: %s 428Timeout: %d 429User: %s 430Pre: %s%s 431Post: %s%s 432Tags: %s 433''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user, 434 self.pre, pre_user, self.post, post_user, self.tags) 435 436 def verify(self, logger): 437 """ 438 Check the pre/post scripts, user and Test. Omit the Test from this 439 run if there are any problems. 440 """ 441 files = [self.pre, self.pathname, self.post] 442 users = [self.pre_user, self.user, self.post_user] 443 444 for f in [f for f in files if len(f)]: 445 if not verify_file(f): 446 logger.info("Warning: Test '%s' not added to this run because" 447 " it failed verification." % f) 448 return False 449 450 for user in [user for user in users if len(user)]: 451 if not verify_user(user, logger): 452 logger.info("Not adding Test '%s' to this run." % 453 self.pathname) 454 return False 455 456 return True 457 458 def run(self, logger, options): 459 """ 460 Create Cmd instances for the pre/post scripts. If the pre script 461 doesn't pass, skip this Test. Run the post script regardless. 462 """ 463 odir = os.path.join(self.outputdir, os.path.basename(self.pre)) 464 pretest = Cmd(self.pre, identifier=self.identifier, outputdir=odir, 465 timeout=self.timeout, user=self.pre_user) 466 test = Cmd(self.pathname, identifier=self.identifier, 467 outputdir=self.outputdir, timeout=self.timeout, 468 user=self.user) 469 odir = os.path.join(self.outputdir, os.path.basename(self.post)) 470 posttest = Cmd(self.post, identifier=self.identifier, outputdir=odir, 471 timeout=self.timeout, user=self.post_user) 472 473 cont = True 474 if len(pretest.pathname): 475 pretest.run(options) 476 cont = pretest.result.result == 'PASS' 477 pretest.log(logger, options) 478 479 if cont: 480 test.run(options) 481 else: 482 test.skip() 483 484 test.log(logger, options) 485 if test.result.result == 'ABORTED': 486 raise RunAborted('test %s' % test.pathname) 487 488 if len(posttest.pathname): 489 logger.debug('Running post cleanup: %s' % posttest.pathname) 490 posttest.run(options) 491 posttest.log(logger, options) 492 if posttest.result.result == 'ABORTED': 493 raise RunAborted( 494 'post cleanup for %s' % self.pathname) 495 496 497class TestGroup(Test): 498 props = Test.props + ['tests'] 499 500 def __init__(self, pathname, tests=None, **kwargs): 501 super(TestGroup, self).__init__(pathname, **kwargs) 502 self.tests = tests or [] 503 self.missing_tests = set() 504 505 def __str__(self): 506 post_user = pre_user = '' 507 if len(self.pre_user): 508 pre_user = ' (as %s)' % (self.pre_user) 509 if len(self.post_user): 510 post_user = ' (as %s)' % (self.post_user) 511 return '''\ 512Pathname: %s 513Identifier: %s 514Outputdir: %s 515Tests: %s 516Timeout: %d 517User: %s 518Pre: %s%s 519Post: %s%s 520Tags: %s 521''' % (self.pathname, self.identifier, self.outputdir, self.tests, 522 self.timeout, self.user, self.pre, pre_user, self.post, post_user, 523 self.tags) 524 525 def filter(self, keeplist): 526 self.tests = [ x for x in self.tests if x in keeplist ] 527 528 def verify(self, logger): 529 """ 530 Check the pre/post scripts, user and tests in this TestGroup. Omit 531 the TestGroup entirely, or simply delete the relevant tests in the 532 group, if that's all that's required. 533 """ 534 # If the pre or post scripts are relative pathnames, convert to 535 # absolute, so they stand a chance of passing verification. 536 if len(self.pre) and not os.path.isabs(self.pre): 537 self.pre = os.path.join(self.pathname, self.pre) 538 if len(self.post) and not os.path.isabs(self.post): 539 self.post = os.path.join(self.pathname, self.post) 540 541 auxfiles = [self.pre, self.post] 542 users = [self.pre_user, self.user, self.post_user] 543 544 for f in [f for f in auxfiles if len(f)]: 545 if self.pathname != os.path.dirname(f): 546 logger.info("Warning: TestGroup '%s' not added to this run. " 547 "Auxiliary script '%s' exists in a different " 548 "directory." % (self.pathname, f)) 549 return False 550 551 if not verify_file(f): 552 logger.info("Warning: TestGroup '%s' not added to this run. " 553 "Auxiliary script '%s' failed verification." % 554 (self.pathname, f)) 555 return False 556 557 for user in [user for user in users if len(user)]: 558 if not verify_user(user, logger): 559 logger.info("Not adding TestGroup '%s' to this run." % 560 self.pathname) 561 return False 562 563 # If one of the tests is invalid, record it as missing so it can be 564 # reported as a failure, rather than silently dropping it. 565 self.missing_tests = set(f for f in self.tests 566 if not verify_file(os.path.join(self.pathname, 567 f))) 568 569 return len(self.tests) != 0 570 571 def run(self, logger, options): 572 """ 573 Create Cmd instances for the pre/post scripts. If the pre script 574 doesn't pass, skip all the tests in this TestGroup. Run the post 575 script regardless. 576 """ 577 # tags assigned to this test group also include the test names 578 if options.tags and not set(self.tags).intersection(set(options.tags)): 579 return 580 581 odir = os.path.join(self.outputdir, os.path.basename(self.pre)) 582 pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout, 583 user=self.pre_user, identifier=self.identifier) 584 odir = os.path.join(self.outputdir, os.path.basename(self.post)) 585 posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout, 586 user=self.post_user, identifier=self.identifier) 587 588 cont = True 589 if len(pretest.pathname): 590 pretest.run(options) 591 cont = pretest.result.result == 'PASS' 592 pretest.log(logger, options) 593 594 try: 595 for fname in self.tests: 596 test = Cmd(os.path.join(self.pathname, fname), 597 outputdir=os.path.join(self.outputdir, fname), 598 timeout=self.timeout, user=self.user, 599 identifier=self.identifier) 600 if fname in self.missing_tests: 601 test.not_found() 602 elif cont: 603 test.run(options) 604 else: 605 test.skip() 606 607 test.log(logger, options) 608 if test.result.result == 'ABORTED': 609 raise RunAborted('test %s' % test.pathname) 610 except KeyboardInterrupt: 611 # Run the post (cleanup) script even when interrupted, in case 612 # the test's own EXIT trap did not clean up (e.g. pools, zinject). 613 if len(posttest.pathname): 614 print('\nInterrupted, running cleanup...', 615 flush=True) 616 posttest.run(options) 617 posttest.log(logger, options) 618 raise 619 620 if len(posttest.pathname): 621 logger.debug('Running post cleanup: %s' % posttest.pathname) 622 posttest.run(options) 623 posttest.log(logger, options) 624 if posttest.result.result == 'ABORTED': 625 raise RunAborted( 626 'post cleanup for %s' % self.pathname) 627 628 629class TestRun(object): 630 props = ['quiet', 'outputdir'] 631 632 def __init__(self, options): 633 self.tests = {} 634 self.testgroups = {} 635 self.starttime = time() 636 self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S') 637 self.outputdir = os.path.join(options.outputdir, self.timestamp) 638 self.logger = self.setup_logging(options) 639 self.defaults = [ 640 ('outputdir', BASEDIR), 641 ('quiet', False), 642 ('timeout', 60), 643 ('user', ''), 644 ('pre', ''), 645 ('pre_user', ''), 646 ('post', ''), 647 ('post_user', ''), 648 ('tags', []) 649 ] 650 651 def __str__(self): 652 s = 'TestRun:\n outputdir: %s\n' % self.outputdir 653 s += 'TESTS:\n' 654 for key in sorted(self.tests.keys()): 655 s += '%s%s' % (self.tests[key].__str__(), '\n') 656 s += 'TESTGROUPS:\n' 657 for key in sorted(self.testgroups.keys()): 658 s += '%s%s' % (self.testgroups[key].__str__(), '\n') 659 return s 660 661 def addtest(self, pathname, options): 662 """ 663 Create a new Test, and apply any properties that were passed in 664 from the command line. If it passes verification, add it to the 665 TestRun. 666 """ 667 test = Test(pathname) 668 for prop in Test.props: 669 setattr(test, prop, getattr(options, prop)) 670 671 if test.verify(self.logger): 672 self.tests[pathname] = test 673 674 def addtestgroup(self, dirname, filenames, options): 675 """ 676 Create a new TestGroup, and apply any properties that were passed 677 in from the command line. If it passes verification, add it to the 678 TestRun. 679 """ 680 if dirname not in self.testgroups: 681 testgroup = TestGroup(dirname) 682 for prop in Test.props: 683 setattr(testgroup, prop, getattr(options, prop)) 684 685 # Prevent pre/post scripts from running as regular tests 686 for f in [testgroup.pre, testgroup.post]: 687 if f in filenames: 688 del filenames[filenames.index(f)] 689 690 self.testgroups[dirname] = testgroup 691 self.testgroups[dirname].tests = sorted(filenames) 692 693 testgroup.verify(self.logger) 694 695 def filter(self, keeplist): 696 for group in list(self.testgroups.keys()): 697 if group not in keeplist: 698 del self.testgroups[group] 699 continue 700 701 g = self.testgroups[group] 702 703 if g.pre and os.path.basename(g.pre) in keeplist[group]: 704 continue 705 706 g.filter(keeplist[group]) 707 708 for test in list(self.tests.keys()): 709 directory, base = os.path.split(test) 710 if directory not in keeplist or base not in keeplist[directory]: 711 del self.tests[test] 712 713 def read(self, logger, options): 714 """ 715 Read in the specified runfile, and apply the TestRun properties 716 listed in the 'DEFAULT' section to our TestRun. Then read each 717 section, and apply the appropriate properties to the Test or 718 TestGroup. Properties from individual sections override those set 719 in the 'DEFAULT' section. If the Test or TestGroup passes 720 verification, add it to the TestRun. 721 """ 722 config = configparser.RawConfigParser() 723 parsed = config.read(options.runfiles) 724 failed = options.runfiles - set(parsed) 725 if len(failed): 726 files = ' '.join(sorted(failed)) 727 fail("Couldn't read config files: %s" % files) 728 729 for opt in TestRun.props: 730 if config.has_option('DEFAULT', opt): 731 setattr(self, opt, config.get('DEFAULT', opt)) 732 self.outputdir = os.path.join(self.outputdir, self.timestamp) 733 734 testdir = options.testdir 735 736 for section in config.sections(): 737 if ('arch' in config.options(section) and 738 platform.machine() != config.get(section, 'arch')): 739 continue 740 741 parts = section.split(':', 1) 742 sectiondir = parts[0] 743 identifier = parts[1] if len(parts) == 2 else None 744 if os.path.isdir(sectiondir): 745 pathname = sectiondir 746 elif os.path.isdir(os.path.join(testdir, sectiondir)): 747 pathname = os.path.join(testdir, sectiondir) 748 else: 749 pathname = sectiondir 750 751 testgroup = TestGroup(os.path.abspath(pathname), 752 identifier=identifier) 753 if 'tests' in config.options(section): 754 for prop in TestGroup.props: 755 for sect in ['DEFAULT', section]: 756 if config.has_option(sect, prop): 757 if prop == 'tags': 758 setattr(testgroup, prop, 759 eval(config.get(sect, prop))) 760 else: 761 setattr(testgroup, prop, 762 config.get(sect, prop)) 763 764 # Repopulate tests using eval to convert the string to a list 765 testgroup.tests = eval(config.get(section, 'tests')) 766 767 if testgroup.verify(logger): 768 self.testgroups[section] = testgroup 769 770 elif 'autotests' in config.options(section): 771 for prop in TestGroup.props: 772 for sect in ['DEFAULT', section]: 773 if config.has_option(sect, prop): 774 setattr(testgroup, prop, config.get(sect, prop)) 775 776 filenames = os.listdir(pathname) 777 # only files starting with "tst." are considered tests 778 filenames = [f for f in filenames if f.startswith("tst.")] 779 testgroup.tests = sorted(filenames) 780 781 if testgroup.verify(logger): 782 self.testgroups[section] = testgroup 783 else: 784 test = Test(section) 785 for prop in Test.props: 786 for sect in ['DEFAULT', section]: 787 if config.has_option(sect, prop): 788 setattr(test, prop, config.get(sect, prop)) 789 790 if test.verify(logger): 791 self.tests[section] = test 792 793 def write(self, options): 794 """ 795 Create a configuration file for editing and later use. The 796 'DEFAULT' section of the config file is created from the 797 properties that were specified on the command line. Tests are 798 simply added as sections that inherit everything from the 799 'DEFAULT' section. TestGroups are the same, except they get an 800 option including all the tests to run in that directory. 801 """ 802 803 defaults = dict([(prop, getattr(options, prop)) for prop, _ in 804 self.defaults]) 805 config = configparser.RawConfigParser(defaults) 806 807 for test in sorted(self.tests.keys()): 808 config.add_section(test) 809 for prop in Test.props: 810 if prop not in self.props: 811 config.set(testgroup, prop, 812 getattr(self.testgroups[testgroup], prop)) 813 814 for testgroup in sorted(self.testgroups.keys()): 815 config.add_section(testgroup) 816 config.set(testgroup, 'tests', self.testgroups[testgroup].tests) 817 for prop in TestGroup.props: 818 if prop not in self.props: 819 config.set(testgroup, prop, 820 getattr(self.testgroups[testgroup], prop)) 821 822 try: 823 with open(options.template, 'w') as f: 824 return config.write(f) 825 except IOError: 826 fail('Could not open \'%s\' for writing.' % options.template) 827 828 def complete_outputdirs(self): 829 """ 830 Collect all the pathnames for Tests, and TestGroups. Work 831 backwards one pathname component at a time, to create a unique 832 directory name in which to deposit test output. Tests will be able 833 to write output files directly in the newly modified outputdir. 834 TestGroups will be able to create one subdirectory per test in the 835 outputdir, and are guaranteed uniqueness because a group can only 836 contain files in one directory. Pre and post tests will create a 837 directory rooted at the outputdir of the Test or TestGroup in 838 question for their output. 839 """ 840 done = False 841 components = 0 842 tmp_dict = dict(list(self.tests.items()) + list(self.testgroups.items())) 843 total = len(tmp_dict) 844 base = self.outputdir 845 846 while not done: 847 l = [] 848 components -= 1 849 for testfile in list(tmp_dict.keys()): 850 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/') 851 if uniq not in l: 852 l.append(uniq) 853 tmp_dict[testfile].outputdir = os.path.join(base, uniq) 854 else: 855 break 856 done = total == len(l) 857 858 def setup_logging(self, options): 859 """ 860 Two loggers are set up here. The first is for the logfile which 861 will contain one line summarizing the test, including the test 862 name, result, and running time. This logger will also capture the 863 timestamped combined stdout and stderr of each run. The second 864 logger is optional console output, which will contain only the one 865 line summary. The loggers are initialized at two different levels 866 to facilitate segregating the output. 867 """ 868 if options.dryrun is True: 869 return 870 871 testlogger = logging.getLogger(__name__) 872 testlogger.setLevel(logging.DEBUG) 873 874 if not options.template: 875 try: 876 old = os.umask(0) 877 os.makedirs(self.outputdir, mode=0o777) 878 os.umask(old) 879 except OSError as e: 880 fail('%s' % e) 881 filename = os.path.join(self.outputdir, 'log') 882 883 logfile = WatchedFileHandlerClosed(filename) 884 logfile.setLevel(logging.DEBUG) 885 logfilefmt = logging.Formatter('%(message)s') 886 logfile.setFormatter(logfilefmt) 887 testlogger.addHandler(logfile) 888 889 cons = logging.StreamHandler() 890 cons.setLevel(logging.INFO) 891 consfmt = logging.Formatter('%(message)s') 892 cons.setFormatter(consfmt) 893 testlogger.addHandler(cons) 894 895 return testlogger 896 897 def run(self, options): 898 """ 899 Walk through all the Tests and TestGroups, calling run(). 900 """ 901 if not options.dryrun: 902 try: 903 os.chdir(self.outputdir) 904 except OSError: 905 fail('Could not change to directory %s' % self.outputdir) 906 907 uname = Popen(['uname', '-a'], stdout=PIPE, stderr=PIPE, 908 universal_newlines=True) 909 out, _ = uname.communicate() 910 self.logger.debug('Test system: %s' % out.strip()) 911 912 for test in sorted(self.tests.keys()): 913 self.tests[test].run(self.logger, options) 914 try: 915 for testgroup in sorted(self.testgroups.keys()): 916 self.testgroups[testgroup].run(self.logger, options) 917 except KeyboardInterrupt: 918 fail('\nRun terminated at user request.') 919 except RunAborted as e: 920 fail('\nRun aborted: %s' % e) 921 922 def summary(self): 923 if Result.total == 0: 924 print('No tests to run') 925 return 926 927 print('\nResults Summary') 928 for key in list(Result.runresults.keys()): 929 if Result.runresults[key] != 0: 930 print('%s\t% 4d' % (key, Result.runresults[key])) 931 932 m, s = divmod(time() - self.starttime, 60) 933 h, m = divmod(m, 60) 934 print('\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)) 935 print('Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) / 936 float(Result.total)) * 100)) 937 print('Log directory:\t%s' % self.outputdir) 938 939 940def verify_file(pathname): 941 """ 942 Verify that the supplied pathname is an executable regular file. 943 """ 944 if os.path.isdir(pathname) or os.path.islink(pathname): 945 return False 946 947 if os.path.isfile(pathname) and os.access(pathname, os.X_OK): 948 return True 949 950 return False 951 952 953def verify_user(user, logger): 954 """ 955 Verify that the specified user exists on this system, and can execute 956 sudo without being prompted for a password. 957 """ 958 testcmd = [SUDO, '-n', '-u', user, TRUE] 959 960 if user in Cmd.verified_users: 961 return True 962 963 try: 964 _ = getpwnam(user) 965 except KeyError: 966 logger.info("Warning: user '%s' does not exist.", user) 967 return False 968 969 p = Popen(testcmd) 970 p.wait() 971 if p.returncode != 0: 972 logger.info("Warning: user '%s' cannot use passwordless sudo.", user) 973 return False 974 else: 975 Cmd.verified_users.append(user) 976 977 return True 978 979 980def find_tests(testrun, options): 981 """ 982 For the given list of pathnames, add files as Tests. For directories, 983 if do_groups is True, add the directory as a TestGroup. If False, 984 recursively search for executable files. 985 """ 986 987 for p in sorted(options.pathnames): 988 if os.path.isdir(p): 989 for dirname, _, filenames in os.walk(p): 990 if options.do_groups: 991 testrun.addtestgroup(dirname, filenames, options) 992 else: 993 for f in sorted(filenames): 994 testrun.addtest(os.path.join(dirname, f), options) 995 else: 996 testrun.addtest(p, options) 997 998 999def filter_tests(testrun, options): 1000 try: 1001 fh = open(options.logfile, "r", errors='replace') 1002 except Exception as e: 1003 fail('%s' % e) 1004 1005 failed = {} 1006 while True: 1007 line = fh.readline() 1008 if not line: 1009 break 1010 m = re.match(r'Test: (.*)/(\S+).*\[FAIL\]', line) 1011 if not m: 1012 continue 1013 group, test = m.group(1, 2) 1014 m = re.match(re.escape(options.testdir) + r'(.*)', group) 1015 if m: 1016 group = m.group(1) 1017 try: 1018 failed[group].append(test) 1019 except KeyError: 1020 failed[group] = [ test ] 1021 fh.close() 1022 1023 testrun.filter(failed) 1024 1025 1026def fail(retstr, ret=1): 1027 print('%s: %s' % (argv[0], retstr)) 1028 exit(ret) 1029 1030 1031def options_cb(option, opt_str, value, parser): 1032 path_options = ['outputdir', 'template', 'testdir', 'logfile'] 1033 1034 if opt_str in parser.rargs: 1035 fail('%s may only be specified once.' % opt_str) 1036 1037 if option.dest == 'runfiles': 1038 parser.values.cmd = 'rdconfig' 1039 value = set(os.path.abspath(p) for p in value.split(',')) 1040 if option.dest == 'tags': 1041 value = [x.strip() for x in value.split(',')] 1042 1043 if option.dest in path_options: 1044 setattr(parser.values, option.dest, os.path.abspath(value)) 1045 else: 1046 setattr(parser.values, option.dest, value) 1047 1048 1049def parse_args(): 1050 parser = OptionParser() 1051 parser.add_option('-c', action='callback', callback=options_cb, 1052 type='string', dest='runfiles', metavar='runfiles', 1053 help='Specify tests to run via config files.') 1054 parser.add_option('-d', action='store_true', default=False, dest='dryrun', 1055 help='Dry run. Print tests, but take no other action.') 1056 parser.add_option('-l', action='callback', callback=options_cb, 1057 default=None, dest='logfile', metavar='logfile', 1058 type='string', 1059 help='Read logfile and re-run tests which failed.') 1060 parser.add_option('-g', action='store_true', default=False, 1061 dest='do_groups', help='Make directories TestGroups.') 1062 parser.add_option('-o', action='callback', callback=options_cb, 1063 default=BASEDIR, dest='outputdir', type='string', 1064 metavar='outputdir', help='Specify an output directory.') 1065 parser.add_option('-i', action='callback', callback=options_cb, 1066 default=TESTDIR, dest='testdir', type='string', 1067 metavar='testdir', help='Specify a test directory.') 1068 parser.add_option('-p', action='callback', callback=options_cb, 1069 default='', dest='pre', metavar='script', 1070 type='string', help='Specify a pre script.') 1071 parser.add_option('-P', action='callback', callback=options_cb, 1072 default='', dest='post', metavar='script', 1073 type='string', help='Specify a post script.') 1074 parser.add_option('-q', action='store_true', default=False, dest='quiet', 1075 help='Silence on the console during a test run.') 1076 parser.add_option('-t', action='callback', callback=options_cb, default=60, 1077 dest='timeout', metavar='seconds', type='int', 1078 help='Timeout (in seconds) for an individual test.') 1079 parser.add_option('-u', action='callback', callback=options_cb, 1080 default='', dest='user', metavar='user', type='string', 1081 help='Specify a different user name to run as.') 1082 parser.add_option('-w', action='callback', callback=options_cb, 1083 default=None, dest='template', metavar='template', 1084 type='string', help='Create a new config file.') 1085 parser.add_option('-x', action='callback', callback=options_cb, default='', 1086 dest='pre_user', metavar='pre_user', type='string', 1087 help='Specify a user to execute the pre script.') 1088 parser.add_option('-X', action='callback', callback=options_cb, default='', 1089 dest='post_user', metavar='post_user', type='string', 1090 help='Specify a user to execute the post script.') 1091 parser.add_option('-T', action='callback', callback=options_cb, default='', 1092 dest='tags', metavar='tags', type='string', 1093 help='Specify tags to execute specific test groups.') 1094 (options, pathnames) = parser.parse_args() 1095 1096 if options.runfiles and len(pathnames): 1097 fail('Extraneous arguments.') 1098 1099 options.pathnames = [os.path.abspath(path) for path in pathnames] 1100 1101 return options 1102 1103 1104def main(): 1105 options = parse_args() 1106 1107 testrun = TestRun(options) 1108 1109 if options.runfiles: 1110 testrun.read(testrun.logger, options) 1111 else: 1112 find_tests(testrun, options) 1113 1114 if options.logfile: 1115 filter_tests(testrun, options) 1116 1117 if options.template: 1118 testrun.write(options) 1119 exit(0) 1120 1121 testrun.complete_outputdirs() 1122 testrun.run(options) 1123 testrun.summary() 1124 exit(retcode) 1125 1126 1127if __name__ == '__main__': 1128 main() 1129