Tests: added file descriptor leak detection.

This commit is contained in:
Andrei Zeliankou
2021-03-31 03:24:01 +01:00
parent e8577afc21
commit 0ae75733f7
2 changed files with 132 additions and 2 deletions

View File

@@ -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']