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