diff options
author | Aleksandr <ivansduck@gmail.com> | 2022-02-10 16:47:52 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:47:52 +0300 |
commit | ea6c5b7f172becca389cacaff7d5f45f6adccbe6 (patch) | |
tree | d16cef493ac1e092b4a03ab9437ec06ffe3d188f /library/python/pytest | |
parent | 37de222addabbef336dcaaea5f7c7645a629fc6d (diff) | |
download | ydb-ea6c5b7f172becca389cacaff7d5f45f6adccbe6.tar.gz |
Restoring authorship annotation for Aleksandr <ivansduck@gmail.com>. Commit 1 of 2.
Diffstat (limited to 'library/python/pytest')
-rw-r--r-- | library/python/pytest/main.py | 96 | ||||
-rw-r--r-- | library/python/pytest/plugins/collection.py | 2 | ||||
-rw-r--r-- | library/python/pytest/plugins/fakeid_py2.py | 4 | ||||
-rw-r--r-- | library/python/pytest/plugins/fakeid_py3.py | 4 | ||||
-rw-r--r-- | library/python/pytest/plugins/ya.make | 32 | ||||
-rw-r--r-- | library/python/pytest/plugins/ya.py | 478 | ||||
-rw-r--r-- | library/python/pytest/ya.make | 22 | ||||
-rw-r--r-- | library/python/pytest/yatest_tools.py | 196 |
8 files changed, 417 insertions, 417 deletions
diff --git a/library/python/pytest/main.py b/library/python/pytest/main.py index 6296bd6f0f..aa08f846b1 100644 --- a/library/python/pytest/main.py +++ b/library/python/pytest/main.py @@ -1,47 +1,47 @@ -import os +import os import sys import time - + import __res -FORCE_EXIT_TESTSFAILED_ENV = 'FORCE_EXIT_TESTSFAILED' - +FORCE_EXIT_TESTSFAILED_ENV = 'FORCE_EXIT_TESTSFAILED' + def main(): import library.python.pytest.context as context context.Ctx["YA_PYTEST_START_TIMESTAMP"] = time.time() - profile = None - if '--profile-pytest' in sys.argv: - sys.argv.remove('--profile-pytest') - - import pstats - import cProfile - profile = cProfile.Profile() - profile.enable() + profile = None + if '--profile-pytest' in sys.argv: + sys.argv.remove('--profile-pytest') - # Reset influencing env. vars - # For more info see library/python/testing/yatest_common/yatest/common/errors.py - if FORCE_EXIT_TESTSFAILED_ENV in os.environ: - del os.environ[FORCE_EXIT_TESTSFAILED_ENV] + import pstats + import cProfile + profile = cProfile.Profile() + profile.enable() + # Reset influencing env. vars + # For more info see library/python/testing/yatest_common/yatest/common/errors.py + if FORCE_EXIT_TESTSFAILED_ENV in os.environ: + del os.environ[FORCE_EXIT_TESTSFAILED_ENV] + if "Y_PYTHON_CLEAR_ENTRY_POINT" in os.environ: if "Y_PYTHON_ENTRY_POINT" in os.environ: del os.environ["Y_PYTHON_ENTRY_POINT"] del os.environ["Y_PYTHON_CLEAR_ENTRY_POINT"] - listing_mode = '--collect-only' in sys.argv - yatest_runner = os.environ.get('YA_TEST_RUNNER') == '1' - - import pytest - - import library.python.pytest.plugins.collection as collection - import library.python.pytest.plugins.ya as ya - import library.python.pytest.plugins.conftests as conftests - + listing_mode = '--collect-only' in sys.argv + yatest_runner = os.environ.get('YA_TEST_RUNNER') == '1' + + import pytest + + import library.python.pytest.plugins.collection as collection + import library.python.pytest.plugins.ya as ya + import library.python.pytest.plugins.conftests as conftests + import _pytest.assertion from _pytest.monkeypatch import MonkeyPatch - from . import rewrite + from . import rewrite m = MonkeyPatch() m.setattr(_pytest.assertion.rewrite, "AssertionRewritingHook", rewrite.AssertionRewritingHook) @@ -52,10 +52,10 @@ def main(): if name.startswith(prefix) and not name.endswith('.conftest') ] - doctest_packages = __res.find("PY_DOCTEST_PACKAGES") or "" - if isinstance(doctest_packages, bytes): - doctest_packages = doctest_packages.decode('utf-8') - doctest_packages = doctest_packages.split() + doctest_packages = __res.find("PY_DOCTEST_PACKAGES") or "" + if isinstance(doctest_packages, bytes): + doctest_packages = doctest_packages.decode('utf-8') + doctest_packages = doctest_packages.split() def is_doctest_module(name): for package in doctest_packages: @@ -85,31 +85,31 @@ def main(): return new_paths sys.path = remove_user_site(sys.path) - rc = pytest.main(plugins=[ + rc = pytest.main(plugins=[ collection.CollectionPlugin(test_modules, doctest_modules), ya, conftests, - ]) - + ]) + if rc == 5: # don't care about EXIT_NOTESTSCOLLECTED rc = 0 - if rc == 1 and yatest_runner and not listing_mode and not os.environ.get(FORCE_EXIT_TESTSFAILED_ENV) == '1': - # XXX it's place for future improvements - # Test wrapper should terminate with 0 exit code if there are common test failures - # and report it with trace-file machinery. - # However, there are several case when we don't want to suppress exit_code: - # - listing machinery doesn't use trace-file currently and rely on stdout and exit_code - # - RestartTestException and InfrastructureException required non-zero exit_code to be processes correctly - rc = 0 - - if profile: - profile.disable() - ps = pstats.Stats(profile, stream=sys.stderr).sort_stats('cumulative') - ps.print_stats() - - sys.exit(rc) + if rc == 1 and yatest_runner and not listing_mode and not os.environ.get(FORCE_EXIT_TESTSFAILED_ENV) == '1': + # XXX it's place for future improvements + # Test wrapper should terminate with 0 exit code if there are common test failures + # and report it with trace-file machinery. + # However, there are several case when we don't want to suppress exit_code: + # - listing machinery doesn't use trace-file currently and rely on stdout and exit_code + # - RestartTestException and InfrastructureException required non-zero exit_code to be processes correctly + rc = 0 + + if profile: + profile.disable() + ps = pstats.Stats(profile, stream=sys.stderr).sort_stats('cumulative') + ps.print_stats() + + sys.exit(rc) if __name__ == '__main__': diff --git a/library/python/pytest/plugins/collection.py b/library/python/pytest/plugins/collection.py index e36f47a78f..93932e4b02 100644 --- a/library/python/pytest/plugins/collection.py +++ b/library/python/pytest/plugins/collection.py @@ -89,7 +89,7 @@ def pytest_ignore_collect(module, session, filenames_from_full_filters, accept_f if test_file_filter is None: return False if module.name != test_file_filter.replace('/', '.'): - return True + return True return False diff --git a/library/python/pytest/plugins/fakeid_py2.py b/library/python/pytest/plugins/fakeid_py2.py index 8b26148e2e..8efc368629 100644 --- a/library/python/pytest/plugins/fakeid_py2.py +++ b/library/python/pytest/plugins/fakeid_py2.py @@ -1,2 +1,2 @@ -# Inc this number to change uid for every PYTEST() target -fake_id = 0 +# Inc this number to change uid for every PYTEST() target +fake_id = 0 diff --git a/library/python/pytest/plugins/fakeid_py3.py b/library/python/pytest/plugins/fakeid_py3.py index 247cc8b29d..d6812eadba 100644 --- a/library/python/pytest/plugins/fakeid_py3.py +++ b/library/python/pytest/plugins/fakeid_py3.py @@ -1,2 +1,2 @@ -# Inc this number to change uid for every PY3TEST() target -fake_id = 10 +# Inc this number to change uid for every PY3TEST() target +fake_id = 10 diff --git a/library/python/pytest/plugins/ya.make b/library/python/pytest/plugins/ya.make index c15d6f759d..638c532e86 100644 --- a/library/python/pytest/plugins/ya.make +++ b/library/python/pytest/plugins/ya.make @@ -1,4 +1,4 @@ -OWNER(g:yatest) +OWNER(g:yatest) PY23_LIBRARY() @@ -10,23 +10,23 @@ PY_SRCS( ) PEERDIR( - library/python/filelock + library/python/filelock library/python/find_root library/python/testing/filter ) -IF (PYTHON2) - PY_SRCS( - fakeid_py2.py - ) - - PEERDIR( - contrib/python/faulthandler - ) -ELSE() - PY_SRCS( - fakeid_py3.py - ) -ENDIF() - +IF (PYTHON2) + PY_SRCS( + fakeid_py2.py + ) + + PEERDIR( + contrib/python/faulthandler + ) +ELSE() + PY_SRCS( + fakeid_py3.py + ) +ENDIF() + END() diff --git a/library/python/pytest/plugins/ya.py b/library/python/pytest/plugins/ya.py index 1bde03042d..d7398ae90d 100644 --- a/library/python/pytest/plugins/ya.py +++ b/library/python/pytest/plugins/ya.py @@ -1,15 +1,15 @@ -# coding: utf-8 - -import base64 -import errno -import re +# coding: utf-8 + +import base64 +import errno +import re import sys import os import logging import fnmatch import json import time -import traceback +import traceback import collections import signal import inspect @@ -28,7 +28,7 @@ import _pytest.outcomes import _pytest.skipping from _pytest.warning_types import PytestUnhandledCoroutineWarning - + from yatest_lib import test_splitter try: @@ -42,12 +42,12 @@ except ImportError: # fallback for pytest script mode import yatest_tools as tools -try: - from library.python import filelock -except ImportError: - filelock = None - - +try: + from library.python import filelock +except ImportError: + filelock = None + + import yatest_lib.tools import yatest_lib.external as canon @@ -61,7 +61,7 @@ yatest_logger = logging.getLogger("ya.test") _pytest.main.EXIT_NOTESTSCOLLECTED = 0 -SHUTDOWN_REQUESTED = False +SHUTDOWN_REQUESTED = False pytest_config = None @@ -71,8 +71,8 @@ def configure_pdb_on_demand(): if hasattr(signal, "SIGUSR1"): def on_signal(*args): - import ipdb - ipdb.set_trace() + import ipdb + ipdb.set_trace() signal.signal(signal.SIGUSR1, on_signal) @@ -147,40 +147,40 @@ def pytest_addoption(parser): parser.addoption("--python-path", action="store", dest="python_path", default="", help="path the canonical python binary") parser.addoption("--valgrind-path", action="store", dest="valgrind_path", default="", help="path the canonical valgring binary") parser.addoption("--test-filter", action="append", dest="test_filter", default=None, help="test filter") - parser.addoption("--test-file-filter", action="store", dest="test_file_filter", default=None, help="test file filter") + parser.addoption("--test-file-filter", action="store", dest="test_file_filter", default=None, help="test file filter") parser.addoption("--test-param", action="append", dest="test_params", default=None, help="test parameters") parser.addoption("--test-log-level", action="store", dest="test_log_level", choices=["critical", "error", "warning", "info", "debug"], default="debug", help="test log level") parser.addoption("--mode", action="store", choices=[yatest_lib.ya.RunMode.List, yatest_lib.ya.RunMode.Run], dest="mode", default=yatest_lib.ya.RunMode.Run, help="testing mode") - parser.addoption("--test-list-file", action="store", dest="test_list_file") + parser.addoption("--test-list-file", action="store", dest="test_list_file") parser.addoption("--modulo", default=1, type=int) parser.addoption("--modulo-index", default=0, type=int) parser.addoption("--partition-mode", default='SEQUENTIAL', help="Split tests according to partitoin mode") parser.addoption("--split-by-tests", action='store_true', help="Split test execution by tests instead of suites", default=False) parser.addoption("--project-path", action="store", default="", help="path to CMakeList where test is declared") parser.addoption("--build-type", action="store", default="", help="build type") - parser.addoption("--flags", action="append", dest="flags", default=[], help="build flags (-D)") + parser.addoption("--flags", action="append", dest="flags", default=[], help="build flags (-D)") parser.addoption("--sanitize", action="store", default="", help="sanitize mode") parser.addoption("--test-stderr", action="store_true", default=False, help="test stderr") parser.addoption("--test-debug", action="store_true", default=False, help="test debug mode") parser.addoption("--root-dir", action="store", default=None) parser.addoption("--ya-trace", action="store", dest="ya_trace_path", default=None, help="path to ya trace report") - parser.addoption("--ya-version", action="store", dest="ya_version", default=0, type=int, help="allows to be compatible with ya and the new changes in ya-dev") + parser.addoption("--ya-version", action="store", dest="ya_version", default=0, type=int, help="allows to be compatible with ya and the new changes in ya-dev") parser.addoption( "--test-suffix", action="store", dest="test_suffix", default=None, help="add suffix to every test name" ) parser.addoption("--gdb-path", action="store", dest="gdb_path", default="", help="path the canonical gdb binary") parser.addoption("--collect-cores", action="store_true", dest="collect_cores", default=False, help="allows core dump file recovering during test") - parser.addoption("--sanitizer-extra-checks", action="store_true", dest="sanitizer_extra_checks", default=False, help="enables extra checks for tests built with sanitizers") + parser.addoption("--sanitizer-extra-checks", action="store_true", dest="sanitizer_extra_checks", default=False, help="enables extra checks for tests built with sanitizers") parser.addoption("--report-deselected", action="store_true", dest="report_deselected", default=False, help="report deselected tests to the trace file") parser.addoption("--pdb-on-sigusr1", action="store_true", default=False, help="setup pdb.set_trace on SIGUSR1") - parser.addoption("--test-tool-bin", help="Path to test_tool") + parser.addoption("--test-tool-bin", help="Path to test_tool") parser.addoption("--test-list-path", dest="test_list_path", action="store", help="path to test list", default="") -def from_ya_test(): - return "YA_TEST_RUNNER" in os.environ - - +def from_ya_test(): + return "YA_TEST_RUNNER" in os.environ + + def pytest_configure(config): global pytest_config pytest_config = config @@ -189,7 +189,7 @@ def pytest_configure(config): config.addinivalue_line("markers", "ya:external") - config.from_ya_test = from_ya_test() + config.from_ya_test = from_ya_test() config.test_logs = collections.defaultdict(dict) config.test_metrics = {} config.suite_metrics = {} @@ -234,65 +234,65 @@ def pytest_configure(config): config.current_test_name = None config.test_cores_count = 0 config.collect_cores = config.option.collect_cores - config.sanitizer_extra_checks = config.option.sanitizer_extra_checks + config.sanitizer_extra_checks = config.option.sanitizer_extra_checks try: config.test_tool_bin = config.option.test_tool_bin except AttributeError: logging.info("test_tool_bin not specified") if config.sanitizer_extra_checks: - for envvar in ['LSAN_OPTIONS', 'ASAN_OPTIONS']: - if envvar in os.environ: - os.environ.pop(envvar) - if envvar + '_ORIGINAL' in os.environ: - os.environ[envvar] = os.environ[envvar + '_ORIGINAL'] + for envvar in ['LSAN_OPTIONS', 'ASAN_OPTIONS']: + if envvar in os.environ: + os.environ.pop(envvar) + if envvar + '_ORIGINAL' in os.environ: + os.environ[envvar] = os.environ[envvar + '_ORIGINAL'] if config.option.root_dir: config.rootdir = py.path.local(config.option.root_dir) config.invocation_params = attr.evolve(config.invocation_params, dir=config.rootdir) - extra_sys_path = [] - # Arcadia paths from the test DEPENDS section of ya.make - extra_sys_path.append(os.path.join(config.option.source_root, config.option.project_path)) + extra_sys_path = [] + # Arcadia paths from the test DEPENDS section of ya.make + extra_sys_path.append(os.path.join(config.option.source_root, config.option.project_path)) # Build root is required for correct import of protobufs, because imports are related to the root # (like import devtools.dummy_arcadia.protos.lib.my_proto_pb2) - extra_sys_path.append(config.option.build_root) - - for path in config.option.dep_roots: - if os.path.isabs(path): - extra_sys_path.append(path) - else: - extra_sys_path.append(os.path.join(config.option.source_root, path)) - - sys_path_set = set(sys.path) - for path in extra_sys_path: - if path not in sys_path_set: - sys.path.append(path) - sys_path_set.add(path) - - os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) - + extra_sys_path.append(config.option.build_root) + + for path in config.option.dep_roots: + if os.path.isabs(path): + extra_sys_path.append(path) + else: + extra_sys_path.append(os.path.join(config.option.source_root, path)) + + sys_path_set = set(sys.path) + for path in extra_sys_path: + if path not in sys_path_set: + sys.path.append(path) + sys_path_set.add(path) + + os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) + if not config.option.collectonly: if config.option.ya_trace_path: config.ya_trace_reporter = TraceReportGenerator(config.option.ya_trace_path) else: config.ya_trace_reporter = DryTraceReportGenerator(config.option.ya_trace_path) - config.ya_version = config.option.ya_version + config.ya_version = config.option.ya_version sys.meta_path.append(CustomImporter([config.option.build_root] + [os.path.join(config.option.build_root, dep) for dep in config.option.dep_roots])) if config.option.pdb_on_sigusr1: configure_pdb_on_demand() - # Dump python backtrace in case of any errors - faulthandler.enable() - if hasattr(signal, "SIGQUIT"): - # SIGQUIT is used by test_tool to teardown tests which overruns timeout - faulthandler.register(signal.SIGQUIT, chain=True) - - if hasattr(signal, "SIGUSR2"): - signal.signal(signal.SIGUSR2, _graceful_shutdown) - + # Dump python backtrace in case of any errors + faulthandler.enable() + if hasattr(signal, "SIGQUIT"): + # SIGQUIT is used by test_tool to teardown tests which overruns timeout + faulthandler.register(signal.SIGQUIT, chain=True) + + if hasattr(signal, "SIGUSR2"): + signal.signal(signal.SIGUSR2, _graceful_shutdown) + session_should_exit = False @@ -313,20 +313,20 @@ def pytest_runtest_logfinish(nodeid, location): _graceful_shutdown_on_log(session_should_exit) -def _graceful_shutdown(*args): +def _graceful_shutdown(*args): global session_should_exit session_should_exit = True - try: - import library.python.coverage - library.python.coverage.stop_coverage_tracing() - except ImportError: - pass - traceback.print_stack(file=sys.stderr) + try: + import library.python.coverage + library.python.coverage.stop_coverage_tracing() + except ImportError: + pass + traceback.print_stack(file=sys.stderr) capman = pytest_config.pluginmanager.getplugin("capturemanager") capman.suspend(in_=True) _graceful_shutdown_on_log(not capman.is_globally_capturing()) - - + + def _get_rusage(): return resource and resource.getrusage(resource.RUSAGE_SELF) @@ -342,7 +342,7 @@ def _collect_test_rusage(item): if not modifier: modifier = lambda x: x if hasattr(item.rusage, attr_name): - ya_inst.set_metric_value(metric_name, modifier(getattr(finish_rusage, attr_name) - getattr(item.rusage, attr_name))) + ya_inst.set_metric_value(metric_name, modifier(getattr(finish_rusage, attr_name) - getattr(item.rusage, attr_name))) for args in [ ("ru_maxrss", "ru_rss", lambda x: x*1024), # to be the same as in util/system/rusage.cpp @@ -431,7 +431,7 @@ def pytest_collection_modifyitems(items, config): canonical_node_id = str(CustomTestItem(item.nodeid, pytest_config.option.test_suffix)) matched = False for flt in filters: - if "::" not in flt and "*" not in flt: + if "::" not in flt and "*" not in flt: flt += "*" # add support for filtering by module name if canonical_node_id.endswith(flt) or fnmatch.fnmatch(tools.escape_for_fnmatch(canonical_node_id), tools.escape_for_fnmatch(flt)): matched = True @@ -507,10 +507,10 @@ def pytest_collection_modifyitems(items, config): "tags": _get_item_tags(item), } tests.append(record) - if config.option.test_list_file: - with open(config.option.test_list_file, 'w') as afile: - json.dump(tests, afile) - # TODO prettyboy remove after test_tool release - currently it's required for backward compatibility + if config.option.test_list_file: + with open(config.option.test_list_file, 'w') as afile: + json.dump(tests, afile) + # TODO prettyboy remove after test_tool release - currently it's required for backward compatibility sys.stderr.write(json.dumps(tests)) @@ -548,7 +548,7 @@ def pytest_runtest_makereport(item, call): if not pytest_config.suite_metrics and context.Ctx.get("YA_PYTEST_START_TIMESTAMP"): pytest_config.suite_metrics["pytest_startup_duration"] = call.start - context.Ctx["YA_PYTEST_START_TIMESTAMP"] pytest_config.ya_trace_reporter.dump_suite_metrics() - + pytest_config.ya_trace_reporter.on_log_report(test_item) if report.outcome == "failed": @@ -591,48 +591,48 @@ def pytest_make_parametrize_id(config, val, argname): return None -def get_formatted_error(report): - if isinstance(report.longrepr, tuple): - text = "" - for entry in report.longrepr: - text += colorize(entry) - else: - text = colorize(report.longrepr) +def get_formatted_error(report): + if isinstance(report.longrepr, tuple): + text = "" + for entry in report.longrepr: + text += colorize(entry) + else: + text = colorize(report.longrepr) text = yatest_lib.tools.to_utf8(text) - return text - - -def colorize(longrepr): - # use default pytest colorization + return text + + +def colorize(longrepr): + # use default pytest colorization if pytest_config.option.tbstyle != "short": - io = py.io.TextIO() + io = py.io.TextIO() if six.PY2: writer = py.io.TerminalWriter(file=io) else: writer = _pytest._io.TerminalWriter(file=io) - # enable colorization - writer.hasmarkup = True - - if hasattr(longrepr, 'reprtraceback') and hasattr(longrepr.reprtraceback, 'toterminal'): - longrepr.reprtraceback.toterminal(writer) - return io.getvalue().strip() + # enable colorization + writer.hasmarkup = True + + if hasattr(longrepr, 'reprtraceback') and hasattr(longrepr.reprtraceback, 'toterminal'): + longrepr.reprtraceback.toterminal(writer) + return io.getvalue().strip() return yatest_lib.tools.to_utf8(longrepr) - + text = yatest_lib.tools.to_utf8(longrepr) - pos = text.find("E ") - if pos == -1: - return text - - bt, error = text[:pos], text[pos:] - filters = [ - # File path, line number and function name - (re.compile(r"^(.*?):(\d+): in (\S+)", flags=re.MULTILINE), r"[[unimp]]\1[[rst]]:[[alt2]]\2[[rst]]: in [[alt1]]\3[[rst]]"), - ] - for regex, substitution in filters: - bt = regex.sub(substitution, bt) - return "{}[[bad]]{}".format(bt, error) - - + pos = text.find("E ") + if pos == -1: + return text + + bt, error = text[:pos], text[pos:] + filters = [ + # File path, line number and function name + (re.compile(r"^(.*?):(\d+): in (\S+)", flags=re.MULTILINE), r"[[unimp]]\1[[rst]]:[[alt2]]\2[[rst]]: in [[alt1]]\3[[rst]]"), + ] + for regex, substitution in filters: + bt = regex.sub(substitution, bt) + return "{}[[bad]]{}".format(bt, error) + + class TestItem(object): def __init__(self, report, result, test_suffix): @@ -691,7 +691,7 @@ class TestItem(object): def error(self): return self._error - def set_error(self, entry, marker='bad'): + def set_error(self, entry, marker='bad'): if isinstance(entry, _pytest.reports.BaseReport): self._error = get_formatted_error(entry) else: @@ -750,80 +750,80 @@ class DeselectedTestItem(CustomTestItem): class TraceReportGenerator(object): def __init__(self, out_file_path): - self._filename = out_file_path - self._file = open(out_file_path, 'w') - self._wreckage_filename = out_file_path + '.wreckage' + self._filename = out_file_path + self._file = open(out_file_path, 'w') + self._wreckage_filename = out_file_path + '.wreckage' self._test_messages = {} self._test_duration = {} - # Some machinery to avoid data corruption due sloppy fork() - self._current_test = (None, None) - self._pid = os.getpid() - self._check_intricate_respawn() - - def _check_intricate_respawn(self): - pid_file = self._filename + '.pid' - try: - # python2 doesn't support open(f, 'x') - afile = os.fdopen(os.open(pid_file, os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'w') - afile.write(str(self._pid)) - afile.close() - return - except OSError as e: - if e.errno != errno.EEXIST: - raise - - # Looks like the test binary was respawned - if from_ya_test(): - try: - with open(pid_file) as afile: - prev_pid = afile.read() - except Exception as e: - prev_pid = '(failed to obtain previous pid: {})'.format(e) - - parts = [ - "Aborting test run: test machinery found that the test binary {} has already been run before.".format(sys.executable), - "Looks like test has incorrect respawn/relaunch logic within test binary.", - "Test should not try to restart itself - this is a poorly designed test case that leads to errors and could corrupt internal test machinery files.", - "Debug info: previous pid:{} current:{}".format(prev_pid, self._pid), - ] - msg = '\n'.join(parts) - yatest_logger.error(msg) - - if filelock: - lock = filelock.FileLock(self._wreckage_filename + '.lock') - lock.acquire() - - with open(self._wreckage_filename, 'a') as afile: - self._file = afile - - self._dump_trace('chunk_event', {"errors": [('fail', '[[bad]]' + msg)]}) - - raise Exception(msg) - else: - # Test binary is launched without `ya make -t`'s testing machinery - don't rely on clean environment - pass - + # Some machinery to avoid data corruption due sloppy fork() + self._current_test = (None, None) + self._pid = os.getpid() + self._check_intricate_respawn() + + def _check_intricate_respawn(self): + pid_file = self._filename + '.pid' + try: + # python2 doesn't support open(f, 'x') + afile = os.fdopen(os.open(pid_file, os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'w') + afile.write(str(self._pid)) + afile.close() + return + except OSError as e: + if e.errno != errno.EEXIST: + raise + + # Looks like the test binary was respawned + if from_ya_test(): + try: + with open(pid_file) as afile: + prev_pid = afile.read() + except Exception as e: + prev_pid = '(failed to obtain previous pid: {})'.format(e) + + parts = [ + "Aborting test run: test machinery found that the test binary {} has already been run before.".format(sys.executable), + "Looks like test has incorrect respawn/relaunch logic within test binary.", + "Test should not try to restart itself - this is a poorly designed test case that leads to errors and could corrupt internal test machinery files.", + "Debug info: previous pid:{} current:{}".format(prev_pid, self._pid), + ] + msg = '\n'.join(parts) + yatest_logger.error(msg) + + if filelock: + lock = filelock.FileLock(self._wreckage_filename + '.lock') + lock.acquire() + + with open(self._wreckage_filename, 'a') as afile: + self._file = afile + + self._dump_trace('chunk_event', {"errors": [('fail', '[[bad]]' + msg)]}) + + raise Exception(msg) + else: + # Test binary is launched without `ya make -t`'s testing machinery - don't rely on clean environment + pass + def on_start_test_class(self, test_item): pytest_config.ya.set_test_item_node_id(test_item.nodeid) - class_name = test_item.class_name.decode('utf-8') if sys.version_info[0] < 3 else test_item.class_name - self._current_test = (class_name, None) - self.trace('test-started', {'class': class_name}) + class_name = test_item.class_name.decode('utf-8') if sys.version_info[0] < 3 else test_item.class_name + self._current_test = (class_name, None) + self.trace('test-started', {'class': class_name}) def on_finish_test_class(self, test_item): pytest_config.ya.set_test_item_node_id(test_item.nodeid) self.trace('test-finished', {'class': test_item.class_name.decode('utf-8') if sys.version_info[0] < 3 else test_item.class_name}) def on_start_test_case(self, test_item): - class_name = yatest_lib.tools.to_utf8(test_item.class_name) - subtest_name = yatest_lib.tools.to_utf8(test_item.test_name) + class_name = yatest_lib.tools.to_utf8(test_item.class_name) + subtest_name = yatest_lib.tools.to_utf8(test_item.test_name) message = { - 'class': class_name, - 'subtest': subtest_name, + 'class': class_name, + 'subtest': subtest_name, } if test_item.nodeid in pytest_config.test_logs: message['logs'] = pytest_config.test_logs[test_item.nodeid] pytest_config.ya.set_test_item_node_id(test_item.nodeid) - self._current_test = (class_name, subtest_name) + self._current_test = (class_name, subtest_name) self.trace('subtest-started', message) def on_finish_test_case(self, test_item, duration_only=False): @@ -865,9 +865,9 @@ class TraceReportGenerator(object): message = {"metrics": pytest_config.suite_metrics} self.trace("suite-event", message) - def on_error(self, test_item): - self.trace('chunk_event', {"errors": [(test_item.status, self._get_comment(test_item))]}) - + def on_error(self, test_item): + self.trace('chunk_event', {"errors": [(test_item.status, self._get_comment(test_item))]}) + def on_log_report(self, test_item): if test_item.nodeid in self._test_duration: self._test_duration[test_item.nodeid] += test_item._duration @@ -879,77 +879,77 @@ class TraceReportGenerator(object): msg = yatest_lib.tools.to_utf8(test_item.error) if not msg: return "" - return msg + "[[rst]]" + return msg + "[[rst]]" - def _dump_trace(self, name, value): + def _dump_trace(self, name, value): event = { 'timestamp': time.time(), 'value': value, 'name': name } - + data = yatest_lib.tools.to_str(json.dumps(event, ensure_ascii=False)) - self._file.write(data + '\n') - self._file.flush() - - def _check_sloppy_fork(self, name, value): - if self._pid == os.getpid(): - return - - yatest_logger.error("Skip tracing to avoid data corruption, name = %s, value = %s", name, value) - - try: - # Lock wreckage tracefile to avoid race if multiple tests use fork sloppily - if filelock: - lock = filelock.FileLock(self._wreckage_filename + '.lock') - lock.acquire() - - with open(self._wreckage_filename, 'a') as afile: - self._file = afile - - parts = [ - "It looks like you have leaked process - it could corrupt internal test machinery files.", - "Usually it happens when you casually use fork() without os._exit(),", - "which results in two pytest processes running at the same time.", - "Pid of the original pytest's process is {}, however current process has {} pid.".format(self._pid, os.getpid()), - ] - if self._current_test[1]: - parts.append("Most likely the problem is in '{}' test.".format(self._current_test)) - else: - parts.append("Most likely new process was created before any test was launched (during the import stage?).") - - if value.get('comment'): - comment = value.get('comment', '').strip() - # multiline comment - newline_required = '\n' if '\n' in comment else '' - parts.append("Debug info: name = '{}' comment:{}{}".format(name, newline_required, comment)) - else: - val_str = json.dumps(value, ensure_ascii=False).encode('utf-8') - parts.append("Debug info: name = '{}' value = '{}'".format(name, base64.b64encode(val_str))) - - msg = "[[bad]]{}".format('\n'.join(parts)) - class_name, subtest_name = self._current_test - if subtest_name: - data = { - 'class': class_name, - 'subtest': subtest_name, - 'status': 'fail', - 'comment': msg, - } - # overwrite original status - self._dump_trace('subtest-finished', data) - else: - self._dump_trace('chunk_event', {"errors": [('fail', msg)]}) - except Exception as e: - yatest_logger.exception(e) - finally: - os._exit(38) - - def trace(self, name, value): - self._check_sloppy_fork(name, value) - self._dump_trace(name, value) - - + self._file.write(data + '\n') + self._file.flush() + + def _check_sloppy_fork(self, name, value): + if self._pid == os.getpid(): + return + + yatest_logger.error("Skip tracing to avoid data corruption, name = %s, value = %s", name, value) + + try: + # Lock wreckage tracefile to avoid race if multiple tests use fork sloppily + if filelock: + lock = filelock.FileLock(self._wreckage_filename + '.lock') + lock.acquire() + + with open(self._wreckage_filename, 'a') as afile: + self._file = afile + + parts = [ + "It looks like you have leaked process - it could corrupt internal test machinery files.", + "Usually it happens when you casually use fork() without os._exit(),", + "which results in two pytest processes running at the same time.", + "Pid of the original pytest's process is {}, however current process has {} pid.".format(self._pid, os.getpid()), + ] + if self._current_test[1]: + parts.append("Most likely the problem is in '{}' test.".format(self._current_test)) + else: + parts.append("Most likely new process was created before any test was launched (during the import stage?).") + + if value.get('comment'): + comment = value.get('comment', '').strip() + # multiline comment + newline_required = '\n' if '\n' in comment else '' + parts.append("Debug info: name = '{}' comment:{}{}".format(name, newline_required, comment)) + else: + val_str = json.dumps(value, ensure_ascii=False).encode('utf-8') + parts.append("Debug info: name = '{}' value = '{}'".format(name, base64.b64encode(val_str))) + + msg = "[[bad]]{}".format('\n'.join(parts)) + class_name, subtest_name = self._current_test + if subtest_name: + data = { + 'class': class_name, + 'subtest': subtest_name, + 'status': 'fail', + 'comment': msg, + } + # overwrite original status + self._dump_trace('subtest-finished', data) + else: + self._dump_trace('chunk_event', {"errors": [('fail', msg)]}) + except Exception as e: + yatest_logger.exception(e) + finally: + os._exit(38) + + def trace(self, name, value): + self._check_sloppy_fork(name, value) + self._dump_trace(name, value) + + class DryTraceReportGenerator(TraceReportGenerator): """ Generator does not write any information. diff --git a/library/python/pytest/ya.make b/library/python/pytest/ya.make index 060c92c313..662c7787b3 100644 --- a/library/python/pytest/ya.make +++ b/library/python/pytest/ya.make @@ -6,7 +6,7 @@ OWNER( ) PY_SRCS( - __init__.py + __init__.py main.py rewrite.py yatest_tools.py @@ -14,19 +14,19 @@ PY_SRCS( ) PEERDIR( - contrib/python/dateutil - contrib/python/ipdb - contrib/python/py - contrib/python/pytest - contrib/python/requests + contrib/python/dateutil + contrib/python/ipdb + contrib/python/py + contrib/python/pytest + contrib/python/requests library/python/pytest/plugins library/python/testing/yatest_common library/python/testing/yatest_lib ) -RESOURCE_FILES( - PREFIX library/python/pytest/ - pytest.yatest.ini -) - +RESOURCE_FILES( + PREFIX library/python/pytest/ + pytest.yatest.ini +) + END() diff --git a/library/python/pytest/yatest_tools.py b/library/python/pytest/yatest_tools.py index 6b8b896394..0c336250c6 100644 --- a/library/python/pytest/yatest_tools.py +++ b/library/python/pytest/yatest_tools.py @@ -1,25 +1,25 @@ -# coding: utf-8 - -import collections -import functools -import math +# coding: utf-8 + +import collections +import functools +import math import os import re -import sys +import sys import yatest_lib.tools class Subtest(object): - def __init__(self, name, test_name, status, comment, elapsed, result=None, test_type=None, logs=None, cwd=None, metrics=None): - self._name = name - self._test_name = test_name + def __init__(self, name, test_name, status, comment, elapsed, result=None, test_type=None, logs=None, cwd=None, metrics=None): + self._name = name + self._test_name = test_name self.status = status self.elapsed = elapsed self.comment = comment self.result = result self.test_type = test_type - self.logs = logs or {} + self.logs = logs or {} self.cwd = cwd self.metrics = metrics @@ -31,17 +31,17 @@ class Subtest(object): def __str__(self): return yatest_lib.tools.to_utf8(unicode(self)) - def __unicode__(self): - return u"{}::{}".format(self.test_name, self.test_name) - - @property - def name(self): + def __unicode__(self): + return u"{}::{}".format(self.test_name, self.test_name) + + @property + def name(self): return yatest_lib.tools.to_utf8(self._name) - - @property - def test_name(self): + + @property + def test_name(self): return yatest_lib.tools.to_utf8(self._test_name) - + def __repr__(self): return "Subtest [{}::{} - {}[{}]: {}]".format(self.name, self.test_name, self.status, self.elapsed, self.comment) @@ -84,7 +84,7 @@ class SubtestInfo(object): class Status(object): - GOOD, XFAIL, FAIL, XPASS, MISSING, CRASHED, TIMEOUT = range(7) + GOOD, XFAIL, FAIL, XPASS, MISSING, CRASHED, TIMEOUT = range(7) SKIPPED = -100 NOT_LAUNCHED = -200 CANON_DIFF = -300 @@ -152,76 +152,76 @@ ya_ctx = YaCtx() TRACE_FILE_NAME = "ytest.report.trace" -def lazy(func): - mem = {} - - @functools.wraps(func) - def wrapper(): - if "results" not in mem: - mem["results"] = func() - return mem["results"] - - return wrapper - - -@lazy -def _get_mtab(): - if os.path.exists("/etc/mtab"): - with open("/etc/mtab") as afile: - data = afile.read() - return [line.split(" ") for line in data.split("\n") if line] - return [] - - -def get_max_filename_length(dirname): +def lazy(func): + mem = {} + + @functools.wraps(func) + def wrapper(): + if "results" not in mem: + mem["results"] = func() + return mem["results"] + + return wrapper + + +@lazy +def _get_mtab(): + if os.path.exists("/etc/mtab"): + with open("/etc/mtab") as afile: + data = afile.read() + return [line.split(" ") for line in data.split("\n") if line] + return [] + + +def get_max_filename_length(dirname): """ - Return maximum filename length for the filesystem - :return: - """ - if sys.platform.startswith("linux"): - # Linux user's may work on mounted ecryptfs filesystem - # which has filename length limitations - for entry in _get_mtab(): - mounted_dir, filesystem = entry[1], entry[2] - # http://unix.stackexchange.com/questions/32795/what-is-the-maximum-allowed-filename-and-folder-size-with-ecryptfs - if filesystem == "ecryptfs" and dirname and dirname.startswith(mounted_dir): - return 140 - # default maximum filename length for most filesystems - return 255 - - -def get_unique_file_path(dir_path, filename, cache=collections.defaultdict(set)): - """ - Get unique filename in dir with proper filename length, using given filename/dir. - File/dir won't be created (thread nonsafe) + Return maximum filename length for the filesystem + :return: + """ + if sys.platform.startswith("linux"): + # Linux user's may work on mounted ecryptfs filesystem + # which has filename length limitations + for entry in _get_mtab(): + mounted_dir, filesystem = entry[1], entry[2] + # http://unix.stackexchange.com/questions/32795/what-is-the-maximum-allowed-filename-and-folder-size-with-ecryptfs + if filesystem == "ecryptfs" and dirname and dirname.startswith(mounted_dir): + return 140 + # default maximum filename length for most filesystems + return 255 + + +def get_unique_file_path(dir_path, filename, cache=collections.defaultdict(set)): + """ + Get unique filename in dir with proper filename length, using given filename/dir. + File/dir won't be created (thread nonsafe) :param dir_path: path to dir - :param filename: original filename - :return: unique filename + :param filename: original filename + :return: unique filename """ - max_suffix = 10000 - # + 1 symbol for dot before suffix - tail_length = int(round(math.log(max_suffix, 10))) + 1 - # truncate filename length in accordance with filesystem limitations - filename, extension = os.path.splitext(filename) - # XXX - if sys.platform.startswith("win"): - # Trying to fit into MAX_PATH if it's possible. - # Remove after DEVTOOLS-1646 - max_path = 260 - filename_len = len(dir_path) + len(extension) + tail_length + len(os.sep) - if filename_len < max_path: + max_suffix = 10000 + # + 1 symbol for dot before suffix + tail_length = int(round(math.log(max_suffix, 10))) + 1 + # truncate filename length in accordance with filesystem limitations + filename, extension = os.path.splitext(filename) + # XXX + if sys.platform.startswith("win"): + # Trying to fit into MAX_PATH if it's possible. + # Remove after DEVTOOLS-1646 + max_path = 260 + filename_len = len(dir_path) + len(extension) + tail_length + len(os.sep) + if filename_len < max_path: filename = yatest_lib.tools.trim_string(filename, max_path - filename_len) filename = yatest_lib.tools.trim_string(filename, get_max_filename_length(dir_path) - tail_length - len(extension)) + extension - candidate = os.path.join(dir_path, filename) - - key = dir_path + filename - counter = sorted(cache.get(key, {0, }))[-1] - while os.path.exists(candidate): - cache[key].add(counter) - counter += 1 - assert counter < max_suffix - candidate = os.path.join(dir_path, filename + ".{}".format(counter)) - return candidate + candidate = os.path.join(dir_path, filename) + + key = dir_path + filename + counter = sorted(cache.get(key, {0, }))[-1] + while os.path.exists(candidate): + cache[key].add(counter) + counter += 1 + assert counter < max_suffix + candidate = os.path.join(dir_path, filename + ".{}".format(counter)) + return candidate def escape_for_fnmatch(s): @@ -251,18 +251,18 @@ def normalize_name(name): return name -def normalize_filename(filename): +def normalize_filename(filename): """ Replace invalid for file names characters with string equivalents :param some_string: string to be converted to a valid file name :return: valid file name """ not_allowed_pattern = r"[\[\]\/:*?\"\'<>|+\0\\\s\x0b\x0c]" - filename = re.sub(not_allowed_pattern, ".", filename) - return re.sub(r"\.{2,}", ".", filename) + filename = re.sub(not_allowed_pattern, ".", filename) + return re.sub(r"\.{2,}", ".", filename) -def get_test_log_file_path(output_dir, class_name, test_name, extension="log"): +def get_test_log_file_path(output_dir, class_name, test_name, extension="log"): """ get test log file path, platform dependant :param output_dir: dir where log file should be placed @@ -271,16 +271,16 @@ def get_test_log_file_path(output_dir, class_name, test_name, extension="log"): :return: test log file name """ if os.name == "nt": - # don't add class name to the log's filename - # to reduce it's length on windows - filename = test_name + # don't add class name to the log's filename + # to reduce it's length on windows + filename = test_name else: - filename = "{}.{}".format(class_name, test_name) - if not filename: - filename = "test" - filename += "." + extension - filename = normalize_filename(filename) - return get_unique_file_path(output_dir, filename) + filename = "{}.{}".format(class_name, test_name) + if not filename: + filename = "test" + filename += "." + extension + filename = normalize_filename(filename) + return get_unique_file_path(output_dir, filename) def split_node_id(nodeid, test_suffix=None): |