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