summaryrefslogtreecommitdiffstats
path: root/library/python/pytest
diff options
context:
space:
mode:
authorantonyzhilin <[email protected]>2026-03-16 16:02:58 +0300
committerantonyzhilin <[email protected]>2026-03-17 06:05:46 +0300
commit8b3f2accec2c327842e23d6c7e3674acb11419ed (patch)
tree6c506cbb27aef24df44ce865560786691263341a /library/python/pytest
parent456efb878536940cf1442d584ea367ec4bcbfa0e (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')
-rw-r--r--library/python/pytest/main.py2
-rw-r--r--library/python/pytest/plugins/collection.py54
-rw-r--r--library/python/pytest/plugins/conftests.py67
-rw-r--r--library/python/pytest/ut/conftest_non_local/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_non_local/lib/conftest.py19
-rw-r--r--library/python/pytest/ut/conftest_non_local/lib/ya.make13
-rw-r--r--library/python/pytest/ut/conftest_non_local/lib2/conftest.py19
-rw-r--r--library/python/pytest/ut/conftest_non_local/lib2/ya.make13
-rw-r--r--library/python/pytest/ut/conftest_non_local/tests/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_non_local/tests/lib_child/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_non_local/tests/lib_child/ya.make13
-rw-r--r--library/python/pytest/ut/conftest_non_local/tests/test_non_local.py31
-rw-r--r--library/python/pytest/ut/conftest_non_local/tests/ya.make19
-rw-r--r--library/python/pytest/ut/conftest_non_local/ya.make21
-rw-r--r--library/python/pytest/ut/test_tools.py12
-rw-r--r--library/python/pytest/ut/ya.make1
-rw-r--r--library/python/pytest/yatest_tools.py84
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):