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