diff options
Diffstat (limited to 'test/conftest.py')
-rw-r--r-- | test/conftest.py | 213 |
1 files changed, 172 insertions, 41 deletions
diff --git a/test/conftest.py b/test/conftest.py index 20ac6e81..5ea4e49d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,7 +6,6 @@ import platform import re import shutil import signal -import socket import stat import subprocess import sys @@ -15,13 +14,16 @@ import time from multiprocessing import Process import pytest + +from unit.check.chroot import check_chroot from unit.check.go import check_go from unit.check.isolation import check_isolation from unit.check.node import check_node -from unit.check.tls import check_openssl from unit.check.regex import check_regex +from unit.check.tls import check_openssl from unit.http import TestHTTP from unit.option import option +from unit.log import Log from unit.utils import public_dir from unit.utils import waitforfiles @@ -57,6 +59,12 @@ def pytest_addoption(parser): help="Default user for non-privileged processes of unitd", ) parser.addoption( + "--fds-threshold", + type=int, + default=0, + help="File descriptors threshold", + ) + parser.addoption( "--restart", default=False, action="store_true", @@ -65,14 +73,25 @@ def pytest_addoption(parser): unit_instance = {} -unit_log_copy = "unit.log.copy" _processes = [] +_fds_check = { + 'main': {'fds': 0, 'skip': False}, + 'router': {'name': 'unit: router', 'pid': -1, 'fds': 0, 'skip': False}, + 'controller': { + 'name': 'unit: controller', + 'pid': -1, + 'fds': 0, + 'skip': False, + }, +} http = TestHTTP() + def pytest_configure(config): option.config = config.option option.detailed = config.option.detailed + option.fds_threshold = config.option.fds_threshold option.print_log = config.option.print_log option.save_log = config.option.save_log option.unsafe = config.option.unsafe @@ -98,9 +117,11 @@ def pytest_configure(config): def pytest_generate_tests(metafunc): cls = metafunc.cls - if (not hasattr(cls, 'application_type') - or cls.application_type == None - or cls.application_type == 'external'): + if ( + not hasattr(cls, 'application_type') + or cls.application_type == None + or cls.application_type == 'external' + ): return type = cls.application_type @@ -145,12 +166,11 @@ def pytest_sessionstart(session): option.available = {'modules': {}, 'features': {}} unit = unit_run() - option.temp_dir = unit['temp_dir'] # read unit.log for i in range(50): - with open(unit['temp_dir'] + '/unit.log', 'r') as f: + with open(Log.get_path(), 'r') as f: log = f.read() m = re.search('controller started', log) @@ -185,6 +205,7 @@ def pytest_sessionstart(session): k: v for k, v in option.available['modules'].items() if v is not None } + check_chroot() check_isolation() _clear_conf(unit['temp_dir'] + '/control.unit.sock') @@ -196,8 +217,6 @@ def pytest_sessionstart(session): if option.restart: shutil.rmtree(unit_instance['temp_dir']) - elif option.save_log: - open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'w').close() @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -248,7 +267,6 @@ def check_prerequisites(request): @pytest.fixture(autouse=True) def run(request): unit = unit_run() - option.temp_dir = unit['temp_dir'] option.skip_alerts = [ r'read signalfd\(4\) failed', @@ -257,6 +275,10 @@ def run(request): ] option.skip_sanitizer = False + _fds_check['main']['skip'] = False + _fds_check['router']['skip'] = False + _fds_check['controller']['skip'] = False + yield # stop unit @@ -266,44 +288,83 @@ def run(request): # prepare log - with open( - unit_instance['log'], 'r', encoding='utf-8', errors='ignore' - ) as f: + with Log.open(encoding='utf-8') as f: log = f.read() - - if not option.restart and option.save_log: - with open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'a') as f: - f.write(log) - - # remove unit.log + Log.set_pos(f.tell()) if not option.save_log and option.restart: shutil.rmtree(unit['temp_dir']) + Log.set_pos(0) # clean temp_dir before the next test if not option.restart: _clear_conf(unit['temp_dir'] + '/control.unit.sock', log) - open(unit['log'], 'w').close() - for item in os.listdir(unit['temp_dir']): if item not in [ 'control.unit.sock', 'state', 'unit.pid', 'unit.log', - unit_log_copy, ]: path = os.path.join(unit['temp_dir'], item) public_dir(path) - if os.path.isfile(path) or stat.S_ISSOCK(os.stat(path).st_mode): + if os.path.isfile(path) or stat.S_ISSOCK( + os.stat(path).st_mode + ): os.remove(path) else: shutil.rmtree(path) + # check descriptors (wait for some time before check) + + def waitforfds(diff): + for i in range(600): + fds_diff = diff() + + if fds_diff <= option.fds_threshold: + break + + time.sleep(0.1) + + return fds_diff + + ps = _fds_check['main'] + if not ps['skip']: + fds_diff = waitforfds( + lambda: _count_fds(unit_instance['pid']) - ps['fds'] + ) + ps['fds'] += fds_diff + + assert ( + fds_diff <= option.fds_threshold + ), 'descriptors leak main process' + + else: + ps['fds'] = _count_fds(unit_instance['pid']) + + for name in ['controller', 'router']: + ps = _fds_check[name] + ps_pid = ps['pid'] + ps['pid'] = pid_by_name(ps['name']) + + if not ps['skip']: + fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) + ps['fds'] += fds_diff + + if not option.restart: + assert ps['pid'] == ps_pid, 'same pid %s' % name + + assert fds_diff <= option.fds_threshold, ( + 'descriptors leak %s' % name + ) + + else: + ps['fds'] = _count_fds(ps['pid']) + # print unit.log in case of error if hasattr(request.node, 'rep_call') and request.node.rep_call.failed: @@ -319,6 +380,7 @@ def run(request): _check_alerts(log=log) + def unit_run(): global unit_instance @@ -367,10 +429,27 @@ def unit_run(): exit('Could not start unit') unit_instance['temp_dir'] = temp_dir - unit_instance['log'] = temp_dir + '/unit.log' unit_instance['control_sock'] = temp_dir + '/control.unit.sock' unit_instance['unitd'] = unitd + option.temp_dir = temp_dir + Log.temp_dir = temp_dir + + with open(temp_dir + '/unit.pid', 'r') as f: + unit_instance['pid'] = f.read().rstrip() + + _clear_conf(unit_instance['temp_dir'] + '/control.unit.sock') + + _fds_check['main']['fds'] = _count_fds(unit_instance['pid']) + + router = _fds_check['router'] + router['pid'] = pid_by_name(router['name']) + router['fds'] = _count_fds(router['pid']) + + controller = _fds_check['controller'] + controller['pid'] = pid_by_name(controller['name']) + controller['fds'] = _count_fds(controller['pid']) + return unit_instance @@ -402,13 +481,9 @@ def unit_stop(): return 'Could not terminate unit' - -def _check_alerts(path=None, log=None): - if path is None: - path = unit_instance['log'] - +def _check_alerts(log=None): if log is None: - with open(path, 'r', encoding='utf-8', errors='ignore') as f: + with Log.open(encoding='utf-8') as f: log = f.read() found = False @@ -440,7 +515,7 @@ def _check_alerts(path=None, log=None): def _print_log(data=None): - path = unit_instance['log'] + path = Log.get_path() print('Path to unit.log:\n' + path + '\n') @@ -474,24 +549,47 @@ def _clear_conf(sock, log=None): return try: - certs = json.loads(http.get( - url='/certificates', - sock_type='unix', - addr=sock, - )['body']).keys() + certs = json.loads( + http.get(url='/certificates', sock_type='unix', addr=sock,)['body'] + ).keys() except json.JSONDecodeError: pytest.fail('Can\'t parse certificates list.') for cert in certs: resp = http.delete( - url='/certificates/' + cert, - sock_type='unix', - addr=sock, + url='/certificates/' + cert, sock_type='unix', addr=sock, )['body'] check_success(resp) + +def _count_fds(pid): + procfile = '/proc/%s/fd' % pid + if os.path.isdir(procfile): + return len(os.listdir(procfile)) + + try: + out = subprocess.check_output( + ['procstat', '-f', pid], stderr=subprocess.STDOUT, + ).decode() + return len(out.splitlines()) + + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): + pass + + try: + out = subprocess.check_output( + ['lsof', '-n', '-p', pid], stderr=subprocess.STDOUT, + ).decode() + return len(out.splitlines()) + + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): + pass + + return 0 + + def run_process(target, *args): global _processes @@ -500,6 +598,7 @@ def run_process(target, *args): _processes.append(process) + def stop_processes(): if not _processes: return @@ -517,6 +616,18 @@ def stop_processes(): return 'Fail to stop process(es)' +def pid_by_name(name): + output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() + m = re.search( + r'\s*(\d+)\s*' + str(unit_instance['pid']) + r'.*' + name, output + ) + return None if m is None else m.group(1) + + +def find_proc(name, ps_output): + return re.findall(str(unit_instance['pid']) + r'.*' + name, ps_output) + + @pytest.fixture() def skip_alert(): def _skip(*alerts): @@ -525,27 +636,47 @@ def skip_alert(): return _skip +@pytest.fixture() +def skip_fds_check(): + def _skip(main=False, router=False, controller=False): + _fds_check['main']['skip'] = main + _fds_check['router']['skip'] = router + _fds_check['controller']['skip'] = controller + + return _skip + + @pytest.fixture def temp_dir(request): return unit_instance['temp_dir'] + @pytest.fixture def is_unsafe(request): return request.config.getoption("--unsafe") + @pytest.fixture def is_su(request): return os.geteuid() == 0 + @pytest.fixture def unit_pid(request): return unit_instance['process'].pid + def pytest_sessionfinish(session): if not option.restart and option.save_log: - print('Path to unit.log:\n' + unit_instance['log'] + '\n') + print('Path to unit.log:\n' + Log.get_path() + '\n') option.restart = True unit_stop() + + public_dir(option.cache_dir) shutil.rmtree(option.cache_dir) + + if not option.save_log and os.path.isdir(option.temp_dir): + public_dir(option.temp_dir) + shutil.rmtree(option.temp_dir) |