1import fcntl 2import inspect 3import json 4import os 5import platform 6import re 7import shutil 8import signal 9import stat 10import subprocess 11import sys 12import tempfile 13import time 14from multiprocessing import Process 15 16import pytest 17from unit.check.chroot import check_chroot 18from unit.check.go import check_go 19from unit.check.isolation import check_isolation 20from unit.check.node import check_node 21from unit.check.regex import check_regex 22from unit.check.tls import check_openssl 23from unit.http import TestHTTP 24from unit.log import Log 25from unit.option import option 26from unit.utils import public_dir 27from unit.utils import waitforfiles 28 29 30def pytest_addoption(parser): 31 parser.addoption( 32 "--detailed", 33 default=False, 34 action="store_true", 35 help="Detailed output for tests", 36 ) 37 parser.addoption( 38 "--print-log", 39 default=False, 40 action="store_true", 41 help="Print unit.log to stdout in case of errors", 42 ) 43 parser.addoption( 44 "--save-log", 45 default=False, 46 action="store_true", 47 help="Save unit.log after the test execution", 48 ) 49 parser.addoption( 50 "--unsafe", 51 default=False, 52 action="store_true", 53 help="Run unsafe tests", 54 ) 55 parser.addoption( 56 "--user", 57 type=str, 58 help="Default user for non-privileged processes of unitd", 59 ) 60 parser.addoption( 61 "--fds-threshold", 62 type=int, 63 default=0, 64 help="File descriptors threshold", 65 ) 66 parser.addoption( 67 "--restart", 68 default=False, 69 action="store_true", 70 help="Force Unit to restart after every test", 71 ) 72 73 74unit_instance = {} 75_processes = [] 76_fds_info = { 77 'main': {'fds': 0, 'skip': False}, 78 'router': {'name': 'unit: router', 'pid': -1, 'fds': 0, 'skip': False}, 79 'controller': { 80 'name': 'unit: controller', 81 'pid': -1, 82 'fds': 0, 83 'skip': False, 84 }, 85} 86http = TestHTTP() 87 88 89def pytest_configure(config): 90 option.config = config.option 91 92 option.detailed = config.option.detailed 93 option.fds_threshold = config.option.fds_threshold 94 option.print_log = config.option.print_log 95 option.save_log = config.option.save_log 96 option.unsafe = config.option.unsafe 97 option.user = config.option.user 98 option.restart = config.option.restart 99 100 option.generated_tests = {} 101 option.current_dir = os.path.abspath( 102 os.path.join(os.path.dirname(__file__), os.pardir) 103 ) 104 option.test_dir = option.current_dir + '/test' 105 option.architecture = platform.architecture()[0] 106 option.system = platform.system() 107 108 option.cache_dir = tempfile.mkdtemp(prefix='unit-test-cache-') 109 public_dir(option.cache_dir) 110 111 # set stdout to non-blocking 112 113 if option.detailed or option.print_log: 114 fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) 115 116 117def print_log_on_assert(func): 118 def inner_function(*args, **kwargs): 119 try: 120 func(*args, **kwargs) 121 except AssertionError as e: 122 _print_log(kwargs.get('log', None)) 123 raise e 124 125 return inner_function 126 127 128def pytest_generate_tests(metafunc): 129 cls = metafunc.cls 130 if ( 131 not hasattr(cls, 'application_type') 132 or cls.application_type == None 133 or cls.application_type == 'external' 134 ): 135 return 136 137 type = cls.application_type 138 139 def generate_tests(versions): 140 metafunc.fixturenames.append('tmp_ct') 141 metafunc.parametrize('tmp_ct', versions) 142 143 for version in versions: 144 option.generated_tests[ 145 metafunc.function.__name__ + '[{}]'.format(version) 146 ] = (type + ' ' + version) 147 148 # take available module from option and generate tests for each version 149 150 for module, prereq_version in cls.prerequisites['modules'].items(): 151 if module in option.available['modules']: 152 available_versions = option.available['modules'][module] 153 154 if prereq_version == 'all': 155 generate_tests(available_versions) 156 157 elif prereq_version == 'any': 158 option.generated_tests[metafunc.function.__name__] = ( 159 type + ' ' + available_versions[0] 160 ) 161 elif callable(prereq_version): 162 generate_tests( 163 list(filter(prereq_version, available_versions)) 164 ) 165 166 else: 167 raise ValueError( 168 """ 169Unexpected prerequisite version "%s" for module "%s" in %s. 170'all', 'any' or callable expected.""" 171 % (str(prereq_version), module, str(cls)) 172 ) 173 174 175def pytest_sessionstart(session): 176 option.available = {'modules': {}, 'features': {}} 177 178 unit = unit_run() 179 180 # read unit.log 181 182 for i in range(50): 183 with open(Log.get_path(), 'r') as f: 184 log = f.read() 185 m = re.search('controller started', log) 186 187 if m is None: 188 time.sleep(0.1) 189 else: 190 break 191 192 if m is None: 193 _print_log(log) 194 exit("Unit is writing log too long") 195 196 # discover available modules from unit.log 197 198 for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): 199 versions = option.available['modules'].setdefault(module[0], []) 200 if module[1] not in versions: 201 versions.append(module[1]) 202 203 # discover modules from check 204 205 option.available['modules']['openssl'] = check_openssl(unit['unitd']) 206 option.available['modules']['go'] = check_go( 207 option.current_dir, unit['temp_dir'], option.test_dir 208 ) 209 option.available['modules']['node'] = check_node(option.current_dir) 210 option.available['modules']['regex'] = check_regex(unit['unitd']) 211 212 # remove None values 213 214 option.available['modules'] = { 215 k: v for k, v in option.available['modules'].items() if v is not None 216 } 217 218 check_chroot() 219 check_isolation() 220 221 _clear_conf(unit['temp_dir'] + '/control.unit.sock') 222 223 unit_stop() 224 225 _check_alerts() 226 227 if option.restart: 228 shutil.rmtree(unit_instance['temp_dir']) 229 230 231@pytest.hookimpl(tryfirst=True, hookwrapper=True) 232def pytest_runtest_makereport(item, call): 233 # execute all other hooks to obtain the report object 234 outcome = yield 235 rep = outcome.get_result() 236 237 # set a report attribute for each phase of a call, which can 238 # be "setup", "call", "teardown" 239 240 setattr(item, "rep_" + rep.when, rep) 241 242 243@pytest.fixture(scope='class', autouse=True) 244def check_prerequisites(request): 245 cls = request.cls 246 missed = [] 247 248 # check modules 249 250 if 'modules' in cls.prerequisites: 251 available_modules = list(option.available['modules'].keys()) 252 253 for module in cls.prerequisites['modules']: 254 if module in available_modules: 255 continue 256 257 missed.append(module) 258 259 if missed: 260 pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)') 261 262 # check features 263 264 if 'features' in cls.prerequisites: 265 available_features = list(option.available['features'].keys()) 266 267 for feature in cls.prerequisites['features']: 268 if feature in available_features: 269 continue 270 271 missed.append(feature) 272 273 if missed: 274 pytest.skip(', '.join(missed) + ' feature(s) not supported') 275 276 277@pytest.fixture(autouse=True) 278def run(request): 279 unit = unit_run() 280 281 option.skip_alerts = [ 282 r'read signalfd\(4\) failed', 283 r'sendmsg.+failed', 284 r'recvmsg.+failed', 285 ] 286 option.skip_sanitizer = False 287 288 _fds_info['main']['skip'] = False 289 _fds_info['router']['skip'] = False 290 _fds_info['controller']['skip'] = False 291 292 yield 293 294 # stop unit 295 296 error_stop_unit = unit_stop() 297 error_stop_processes = stop_processes() 298 299 # prepare log 300 301 with Log.open(encoding='utf-8') as f: 302 log = f.read() 303 Log.set_pos(f.tell()) 304 305 if not option.save_log and option.restart: 306 shutil.rmtree(unit['temp_dir']) 307 Log.set_pos(0) 308 309 # clean temp_dir before the next test 310 311 if not option.restart: 312 _clear_conf(unit['temp_dir'] + '/control.unit.sock', log=log) 313 314 for item in os.listdir(unit['temp_dir']): 315 if item not in [ 316 'control.unit.sock', 317 'state', 318 'unit.pid', 319 'unit.log', 320 ]: 321 path = os.path.join(unit['temp_dir'], item) 322 323 public_dir(path) 324 325 if os.path.isfile(path) or stat.S_ISSOCK( 326 os.stat(path).st_mode 327 ): 328 os.remove(path) 329 else: 330 for attempt in range(10): 331 try: 332 shutil.rmtree(path) 333 break 334 except OSError as err: 335 if err.errno != 16: 336 raise 337 time.sleep(1) 338 339 # check descriptors 340 341 _check_fds(log=log) 342 343 # print unit.log in case of error 344 345 if hasattr(request.node, 'rep_call') and request.node.rep_call.failed: 346 _print_log(log) 347 348 if error_stop_unit or error_stop_processes: 349 _print_log(log) 350 351 # check unit.log for errors 352 353 assert error_stop_unit is None, 'stop unit' 354 assert error_stop_processes is None, 'stop processes' 355 356 _check_alerts(log=log) 357 358 359def unit_run(): 360 global unit_instance 361 362 if not option.restart and 'unitd' in unit_instance: 363 return unit_instance 364 365 build_dir = option.current_dir + '/build' 366 unitd = build_dir + '/unitd' 367 368 if not os.path.isfile(unitd): 369 exit('Could not find unit') 370 371 temp_dir = tempfile.mkdtemp(prefix='unit-test-') 372 public_dir(temp_dir) 373 374 if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': 375 public_dir(build_dir) 376 377 os.mkdir(temp_dir + '/state') 378 379 unitd_args = [ 380 unitd, 381 '--no-daemon', 382 '--modules', 383 build_dir, 384 '--state', 385 temp_dir + '/state', 386 '--pid', 387 temp_dir + '/unit.pid', 388 '--log', 389 temp_dir + '/unit.log', 390 '--control', 391 'unix:' + temp_dir + '/control.unit.sock', 392 '--tmp', 393 temp_dir, 394 ] 395 396 if option.user: 397 unitd_args.extend(['--user', option.user]) 398 399 with open(temp_dir + '/unit.log', 'w') as log: 400 unit_instance['process'] = subprocess.Popen(unitd_args, stderr=log) 401 402 Log.temp_dir = temp_dir 403 404 if not waitforfiles(temp_dir + '/control.unit.sock'): 405 _print_log() 406 exit('Could not start unit') 407 408 unit_instance['temp_dir'] = temp_dir 409 unit_instance['control_sock'] = temp_dir + '/control.unit.sock' 410 unit_instance['unitd'] = unitd 411 412 option.temp_dir = temp_dir 413 414 with open(temp_dir + '/unit.pid', 'r') as f: 415 unit_instance['pid'] = f.read().rstrip() 416 417 _clear_conf(unit_instance['temp_dir'] + '/control.unit.sock') 418 419 _fds_info['main']['fds'] = _count_fds(unit_instance['pid']) 420 421 router = _fds_info['router'] 422 router['pid'] = pid_by_name(router['name']) 423 router['fds'] = _count_fds(router['pid']) 424 425 controller = _fds_info['controller'] 426 controller['pid'] = pid_by_name(controller['name']) 427 controller['fds'] = _count_fds(controller['pid']) 428 429 return unit_instance 430 431 432def unit_stop(): 433 if not option.restart: 434 if inspect.stack()[1].function.startswith('test_'): 435 pytest.skip('no restart mode') 436 437 return 438 439 p = unit_instance['process'] 440 441 if p.poll() is not None: 442 return 443 444 p.send_signal(signal.SIGQUIT) 445 446 try: 447 retcode = p.wait(15) 448 if retcode: 449 return 'Child process terminated with code ' + str(retcode) 450 451 except KeyboardInterrupt: 452 p.kill() 453 raise 454 455 except: 456 p.kill() 457 return 'Could not terminate unit' 458 459 460@print_log_on_assert 461def _check_alerts(*, log=None): 462 if log is None: 463 with Log.open(encoding='utf-8') as f: 464 log = f.read() 465 466 found = False 467 468 alerts = re.findall(r'.+\[alert\].+', log) 469 470 if alerts: 471 print('\nAll alerts/sanitizer errors found in log:') 472 [print(alert) for alert in alerts] 473 found = True 474 475 if option.skip_alerts: 476 for skip in option.skip_alerts: 477 alerts = [al for al in alerts if re.search(skip, al) is None] 478 479 assert not alerts, 'alert(s)' 480 481 if not option.skip_sanitizer: 482 sanitizer_errors = re.findall('.+Sanitizer.+', log) 483 484 assert not sanitizer_errors, 'sanitizer error(s)' 485 486 if found: 487 print('skipped.') 488 489 490def _print_log(log=None): 491 path = Log.get_path() 492 493 print('Path to unit.log:\n' + path + '\n') 494 495 if option.print_log: 496 os.set_blocking(sys.stdout.fileno(), True) 497 sys.stdout.flush() 498 499 if log is None: 500 with open(path, 'r', encoding='utf-8', errors='ignore') as f: 501 shutil.copyfileobj(f, sys.stdout) 502 else: 503 sys.stdout.write(log) 504 505 506@print_log_on_assert 507def _clear_conf(sock, *, log=None): 508 resp = http.put( 509 url='/config', 510 sock_type='unix', 511 addr=sock, 512 body=json.dumps({"listeners": {}, "applications": {}}), 513 )['body'] 514 515 assert 'success' in resp, 'clear conf' 516 517 if 'openssl' not in option.available['modules']: 518 return 519 520 try: 521 certs = json.loads( 522 http.get(url='/certificates', sock_type='unix', addr=sock,)['body'] 523 ).keys() 524 525 except json.JSONDecodeError: 526 pytest.fail('Can\'t parse certificates list.') 527 528 for cert in certs: 529 resp = http.delete( 530 url='/certificates/' + cert, sock_type='unix', addr=sock, 531 )['body'] 532 533 assert 'success' in resp, 'remove certificate' 534 535 536@print_log_on_assert 537def _check_fds(*, log=None): 538 def waitforfds(diff): 539 for i in range(600): 540 fds_diff = diff() 541 542 if fds_diff <= option.fds_threshold: 543 break 544 545 time.sleep(0.1) 546 547 return fds_diff 548 549 ps = _fds_info['main'] 550 if not ps['skip']: 551 fds_diff = waitforfds( 552 lambda: _count_fds(unit_instance['pid']) - ps['fds'] 553 ) 554 ps['fds'] += fds_diff 555 556 assert ( 557 fds_diff <= option.fds_threshold 558 ), 'descriptors leak main process' 559 560 else: 561 ps['fds'] = _count_fds(unit_instance['pid']) 562 563 for name in ['controller', 'router']: 564 ps = _fds_info[name] 565 ps_pid = ps['pid'] 566 ps['pid'] = pid_by_name(ps['name']) 567 568 if not ps['skip']: 569 fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) 570 ps['fds'] += fds_diff 571 572 if not option.restart: 573 assert ps['pid'] == ps_pid, 'same pid %s' % name 574 575 assert fds_diff <= option.fds_threshold, ( 576 'descriptors leak %s' % name 577 ) 578 579 else: 580 ps['fds'] = _count_fds(ps['pid']) 581 582 583def _count_fds(pid): 584 procfile = '/proc/%s/fd' % pid 585 if os.path.isdir(procfile): 586 return len(os.listdir(procfile)) 587 588 try: 589 out = subprocess.check_output( 590 ['procstat', '-f', pid], stderr=subprocess.STDOUT, 591 ).decode() 592 return len(out.splitlines()) 593 594 except (FileNotFoundError, TypeError, subprocess.CalledProcessError): 595 pass 596 597 try: 598 out = subprocess.check_output( 599 ['lsof', '-n', '-p', pid], stderr=subprocess.STDOUT, 600 ).decode() 601 return len(out.splitlines()) 602 603 except (FileNotFoundError, TypeError, subprocess.CalledProcessError): 604 pass 605 606 return 0 607 608 609def run_process(target, *args): 610 global _processes 611 612 process = Process(target=target, args=args) 613 process.start() 614 615 _processes.append(process) 616 617 618def stop_processes(): 619 if not _processes: 620 return 621 622 fail = False 623 for process in _processes: 624 if process.is_alive(): 625 process.terminate() 626 process.join(timeout=15) 627 628 if process.is_alive(): 629 fail = True 630 631 if fail: 632 return 'Fail to stop process(es)' 633 634 635def pid_by_name(name): 636 output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() 637 m = re.search( 638 r'\s*(\d+)\s*' + str(unit_instance['pid']) + r'.*' + name, output 639 ) 640 return None if m is None else m.group(1) 641 642 643def find_proc(name, ps_output): 644 return re.findall(str(unit_instance['pid']) + r'.*' + name, ps_output) 645 646 647@pytest.fixture() 648def skip_alert(): 649 def _skip(*alerts): 650 option.skip_alerts.extend(alerts) 651 652 return _skip 653 654 655@pytest.fixture() 656def skip_fds_check(): 657 def _skip(main=False, router=False, controller=False): 658 _fds_info['main']['skip'] = main 659 _fds_info['router']['skip'] = router 660 _fds_info['controller']['skip'] = controller 661 662 return _skip 663 664 665@pytest.fixture 666def temp_dir(request): 667 return unit_instance['temp_dir'] 668 669 670@pytest.fixture 671def is_unsafe(request): 672 return request.config.getoption("--unsafe") 673 674 675@pytest.fixture 676def is_su(request): 677 return os.geteuid() == 0 678 679 680@pytest.fixture 681def unit_pid(request): 682 return unit_instance['process'].pid 683 684 685def pytest_sessionfinish(session): 686 if not option.restart and option.save_log: 687 print('Path to unit.log:\n' + Log.get_path() + '\n') 688 689 option.restart = True 690 691 unit_stop() 692 693 public_dir(option.cache_dir) 694 shutil.rmtree(option.cache_dir) 695 696 if not option.save_log and os.path.isdir(option.temp_dir): 697 public_dir(option.temp_dir) 698 shutil.rmtree(option.temp_dir) 699