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