From d5e915934066c77a59d211efafca10c117b73d05 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 16 Sep 2020 21:31:15 +0100 Subject: Tests: migrated to the pytest. --- test/conftest.py | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 test/conftest.py (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..8683a023 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,299 @@ +import fcntl +import os +import platform +import pytest +import signal +import stat +import subprocess +import sys +import re +import tempfile +import time + + +def pytest_addoption(parser): + parser.addoption( + "--detailed", + default=False, + action="store_true", + help="Detailed output for tests", + ) + parser.addoption( + "--print_log", + default=False, + action="store_true", + help="Print unit.log to stdout in case of errors", + ) + parser.addoption( + "--save_log", + default=False, + action="store_true", + help="Save unit.log after the test execution", + ) + parser.addoption( + "--unsafe", + default=False, + action="store_true", + help="Run unsafe tests", + ) + + +unit_instance = {} +option = None + + +def pytest_configure(config): + global option + option = config.option + + option.generated_tests = {} + option.current_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) + ) + option.test_dir = option.current_dir + '/test' + option.architecture = platform.architecture()[0] + option.system = platform.system() + + # set stdout to non-blocking + + if option.detailed or option.print_log: + fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) + + +def pytest_generate_tests(metafunc): + cls = metafunc.cls + if not hasattr(cls, 'application_type'): + return + + type = cls.application_type + + # take available module from option and generate tests for each version + + for module in cls.prerequisites['modules']: + if module in option.available['modules']: + prereq_version = cls.prerequisites['modules'][module] + available_versions = option.available['modules'][module] + + if prereq_version == 'all': + metafunc.fixturenames.append('tmp_ct') + metafunc.parametrize('tmp_ct', range(len(available_versions))) + + for i in range(len(available_versions)): + version = available_versions[i] + option.generated_tests[ + metafunc.function.__name__ + '[{}]'.format(i) + ] = (type + ' ' + version) + elif prereq_version == 'any': + option.generated_tests[metafunc.function.__name__] = ( + type + ' ' + available_versions[0] + ) + else: + for version in available_versions: + if version.startswith(prereq_version): + option.generated_tests[metafunc.function.__name__] = ( + type + ' ' + version + ) + + +def pytest_sessionstart(session): + option.available = {'modules': {}, 'features': {}} + + unit = unit_run() + + # read unit.log + + for i in range(50): + with open(unit['temp_dir'] + '/unit.log', 'r') as f: + log = f.read() + m = re.search('controller started', log) + + if m is None: + time.sleep(0.1) + else: + break + + if m is None: + _print_log() + exit("Unit is writing log too long") + + # discover available modules from unit.log + + for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): + if module[0] not in option.available['modules']: + option.available['modules'][module[0]] = [module[1]] + else: + option.available['modules'][module[0]].append(module[1]) + + unit_stop() + + +def setup_method(self): + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + +def unit_run(): + global unit_instance + build_dir = option.current_dir + '/build' + unitd = build_dir + '/unitd' + + if not os.path.isfile(unitd): + exit('Could not find unit') + + temp_dir = tempfile.mkdtemp(prefix='unit-test-') + public_dir(temp_dir) + + if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': + public_dir(build_dir) + + os.mkdir(temp_dir + '/state') + + with open(temp_dir + '/unit.log', 'w') as log: + unit_instance['process'] = subprocess.Popen( + [ + unitd, + '--no-daemon', + '--modules', + build_dir, + '--state', + temp_dir + '/state', + '--pid', + temp_dir + '/unit.pid', + '--log', + temp_dir + '/unit.log', + '--control', + 'unix:' + temp_dir + '/control.unit.sock', + '--tmp', + temp_dir, + ], + stderr=log, + ) + + if not waitforfiles(temp_dir + '/control.unit.sock'): + _print_log() + exit('Could not start unit') + + # dumb (TODO: remove) + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + + 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 + + return unit_instance + + +def unit_stop(): + p = unit_instance['process'] + + if p.poll() is not None: + return + + p.send_signal(signal.SIGQUIT) + + try: + retcode = p.wait(15) + if retcode: + return 'Child process terminated with code ' + str(retcode) + except: + p.kill() + return 'Could not terminate unit' + + +def public_dir(path): + os.chmod(path, 0o777) + + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), 0o777) + for f in files: + os.chmod(os.path.join(root, f), 0o777) + +def waitforfiles(*files): + for i in range(50): + wait = False + ret = False + + for f in files: + if not os.path.exists(f): + wait = True + break + + if wait: + time.sleep(0.1) + + else: + ret = True + break + + return ret + + +def skip_alert(*alerts): + option.skip_alerts.extend(alerts) + + +def _check_alerts(log): + found = False + + alerts = re.findall(r'.+\[alert\].+', log) + + if alerts: + print('All alerts/sanitizer errors found in log:') + [print(alert) for alert in alerts] + found = True + + if option.skip_alerts: + for skip in option.skip_alerts: + alerts = [al for al in alerts if re.search(skip, al) is None] + + if alerts: + _print_log(log) + assert not alerts, 'alert(s)' + + if not option.skip_sanitizer: + sanitizer_errors = re.findall('.+Sanitizer.+', log) + + if sanitizer_errors: + _print_log(log) + assert not sanitizer_errors, 'sanitizer error(s)' + + if found: + print('skipped.') + + +def _print_log(data=None): + unit_log = unit_instance['log'] + + print('Path to unit.log:\n' + unit_log + '\n') + + if option.print_log: + os.set_blocking(sys.stdout.fileno(), True) + sys.stdout.flush() + + if data is None: + with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: + shutil.copyfileobj(f, sys.stdout) + else: + sys.stdout.write(data) + + +@pytest.fixture +def is_unsafe(request): + return request.config.getoption("--unsafe") + +@pytest.fixture +def is_su(request): + return os.geteuid() == 0 + +def pytest_sessionfinish(session): + unit_stop() -- cgit From efe65dee4de9d429a3cd0d7e88e4999a8c89081a Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 24 Sep 2020 09:47:27 +0300 Subject: Tests: prerequisites check improved by using callable. This is required for more flexible Python version check since ASGI works for Python 3.5+. Version check via 'startswith()' function removed as not consistent. --- 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 8683a023..6bc871e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -67,32 +67,40 @@ def pytest_generate_tests(metafunc): type = cls.application_type + def generate_tests(versions): + metafunc.fixturenames.append('tmp_ct') + metafunc.parametrize('tmp_ct', range(len(versions))) + + for i, version in enumerate(versions): + option.generated_tests[ + metafunc.function.__name__ + '[{}]'.format(i) + ] = (type + ' ' + version) + # take available module from option and generate tests for each version - for module in cls.prerequisites['modules']: + for module, prereq_version in cls.prerequisites['modules'].items(): if module in option.available['modules']: - prereq_version = cls.prerequisites['modules'][module] available_versions = option.available['modules'][module] if prereq_version == 'all': - metafunc.fixturenames.append('tmp_ct') - metafunc.parametrize('tmp_ct', range(len(available_versions))) - - for i in range(len(available_versions)): - version = available_versions[i] - option.generated_tests[ - metafunc.function.__name__ + '[{}]'.format(i) - ] = (type + ' ' + version) + generate_tests(available_versions) + elif prereq_version == 'any': option.generated_tests[metafunc.function.__name__] = ( type + ' ' + available_versions[0] ) + elif callable(prereq_version): + generate_tests( + list(filter(prereq_version, available_versions)) + ) + else: - for version in available_versions: - if version.startswith(prereq_version): - option.generated_tests[metafunc.function.__name__] = ( - type + ' ' + version - ) + raise ValueError( + """ +Unexpected prerequisite version "%s" for module "%s" in %s. +'all', 'any' or callable expected.""" + % (str(prereq_version), module, str(cls)) + ) def pytest_sessionstart(session): -- cgit From d491527555c076695a4202577198e12bf0b919ec Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Thu, 1 Oct 2020 10:17:00 +0100 Subject: Tests: minor fixes. Fixed temporary dir removing. Fixed printing path to log. Module checks moved to the separate file. --- test/conftest.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 6bc871e2..8e9009b6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,6 +2,7 @@ import fcntl import os import platform import pytest +import shutil import signal import stat import subprocess @@ -10,6 +11,10 @@ import re import tempfile import time +from unit.check.go import check_go +from unit.check.node import check_node +from unit.check.tls import check_openssl + def pytest_addoption(parser): parser.addoption( @@ -132,6 +137,20 @@ def pytest_sessionstart(session): else: option.available['modules'][module[0]].append(module[1]) + # discover modules from check + + option.available['modules']['openssl'] = check_openssl(unit['unitd']) + option.available['modules']['go'] = check_go( + option.current_dir, unit['temp_dir'], option.test_dir + ) + option.available['modules']['node'] = check_node(option.current_dir) + + # remove None values + + option.available['modules'] = { + k: v for k, v in option.available['modules'].items() if v is not None + } + unit_stop() @@ -216,6 +235,7 @@ def unit_stop(): p.kill() return 'Could not terminate unit' + shutil.rmtree(unit_instance['temp_dir']) def public_dir(path): os.chmod(path, 0o777) @@ -265,31 +285,32 @@ def _check_alerts(log): alerts = [al for al in alerts if re.search(skip, al) is None] if alerts: - _print_log(log) + _print_log(data=log) assert not alerts, 'alert(s)' if not option.skip_sanitizer: sanitizer_errors = re.findall('.+Sanitizer.+', log) if sanitizer_errors: - _print_log(log) + _print_log(data=log) assert not sanitizer_errors, 'sanitizer error(s)' if found: print('skipped.') -def _print_log(data=None): - unit_log = unit_instance['log'] +def _print_log(path=None, data=None): + if path is None: + path = unit_instance['log'] - print('Path to unit.log:\n' + unit_log + '\n') + print('Path to unit.log:\n' + path + '\n') if option.print_log: os.set_blocking(sys.stdout.fileno(), True) sys.stdout.flush() if data is None: - with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: + with open(path, 'r', encoding='utf-8', errors='ignore') as f: shutil.copyfileobj(f, sys.stdout) else: sys.stdout.write(data) -- cgit From 481e950b8696740397eeda0d3c01a2dda2239143 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 5 Oct 2020 13:26:35 +0300 Subject: Tests: pretty versions output for multi-version tests. --- test/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 8e9009b6..0bc5cdf9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -74,11 +74,11 @@ def pytest_generate_tests(metafunc): def generate_tests(versions): metafunc.fixturenames.append('tmp_ct') - metafunc.parametrize('tmp_ct', range(len(versions))) + metafunc.parametrize('tmp_ct', versions) - for i, version in enumerate(versions): + for version in versions: option.generated_tests[ - metafunc.function.__name__ + '[{}]'.format(i) + metafunc.function.__name__ + '[{}]'.format(version) ] = (type + ' ' + version) # take available module from option and generate tests for each version -- cgit From 6ec0ff35964c7805712d978625949f72ff5a63bc Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 7 Oct 2020 23:18:43 +0100 Subject: Tests: minor fixes. --- test/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'test/conftest.py') diff --git a/test/conftest.py b/test/conftest.py index 0bc5cdf9..b62264ca 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,16 +1,17 @@ import fcntl import os import platform -import pytest +import re import shutil import signal import stat import subprocess import sys -import re import tempfile import time +import pytest + from unit.check.go import check_go from unit.check.node import check_node from unit.check.tls import check_openssl -- cgit