From 0ae75733f7e63c7f2c190edb1425c0031262dc71 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 31 Mar 2021 03:24:01 +0100 Subject: Tests: added file descriptor leak detection. --- test/conftest.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 20ac6e81..7b3314e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -56,6 +56,12 @@ def pytest_addoption(parser): type=str, 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, @@ -67,12 +73,23 @@ 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 @@ -257,6 +274,10 @@ def run(request): ] option.skip_sanitizer = False + _fds_check['main']['skip'] = False + _fds_check['router']['skip'] = False + _fds_check['router']['skip'] = False + yield # stop unit @@ -304,6 +325,50 @@ def run(request): 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 + + 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: @@ -371,6 +436,21 @@ def unit_run(): unit_instance['control_sock'] = temp_dir + '/control.unit.sock' unit_instance['unitd'] = unitd + 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 @@ -492,6 +572,32 @@ def _clear_conf(sock, log=None): 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, subprocess.CalledProcessError): + pass + + try: + out = subprocess.check_output( + ['lsof', '-n', '-p', pid], stderr=subprocess.STDOUT, + ).decode() + return len(out.splitlines()) + + except (FileNotFoundError, subprocess.CalledProcessError): + pass + + return 0 + + def run_process(target, *args): global _processes @@ -517,6 +623,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,6 +643,16 @@ 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'] -- cgit From 6c97a1a069f0ae8cf683c8364fb7f9dabc5e89cb Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 5 Apr 2021 14:03:05 +0100 Subject: Tests: style. --- test/conftest.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 7b3314e2..38e1138e 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,11 +14,12 @@ import time from multiprocessing import Process import pytest + 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.utils import public_dir @@ -85,6 +85,7 @@ _fds_check = { } http = TestHTTP() + def pytest_configure(config): option.config = config.option @@ -115,9 +116,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 @@ -216,6 +219,7 @@ def pytest_sessionstart(session): 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): # execute all other hooks to obtain the report object @@ -320,7 +324,9 @@ def run(request): 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) @@ -384,6 +390,7 @@ def run(request): _check_alerts(log=log) + def unit_run(): global unit_instance @@ -482,7 +489,6 @@ def unit_stop(): return 'Could not terminate unit' - def _check_alerts(path=None, log=None): if path is None: path = unit_instance['log'] @@ -554,24 +560,21 @@ 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): @@ -606,6 +609,7 @@ def run_process(target, *args): _processes.append(process) + def stop_processes(): if not _processes: return @@ -657,18 +661,22 @@ def skip_fds_check(): 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') -- cgit From 74b1b1fc17726d805b00dee6b5547254f5cf230c Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 8 Apr 2021 19:11:11 +0300 Subject: Tests: preserving unit.log when run without restart. Introducing "unit.log.Log" class for "unit.log" file management. Moving "findall()" function into TestApplicationProto. Using "os.kill()" to send signals. --- test/conftest.py | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 38e1138e..b9f5c60b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -22,6 +22,7 @@ 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 @@ -71,7 +72,6 @@ def pytest_addoption(parser): unit_instance = {} -unit_log_copy = "unit.log.copy" _processes = [] _fds_check = { 'main': {'fds': 0, 'skip': False}, @@ -165,12 +165,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) @@ -216,9 +215,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): @@ -269,7 +265,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', @@ -291,34 +286,25 @@ 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) @@ -439,10 +425,12 @@ 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() @@ -489,12 +477,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 @@ -526,7 +511,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') @@ -679,7 +664,7 @@ def unit_pid(request): 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 -- cgit From 5b332cae832c9f8245280fa60a6edc2ce48202c6 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 14 Apr 2021 15:56:03 +0100 Subject: Tests: fixed "skip" descriptors check flag for controller. --- test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index b9f5c60b..0eaf54be 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -275,7 +275,7 @@ def run(request): _fds_check['main']['skip'] = False _fds_check['router']['skip'] = False - _fds_check['router']['skip'] = False + _fds_check['controller']['skip'] = False yield -- cgit From e0a061955bba69aaf28022d405362c8a5e444ff6 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 5 May 2021 12:36:57 +0100 Subject: Tests: added tests for openat2() features. --- test/conftest.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 0eaf54be..a084953a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -15,6 +15,7 @@ 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 @@ -204,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') -- cgit From 07c6bf165d0e414da3827c7b2aebf5044a7e6093 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 11 May 2021 15:30:12 +0100 Subject: Tests: temporary dir removed after tests execution. --- test/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index a084953a..c2781571 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -671,4 +671,10 @@ def pytest_sessionfinish(session): option.restart = True unit_stop() + + public_dir(option.cache_dir) shutil.rmtree(option.cache_dir) + + if not option.save_log: + public_dir(option.temp_dir) + shutil.rmtree(option.temp_dir) -- cgit From 1198118b3b987930c508d78d90af909eec1835db Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 17 May 2021 15:39:15 +0100 Subject: Tests: fixed incorrect "--restart" mode performing. --- test/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index c2781571..5ea4e49d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -355,7 +355,9 @@ def run(request): fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) ps['fds'] += fds_diff - assert ps['pid'] == ps_pid, 'same pid %s' % name + if not option.restart: + assert ps['pid'] == ps_pid, 'same pid %s' % name + assert fds_diff <= option.fds_threshold, ( 'descriptors leak %s' % name ) @@ -573,7 +575,7 @@ def _count_fds(pid): ).decode() return len(out.splitlines()) - except (FileNotFoundError, subprocess.CalledProcessError): + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): pass try: @@ -582,7 +584,7 @@ def _count_fds(pid): ).decode() return len(out.splitlines()) - except (FileNotFoundError, subprocess.CalledProcessError): + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): pass return 0 @@ -675,6 +677,6 @@ def pytest_sessionfinish(session): public_dir(option.cache_dir) shutil.rmtree(option.cache_dir) - if not option.save_log: + if not option.save_log and os.path.isdir(option.temp_dir): public_dir(option.temp_dir) shutil.rmtree(option.temp_dir) -- cgit