diff options
| author | antonyzhilin <[email protected]> | 2026-03-16 16:02:58 +0300 |
|---|---|---|
| committer | antonyzhilin <[email protected]> | 2026-03-17 06:05:46 +0300 |
| commit | 8b3f2accec2c327842e23d6c7e3674acb11419ed (patch) | |
| tree | 6c506cbb27aef24df44ce865560786691263341a /library/python/pytest | |
| parent | 456efb878536940cf1442d584ea367ec4bcbfa0e (diff) | |
feat pytest: partially enable behavior of CONFTEST_LOAD_POLICY_LOCAL for all tests
1. `module_node.path` (py3) or `fspath` (py2) are now always in the form `yatest.common.source_path("path/from/repo/root/to/my_test.py")` instead of the previous `partial.path.to.my_test.py`
2. `module_node.nodeid` is now always in the form `path/from/repo/root/to/my_test.py` instead of the previous `partial.path.to.my_test.py`
3. `module_node.name` was also changed to `path/from/repo/root/to/my_test.py`. But it is not recommended to specialize on that, it may change again shortly in the future
4. Refactorings, simplifications and unifications in `library/python/pytest`
commit_hash:8a5b4951208a034f94231b2f799eb0c32a6d787d
Diffstat (limited to 'library/python/pytest')
17 files changed, 315 insertions, 116 deletions
diff --git a/library/python/pytest/main.py b/library/python/pytest/main.py index b4269247175..5f7caa1725c 100644 --- a/library/python/pytest/main.py +++ b/library/python/pytest/main.py @@ -44,6 +44,7 @@ def main(): 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 library.python.pytest.plugins.fixtures as fixtures import _pytest.assertion from _pytest.monkeypatch import MonkeyPatch @@ -109,6 +110,7 @@ def main(): collection.CollectionPlugin(test_modules, doctest_modules), ya, conftests, + fixtures, ] ) diff --git a/library/python/pytest/plugins/collection.py b/library/python/pytest/plugins/collection.py index 039031635c6..e00d9292921 100644 --- a/library/python/pytest/plugins/collection.py +++ b/library/python/pytest/plugins/collection.py @@ -15,6 +15,7 @@ if six.PY3: from library.python.pytest import module_utils import library.python.testing.filter.filter as test_filter +import yatest.common def _make_module_with_single_skipped_test(module_name, file_name, exc): @@ -40,36 +41,30 @@ class LoadedModule(pytest.Module): self.module_name = name if namespace: assert name.startswith('__tests__.') - self.display_module_name = name[len('__tests__.'):] + self.display_module_name = name[len('__tests__.') :] else: self.display_module_name = name self.is_fake_module = False # Always passed to `super().__init__` explicitly. kwargs.pop('nodeid', None) - if os.getenv('CONFTEST_LOAD_POLICY') == 'LOCAL': - nodeid = module_utils.get_proper_module_path(self.module_name) - if nodeid is None: - self.is_fake_module = True - return + nodeid = module_utils.get_proper_module_path(self.module_name) + if nodeid is None: + self.is_fake_module = True + return - name = nodeid - if six.PY3: - kwargs['path'] = kwargs.get('path') or pathlib.Path(nodeid) - else: - raw_module_path = module_utils.get_module_file_path(self.module_name) - kwargs['fspath'] = kwargs.get('fspath') or py.path.local(raw_module_path) - else: - nodeid = self.display_module_name + '.py' - name = nodeid - if six.PY3: - kwargs['path'] = kwargs.get('path') or pathlib.Path(py.path.local()) - else: - kwargs['fspath'] = kwargs.get('fspath') or py.path.local() + # Avoid specializing on `name`. It may change in the future if we add virtual collection nodes for directories. + name = nodeid + # `path` and `fspath` are required to be absolute paths. + # The original source or build directory may be unavailable (at least in CI) and is irrelevant at this point. + # Meanwhile, `source_path` is useful, because it allows to find nearby test data, like with vanilla pytest. + path = yatest.common.source_path(nodeid) if six.PY3: + kwargs['path'] = kwargs.get('path') or pathlib.Path(path) super().__init__(parent=parent, name=name, nodeid=nodeid, **kwargs) else: + kwargs['fspath'] = kwargs.get('fspath') or py.path.local(path) super(LoadedModule, self).__init__(parent=parent, nodeid=nodeid, **kwargs) self.name = name @@ -83,7 +78,9 @@ class LoadedModule(pytest.Module): __import__(self.module_name) except pytest.skip.Exception as e: if not e.allow_module_level: - raise RuntimeError("Using pytest.skip outside of a test will skip the entire module. If that's your intention, pass `allow_module_level=True`.") + raise RuntimeError( + "Using pytest.skip outside of a test will skip the entire module. If that's your intention, pass `allow_module_level=True`." + ) # DEVTOOLSSUPPORT-79470: reraising pytest.skip.Exception from here seems to break reporting in ya plugin. # Instead, pretend to be a module with a single skipped test. return _make_module_with_single_skipped_test(self.module_name, self.nodeid, e) @@ -114,10 +111,8 @@ class DoctestModule(LoadedModule): for test in finder.find(module, self.display_module_name): if test.examples: # skip empty doctests yield getattr(_pytest.doctest.DoctestItem, 'from_parent', _pytest.doctest.DoctestItem)( - name=test.name, - parent=self, - runner=runner, - dtest=test) + name=test.name, parent=self, runner=runner, dtest=test + ) except Exception: logging.exception('DoctestModule failed, probably you can add NO_DOCTESTS() macro to ya.make') etype, exc, tb = sys.exc_info() @@ -153,12 +148,14 @@ def _patch_set_initial_conftests(pluginmanager): Avoids attempts to access filesystem directly in built-in `pytest_load_initial_conftests` hook impl. """ if six.PY3: + def _set_initial_conftests_py3(pyargs, noconftest, *args, **kwargs): pluginmanager._noconftest = noconftest pluginmanager._using_pyargs = pyargs pluginmanager._set_initial_conftests = _set_initial_conftests_py3 else: + def _set_initial_conftests_py2(namespace): pluginmanager._noconftest = namespace.noconftest pluginmanager._using_pyargs = namespace.pyargs @@ -180,9 +177,8 @@ class CollectionPlugin(object): def collect(*args, **kwargs): config = session.config - if os.getenv('CONFTEST_LOAD_POLICY') == 'LOCAL': - # A custom function that is set in conftests.py. - config._ya_register_non_initial_conftests() + # A custom function that is set in conftests.py. + config._ya_register_non_initial_conftests() accept_filename_predicate = test_filter.make_py_file_filter(config.option.test_filter) full_test_names_file_path = config.option.test_list_path @@ -206,6 +202,8 @@ class CollectionPlugin(object): if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no': for doctest_module in self._doctest_modules: - yield DoctestModule.from_parent(name=doctest_module, parent=session, namespace=False) + module = DoctestModule.from_parent(name=doctest_module, parent=session, namespace=False) + if not module.is_fake_module: + yield module session.collect = collect diff --git a/library/python/pytest/plugins/conftests.py b/library/python/pytest/plugins/conftests.py index 33701fd7452..667874bb3d8 100644 --- a/library/python/pytest/plugins/conftests.py +++ b/library/python/pytest/plugins/conftests.py @@ -8,7 +8,6 @@ import sys import pytest -from library.python.pytest.plugins.fixtures import metrics, links # noqa from library.python.pytest import module_utils import yatest.common @@ -48,7 +47,20 @@ def _import_conftest(pluginmanager, module_name, proper_path): pluginmanager._ya_loaded_conftest_infos.append(conftest_info) -def _getconftestmodules_local(self, path): +def _import_conftest_force_initial(pluginmanager, module_name): + """Sets conftest filename to '', which makes pytest consider it an initial conftest.""" + module = importlib.import_module(module_name) + + if six.PY2: + setattr(module, '__orig_file__', module.__file__) + module.__file__ = '' + + _consider_conftest(pluginmanager, module, '') + conftest_info = LoadedConftestInfo(conftest_dir='', module=module) + pluginmanager._ya_loaded_conftest_infos.append(conftest_info) + + +def _getconftestmodules(self, path): """ _getconftestmodules returns a cached list of conftest modules that relate to the given test path. This list is then used by pytest to temporarily mute other non-initial conftests when processing those tests. @@ -69,17 +81,21 @@ def _getconftestmodules_local(self, path): return cache[path] -def _getconftestmodules_non_local(path): - return _conftest_modules - - -_conftest_modules = [] +def _non_local_conftest_key(name): + if not name.startswith('__tests__.'): + # Make __tests__ come last + return '_.' + name + return name @pytest.hookimpl(trylast=True) def pytest_load_initial_conftests(early_config): pluginmanager = early_config.pluginmanager + pluginmanager._ya_loaded_conftest_infos = [] + pluginmanager._ya_getconftestmodules_cache = {} + pluginmanager._getconftestmodules = functools.partial(_getconftestmodules, pluginmanager) + extra_modules = getattr(sys, 'extra_modules', []) conftests = filter(lambda name: name == 'conftest' or name.endswith('.conftest'), extra_modules) @@ -93,50 +109,37 @@ def pytest_load_initial_conftests(early_config): proper_path = module_utils.get_proper_module_path(module_name) assert proper_path is not None conftest_dir = os.path.dirname(proper_path) + conftest_info = ConftestInfo(module_name=module_name, proper_path=proper_path) # There are two kinds of relevant conftests: # - Initial: conftest is in the directory of the test suite or in a parent directory (applies to all tests) # - Non-initial: conftest is strictly within the test directory if module_utils.is_relative_to(test_dir, conftest_dir): - initial_conftests.append(ConftestInfo(module_name=module_name, proper_path=proper_path)) + initial_conftests.append(conftest_info) elif module_utils.is_relative_to(conftest_dir, test_dir): - non_initial_conftests.append(ConftestInfo(module_name=module_name, proper_path=proper_path)) + non_initial_conftests.append(conftest_info) # Ensure parent conftests are loaded before child conftests initial_conftests.sort(key=lambda x: len(x.proper_path)) non_initial_conftests.sort(key=lambda x: len(x.proper_path)) - pluginmanager._ya_loaded_conftest_infos = [] - pluginmanager._ya_getconftestmodules_cache = {} - for conftest_info in initial_conftests: _import_conftest(pluginmanager, conftest_info.module_name, conftest_info.proper_path) - pluginmanager._getconftestmodules = functools.partial(_getconftestmodules_local, pluginmanager) - def _ya_register_non_initial_conftests(): for conftest_info in non_initial_conftests: _import_conftest(pluginmanager, conftest_info.module_name, conftest_info.proper_path) - # See collection.py, non-initial conftests are registered at the start of collection phase, like in vanilla. - # Alas, stash is not available in py2. - early_config._ya_register_non_initial_conftests = _ya_register_non_initial_conftests else: - _patch_inspect_getfile() + if six.PY2: + _patch_inspect_getfile() - def conftest_key(name): - if not name.startswith('__tests__.'): - # Make __tests__ come last - return '_.' + name - return name + for name in sorted(conftests, key=_non_local_conftest_key): + _import_conftest_force_initial(pluginmanager, name) - for name in sorted(conftests, key=conftest_key): - module = importlib.import_module(name) - - setattr(module, '__orig_file__', module.__file__) - module.__file__ = '' - - _conftest_modules.append(module) - _consider_conftest(pluginmanager, module, module.__file__) + def _ya_register_non_initial_conftests(): + pass - pluginmanager._getconftestmodules = _getconftestmodules_non_local + # See collection.py, non-initial conftests are registered at the start of collection phase, like in vanilla. + # Alas, stash is not available in py2. + early_config._ya_register_non_initial_conftests = _ya_register_non_initial_conftests diff --git a/library/python/pytest/ut/conftest_non_local/conftest.py b/library/python/pytest/ut/conftest_non_local/conftest.py new file mode 100644 index 00000000000..1e4dc8dfade --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/conftest.py @@ -0,0 +1,21 @@ +""" +This conftest SHOULD be counted an initial conftest, see +https://docs.pytest.org/en/stable/how-to/writing_plugins.html#pluginorder + +This is because this conftest is located in a parent directory of the test module. +""" + +import pytest + + +def pytest_configure(config): + config._my_conftests = getattr(config, '_my_conftests', []) + ['conftest_non_local/conftest.py'] + + +def pytest_sessionstart(session): + session._my_conftests = getattr(session, '_my_conftests', []) + ['conftest_non_local/conftest.py'] + + +def fixture_order(): + return ['conftest_non_local/conftest.py'] diff --git a/library/python/pytest/ut/conftest_non_local/lib/conftest.py b/library/python/pytest/ut/conftest_non_local/lib/conftest.py new file mode 100644 index 00000000000..9bc1a3dc68a --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/lib/conftest.py @@ -0,0 +1,19 @@ +""" +This conftest should not be loaded according to vanilla pytest rules. +Still, with CONFTEST_LOCAL_POLICY != LOCAL, we will load it as an initial conftest. +""" + +import pytest + + +def pytest_configure(config): + config._my_conftests = getattr(config, '_my_conftests', []) + ['conftest_non_local/lib/conftest.py'] + + +def pytest_sessionstart(session): + session._my_conftests = getattr(session, '_my_conftests', []) + ['conftest_non_local/lib/conftest.py'] + + +def fixture_order(fixture_order): + return fixture_order + ['conftest_non_local/lib/conftest.py'] diff --git a/library/python/pytest/ut/conftest_non_local/lib/ya.make b/library/python/pytest/ut/conftest_non_local/lib/ya.make new file mode 100644 index 00000000000..01d4899c692 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/lib/ya.make @@ -0,0 +1,13 @@ +PY23_LIBRARY() + +PY_SRCS( + conftest.py +) + +PEERDIR( + library/python/pytest +) + +STYLE_PYTHON() + +END() diff --git a/library/python/pytest/ut/conftest_non_local/lib2/conftest.py b/library/python/pytest/ut/conftest_non_local/lib2/conftest.py new file mode 100644 index 00000000000..d896834a57e --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/lib2/conftest.py @@ -0,0 +1,19 @@ +""" +This conftest should not be loaded according to vanilla pytest rules. +Still, with CONFTEST_LOCAL_POLICY != LOCAL, we will load it as an initial conftest. +""" + +import pytest + + +def pytest_configure(config): + config._my_conftests = getattr(config, '_my_conftests', []) + ['conftest_non_local/lib2/conftest.py'] + + +def pytest_sessionstart(session): + session._my_conftests = getattr(session, '_my_conftests', []) + ['conftest_non_local/lib2/conftest.py'] + + +def fixture_order(fixture_order): + return fixture_order + ['conftest_non_local/lib2/conftest.py'] diff --git a/library/python/pytest/ut/conftest_non_local/lib2/ya.make b/library/python/pytest/ut/conftest_non_local/lib2/ya.make new file mode 100644 index 00000000000..01d4899c692 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/lib2/ya.make @@ -0,0 +1,13 @@ +PY23_LIBRARY() + +PY_SRCS( + conftest.py +) + +PEERDIR( + library/python/pytest +) + +STYLE_PYTHON() + +END() diff --git a/library/python/pytest/ut/conftest_non_local/tests/conftest.py b/library/python/pytest/ut/conftest_non_local/tests/conftest.py new file mode 100644 index 00000000000..4567ed87e43 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/tests/conftest.py @@ -0,0 +1,21 @@ +""" +This conftest SHOULD be counted an initial conftest, see +https://docs.pytest.org/en/stable/how-to/writing_plugins.html#pluginorder + +This is because this conftest is located in the directory of the test module. +""" + +import pytest + + +def pytest_configure(config): + config._my_conftests = getattr(config, '_my_conftests', []) + ['conftest_non_local/tests/conftest.py'] + + +def pytest_sessionstart(session): + session._my_conftests = getattr(session, '_my_conftests', []) + ['conftest_non_local/tests/conftest.py'] + + +def fixture_order(fixture_order): + return fixture_order + ['conftest_non_local/tests/conftest.py'] diff --git a/library/python/pytest/ut/conftest_non_local/tests/lib_child/conftest.py b/library/python/pytest/ut/conftest_non_local/tests/lib_child/conftest.py new file mode 100644 index 00000000000..611d43592fb --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/tests/lib_child/conftest.py @@ -0,0 +1,21 @@ +""" +This conftest should be counted a non-initial conftest, see +https://docs.pytest.org/en/stable/how-to/writing_plugins.html#pluginorder + +Still, with CONFTEST_LOCAL_POLICY != LOCAL, we will load it as an initial conftest. +""" + +import pytest + + +def pytest_configure(config): + config._my_conftests = getattr(config, '_my_conftests', []) + ['conftest_non_local/tests/lib_child/conftest.py'] + + +def pytest_sessionstart(session): + session._my_conftests = getattr(session, '_my_conftests', []) + ['conftest_non_local/tests/lib_child/conftest.py'] + + +def fixture_order(fixture_order): + return fixture_order + ['conftest_non_local/tests/lib_child/conftest.py'] diff --git a/library/python/pytest/ut/conftest_non_local/tests/lib_child/ya.make b/library/python/pytest/ut/conftest_non_local/tests/lib_child/ya.make new file mode 100644 index 00000000000..01d4899c692 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/tests/lib_child/ya.make @@ -0,0 +1,13 @@ +PY23_LIBRARY() + +PY_SRCS( + conftest.py +) + +PEERDIR( + library/python/pytest +) + +STYLE_PYTHON() + +END() diff --git a/library/python/pytest/ut/conftest_non_local/tests/test_non_local.py b/library/python/pytest/ut/conftest_non_local/tests/test_non_local.py new file mode 100644 index 00000000000..0635a57eef2 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/tests/test_non_local.py @@ -0,0 +1,31 @@ +def test_conftest_plugin_order(pytestconfig): + # Unintuitively, pluggy invokes hooks in reverse-registration order. + # This includes `pytest_confugure` for plugins registered before pytest session starts. + # See comments on HookCaller._hookimpls in contrib/python/pluggy/py3/pluggy/_hooks.py + assert pytestconfig._my_conftests == [ + 'conftest_non_local/tests/lib_child/conftest.py', + 'conftest_non_local/tests/conftest.py', + 'conftest_non_local/lib2/conftest.py', + 'conftest_non_local/lib/conftest.py', + 'conftest_non_local/conftest.py', + ] + + +def test_conftests_are_initial(request): + assert set(request.session._my_conftests) == { + 'conftest_non_local/conftest.py', + 'conftest_non_local/lib/conftest.py', + 'conftest_non_local/lib2/conftest.py', + 'conftest_non_local/tests/conftest.py', + 'conftest_non_local/tests/lib_child/conftest.py', + } + + +def test_conftest_fixture_order(fixture_order): + assert fixture_order == [ + 'conftest_non_local/conftest.py', + 'conftest_non_local/lib/conftest.py', + 'conftest_non_local/lib2/conftest.py', + 'conftest_non_local/tests/conftest.py', + 'conftest_non_local/tests/lib_child/conftest.py', + ] diff --git a/library/python/pytest/ut/conftest_non_local/tests/ya.make b/library/python/pytest/ut/conftest_non_local/tests/ya.make new file mode 100644 index 00000000000..0fbcd8831f3 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/tests/ya.make @@ -0,0 +1,19 @@ +PY23_TEST() + +PY_SRCS( + conftest.py +) + +ALL_PYTEST_SRCS(ONLY_TEST_FILES) + +PEERDIR( + library/python/pytest + library/python/pytest/ut/conftest_non_local + library/python/pytest/ut/conftest_non_local/lib + library/python/pytest/ut/conftest_non_local/lib2 + library/python/pytest/ut/conftest_non_local/tests/lib_child +) + +STYLE_PYTHON() + +END() diff --git a/library/python/pytest/ut/conftest_non_local/ya.make b/library/python/pytest/ut/conftest_non_local/ya.make new file mode 100644 index 00000000000..9ecb97f7803 --- /dev/null +++ b/library/python/pytest/ut/conftest_non_local/ya.make @@ -0,0 +1,21 @@ +PY23_LIBRARY() + +PY_SRCS( + conftest.py +) + +PEERDIR( + library/python/pytest +) + +STYLE_PYTHON() + +END() + +RECURSE( + lib +) + +RECURSE_FOR_TESTS( + tests +) diff --git a/library/python/pytest/ut/test_tools.py b/library/python/pytest/ut/test_tools.py index cbfe6eff25b..99e093aa303 100644 --- a/library/python/pytest/ut/test_tools.py +++ b/library/python/pytest/ut/test_tools.py @@ -48,8 +48,12 @@ def test_split_node_id_without_path(parameters, node_id, expected_class_name, ex ), ) def test_split_node_id_with_path(monkeypatch, parameters, node_id, expected_class_name, expected_test_name): + mapping = { + '/arcadia/root/package/test_script.py': 'package.test_script', + } with monkeypatch.context() as context: context.setattr(sys, 'extra_modules', sys.extra_modules | {'__tests__.package.test_script'}) + context.setattr(yatest_tools, '_nodeid_to_display_module_name_mapping', lambda: mapping) got = yatest_tools.split_node_id(node_id + parameters) assert (expected_class_name, expected_test_name + parameters) == got @@ -80,20 +84,24 @@ def test_split_node_id_with_test_suffix(parameters, node_id, expected_class_name ("/arcadia/data/b/a/test.py::test_b_a", "b.a.test.py", "test_b_a"), ("/arcadia/data/a/test.py::test_a", "a.test.py", "test_a"), ("/arcadia/data/test.py::test", "test.py", "test"), - ("b/a/test.py::test_b_a", "b.a.test.py", "test_b_a"), ], ) def test_path_resolving_for_local_conftest_load_policy( monkeypatch, parameters, node_id, expected_class_name, expected_test_name ): - # Order matters extra_modules = [ '__tests__.b.a.test', '__tests__.test', '__tests__.a.test', ] + mapping = { + '/arcadia/data/b/a/test.py': 'b.a.test', + '/arcadia/data/test.py': 'test', + '/arcadia/data/a/test.py': 'a.test', + } with monkeypatch.context() as context: context.setattr(sys, 'extra_modules', extra_modules) + context.setattr(yatest_tools, '_nodeid_to_display_module_name_mapping', lambda: mapping) got = yatest_tools.split_node_id(node_id + parameters) assert (expected_class_name, expected_test_name + parameters) == got diff --git a/library/python/pytest/ut/ya.make b/library/python/pytest/ut/ya.make index 5cf962dd45a..19ddfa8f48d 100644 --- a/library/python/pytest/ut/ya.make +++ b/library/python/pytest/ut/ya.make @@ -15,5 +15,6 @@ END() RECURSE_FOR_TESTS( conftest_local + conftest_non_local pytest_plugins_env ) diff --git a/library/python/pytest/yatest_tools.py b/library/python/pytest/yatest_tools.py index 5c1a61c1177..3fe568215f1 100644 --- a/library/python/pytest/yatest_tools.py +++ b/library/python/pytest/yatest_tools.py @@ -8,9 +8,11 @@ import re import sys from . import config +from library.python.pytest import module_utils import yatest_lib.tools SEP = '/' +PY_EXT = '.py' TEST_MOD_PREFIX = '__tests__.' @@ -291,8 +293,7 @@ def split_node_id(nodeid, test_suffix=None): test_name = None if separator in path: path, test_name = path.split(separator, 1) - path = _unify_path(path) - class_name = os.path.basename(path) + class_name = _nodeid_to_class_name(path) if test_name is None: test_name = class_name if test_suffix: @@ -309,70 +310,45 @@ def split_node_id(nodeid, test_suffix=None): @lazy -def _suffix_test_modules_tree(): - root = {} +def _nodeid_to_display_module_name_mapping(): + nodeid_to_display_module_name = {} - for module in sys.extra_modules: - if not module.startswith(TEST_MOD_PREFIX): - continue - - module = module[len(TEST_MOD_PREFIX) :] - node = root + for module_name in sys.extra_modules: + # Conversion rules are taken from LoadedModule in plugins/collection.py. - for name in reversed(module.split('.')): - if name == '__init__': - continue - node = node.setdefault(name, {}) + if module_name.startswith(TEST_MOD_PREFIX): + display_module_name = module_name[len(TEST_MOD_PREFIX) :] + else: + # Non-test modules can participate in DOCTEST and may need resolving, so include them in mapping too. + display_module_name = module_name - return root + nodeid = module_utils.get_proper_module_path(module_name) + if nodeid is None: + continue + nodeid_to_display_module_name[nodeid] = display_module_name -def _conftest_load_policy_is_local(path): - return SEP in path and getattr(sys, "is_standalone_binary", False) + return nodeid_to_display_module_name class MissingTestModule(Exception): pass -# If CONFTEST_LOAD_POLICY==LOCAL the path parameters is a true test file path. Something like -# /-B/taxi/uservices/services/alt/gen/tests/build/services/alt/validation/test_generated_files.py -# If CONFTEST_LOAD_POLICY is not LOCAL the path parameter is a module name with '.py' extension added. Example: -# validation.test_generated_files.py -# To make test names independent of the CONFTEST_LOAD_POLICY value replace path by module name if possible. -@lazy -def _unify_path(path): - py_ext = ".py" - - path = path.strip() - if _conftest_load_policy_is_local(path) and path.endswith(py_ext): - # Try to find best match for path as a module among test modules and use it as a class name. - # This is the only way to unify different CONFTEST_LOAD_POLICY modes - suff_tree = _suffix_test_modules_tree() - node, res = suff_tree, [] - - assert path.endswith(py_ext), path - parts = path[: -len(py_ext)].split(SEP) - - # Use SEP as trailing terminator to make an extra step - # and find a proper match when parts is a full matching path - for p in reversed([SEP] + parts): - if p in node: - node = node[p] - res.append(p) - else: - if res: - return '.'.join(reversed(res)) + py_ext - else: - # Top level test module - if TEST_MOD_PREFIX + p in sys.extra_modules: - return p + py_ext - # Unknown module - raise an error - break - - raise MissingTestModule("Can't find proper module for '{}' path among: {}".format(path, suff_tree)) +# Module's nodeid is the test file path relative to repository root, like: +# taxi/uservices/services/alt/gen/tests/build/services/alt/validation/test_generated_files.py +# Traditionally we use module name without __tests__ prefix and with .py suffix as yatest "class name": +# validation.test_generated_files.py +def _nodeid_to_class_name(nodeid): + nodeid = nodeid.strip() + if SEP in nodeid and nodeid.endswith(PY_EXT): + mapping = _nodeid_to_display_module_name_mapping() + display_module_name = mapping.get(nodeid) + if display_module_name is not None: + return display_module_name + PY_EXT + raise MissingTestModule("Can't find module name for nodeid='{}' among: {}".format(nodeid, mapping)) else: - return path + return os.path.basename(nodeid) def colorize_pytest_error(text): |
