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