diff options
author | Andrei Zeliankou <zelenkov@nginx.com> | 2021-03-31 03:24:01 +0100 |
---|---|---|
committer | Andrei Zeliankou <zelenkov@nginx.com> | 2021-03-31 03:24:01 +0100 |
commit | 0ae75733f7e63c7f2c190edb1425c0031262dc71 (patch) | |
tree | 1cdb18760f3e4af1982eb924b50bc7c383c8d12d | |
parent | e8577afc2126001db03d4b8ac1dd8670a2504322 (diff) | |
download | unit-0ae75733f7e63c7f2c190edb1425c0031262dc71.tar.gz unit-0ae75733f7e63c7f2c190edb1425c0031262dc71.tar.bz2 |
Tests: added file descriptor leak detection.
-rw-r--r-- | test/conftest.py | 128 | ||||
-rw-r--r-- | test/test_respawn.py | 6 |
2 files changed, 132 insertions, 2 deletions
diff --git a/test/conftest.py b/test/conftest.py index 20ac6e81..7b3314e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -57,6 +57,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", @@ -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'] diff --git a/test/test_respawn.py b/test/test_respawn.py index ed85ee95..50c19186 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -58,7 +58,8 @@ class TestRespawn(TestApplicationPython): assert len(self.find_proc(self.PATTERN_CONTROLLER, unit_pid, out)) == 1 assert len(self.find_proc(self.app_name, unit_pid, out)) == 1 - def test_respawn_router(self, skip_alert, unit_pid): + def test_respawn_router(self, skip_alert, unit_pid, skip_fds_check): + skip_fds_check(router=True) pid = self.pid_by_name(self.PATTERN_ROUTER, unit_pid) self.kill_pids(pid) @@ -68,7 +69,8 @@ class TestRespawn(TestApplicationPython): self.smoke_test(unit_pid) - def test_respawn_controller(self, skip_alert, unit_pid): + def test_respawn_controller(self, skip_alert, unit_pid, skip_fds_check): + skip_fds_check(controller=True) pid = self.pid_by_name(self.PATTERN_CONTROLLER, unit_pid) self.kill_pids(pid) |