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