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