summaryrefslogtreecommitdiffstats
path: root/library/python/pytest
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-02-05 02:05:50 +0300
committerrobot-piglet <[email protected]>2026-02-05 02:33:59 +0300
commit9bcffe0e91eb3b15ae3b223ddb9c46742b579076 (patch)
tree38a818996ca468f0e68f4075d9a6ea6034bd3f51 /library/python/pytest
parent00b2bc2db9a43c4cb6bf6c3eb714323b8a56ab92 (diff)
Intermediate changes
commit_hash:e6d634bd59f23a5fa99d8c54932926f775dab895
Diffstat (limited to 'library/python/pytest')
-rw-r--r--library/python/pytest/main.py2
-rw-r--r--library/python/pytest/module_utils.py83
-rw-r--r--library/python/pytest/plugins/collection.py168
-rw-r--r--library/python/pytest/plugins/conftests.py166
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/test_initial_conftests.py4
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/foo/conftest.py21
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/foo/ya.make16
-rw-r--r--library/python/pytest/ut/conftest_local/initial_conftests/ya.make15
-rw-r--r--library/python/pytest/ut/conftest_local/single_test_module/foo/conftest.py9
-rw-r--r--library/python/pytest/ut/conftest_local/single_test_module/foo_bar/conftest.py9
-rw-r--r--library/python/pytest/ut/conftest_local/single_test_module/foo_bar/test_something.py23
-rw-r--r--library/python/pytest/ut/conftest_local/single_test_module/ya.make13
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/foo/conftest.py9
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/foo/ya.make16
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/conftest.py9
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/test_something.py23
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/ya.make16
-rw-r--r--library/python/pytest/ut/conftest_local/split_test_modules/ya.make15
-rw-r--r--library/python/pytest/ut/conftest_local/ya.make5
-rw-r--r--library/python/pytest/ut/test_tools.py18
-rw-r--r--library/python/pytest/ut/ya.make4
-rw-r--r--library/python/pytest/ya.make1
24 files changed, 568 insertions, 119 deletions
diff --git a/library/python/pytest/main.py b/library/python/pytest/main.py
index 6290ad5f659..c6be6ca802e 100644
--- a/library/python/pytest/main.py
+++ b/library/python/pytest/main.py
@@ -56,7 +56,7 @@ def main():
test_modules = [
# fmt: off
- name[len(prefix) :]
+ name
for name in sys.extra_modules
if name.startswith(prefix) and not name.endswith('.conftest')
# fmt: on
diff --git a/library/python/pytest/module_utils.py b/library/python/pytest/module_utils.py
new file mode 100644
index 00000000000..a26deccfecb
--- /dev/null
+++ b/library/python/pytest/module_utils.py
@@ -0,0 +1,83 @@
+import os
+
+import yatest.common
+import __res
+
+
+def is_relative_to(path, parent):
+ """
+ A polyfill for pathlib.Path.is_relative_to (string-based implementation).
+ Does NOT involve filesystem operations.
+ """
+ path = os.path.normpath(path)
+ parent = os.path.normpath(parent)
+
+ if path == parent:
+ return True
+
+ if not parent.endswith(os.sep):
+ parent += os.sep
+
+ return path.startswith(parent)
+
+
+def relative_to(path, parent):
+ """
+ A polyfill for pathlib.Path.relative_to (string-based implementation).
+ Does NOT involve filesystem operations.
+ """
+ path = os.path.normpath(path)
+ parent = os.path.normpath(parent)
+
+ if path == parent:
+ return ''
+
+ if not parent.endswith(os.sep):
+ parent += os.sep
+
+ # Check if path starts with parent
+ if not path.startswith(parent):
+ raise ValueError('Path {} is not relative to {}', path, parent)
+
+ return path[len(parent) :]
+
+
+def get_module_file_path(module_name):
+ """
+ Get the file path for a module without importing it.
+ For synthesized empty __init__.py modules, returns module_name as a fake file path.
+ """
+ return __res.importer.get_filename(module_name)
+
+
+_BASE_PATHS = None
+
+
+def get_proper_module_path(module_name):
+ """
+ Get the file path for a test module relative to the repository root.
+ For synthesized empty __init__.py modules, returns module_name as a fake file path.
+ """
+ global _BASE_PATHS
+ if _BASE_PATHS is None:
+ _BASE_PATHS = (
+ str(yatest.common.source_path()),
+ str(yatest.common.work_path()),
+ str(yatest.common.build_path()),
+ )
+
+ path = get_module_file_path(module_name)
+
+ if not os.path.isabs(path):
+ if path == module_name:
+ # Synthesized __init__.py module.
+ return None
+ return path
+
+ for base_path in _BASE_PATHS:
+ if is_relative_to(path, base_path):
+ return relative_to(path, base_path)
+
+ raise ValueError(
+ 'Failed to resolve module "{}" with path="{}" against known base paths'.format(module_name, path),
+ )
diff --git a/library/python/pytest/plugins/collection.py b/library/python/pytest/plugins/collection.py
index e9940d8dacb..814649ac17f 100644
--- a/library/python/pytest/plugins/collection.py
+++ b/library/python/pytest/plugins/collection.py
@@ -1,67 +1,78 @@
+import json
import os
import sys
-from six import reraise
import logging
import py
-
-import pytest # noqa
-import _pytest.python
+import pytest
import _pytest.doctest
-import json
-import library.python.testing.filter.filter as test_filter
+import six
+from six import reraise
-if sys.version_info > (3,):
- import _pytest.stash
+if six.PY3:
+ import pathlib
+from library.python.pytest import module_utils
+import library.python.testing.filter.filter as test_filter
-class LoadedModule(_pytest.python.Module):
- def __init__(self, parent, name, **kwargs):
- self.name = name + '.py'
- self.session = parent
- self.parent = parent
- self.config = parent.config
- self.keywords = {}
- self.own_markers = []
- self.extra_keyword_matches = set()
- self.fspath = py.path.local()
- if sys.version_info > (3,):
- self.stash = _pytest.stash.Stash()
- @classmethod
- def from_parent(cls, **kwargs):
- namespace = kwargs.pop('namespace', True)
- kwargs.setdefault('fspath', py.path.local())
+class LoadedModule(pytest.Module):
+ def __init__(self, parent, name, namespace=True, **kwargs):
+ self.module_name = name
+ if namespace:
+ assert name.startswith('__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)
- loaded_module = getattr(super(LoadedModule, cls), 'from_parent', cls)(**kwargs)
- loaded_module.namespace = namespace
+ 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
- return loaded_module
+ 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()
- @property
- def _nodeid(self):
- if os.getenv('CONFTEST_LOAD_POLICY') == 'LOCAL':
- return self._getobj().__file__
+ if six.PY3:
+ super().__init__(parent=parent, name=name, nodeid=nodeid, **kwargs)
else:
- return self.name
+ super(LoadedModule, self).__init__(parent=parent, nodeid=nodeid, **kwargs)
+ self.name = name
- @property
- def nodeid(self):
- return self._nodeid
+ @classmethod
+ def from_parent(cls, **kwargs):
+ return getattr(super(LoadedModule, cls), 'from_parent', cls)(**kwargs)
def _getobj(self):
- module_name = self.name[:-len('.py')]
- if self.namespace:
- module_name = '__tests__.' + module_name
+ # A simplified version of pytest `importtestmodule` that works with resfs.
try:
- __import__(module_name)
+ __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
except Exception as e:
- msg = 'Failed to load module "{}" and obtain list of tests due to an error'.format(module_name)
+ msg = 'Failed to load module "{}" and obtain list of tests due to an error'.format(self.module_name)
logging.exception('%s: %s', msg, e)
etype, exc, tb = sys.exc_info()
reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
- return sys.modules[module_name]
+ return sys.modules[self.module_name]
class DoctestModule(LoadedModule):
@@ -72,14 +83,14 @@ class DoctestModule(LoadedModule):
module = self._getobj()
# uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder()
- if sys.version_info > (3,):
+ if six.PY3:
optionflags = _pytest.doctest.get_optionflags(self.config)
else:
optionflags = _pytest.doctest.get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags)
try:
- for test in finder.find(module, self.name[:-len('.py')]):
+ 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,
@@ -93,34 +104,45 @@ class DoctestModule(LoadedModule):
reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
-def _is_skipped_module_level(module):
- # since we import module by ourselves when CONFTEST_LOAD_POLICY is set to LOCAL we have to handle
- # pytest.skip.Exception https://docs.pytest.org/en/stable/reference/reference.html#pytest-skip
- try:
- module.obj
- 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`.")
+# NOTE: Since we are overriding collect method of pytest session, pytest hooks are not invoked during collection.
+# This function is only used in CollectionPlugin below, this is not an implementation of a pytest hook.
+def _pytest_ignore_collect(module, config, filenames_from_full_filters, accept_filename_predicate):
+ if module.is_fake_module:
return True
- except Exception:
- # letting other exceptions such as ImportError slip through
- pass
- return False
+ # TODO refactor to use meaningful attributes like `path` instead?
+ # test_file_filter would need to be relative to repository root.
+ legacy_name = module.display_module_name + '.py'
-# NOTE: Since we are overriding collect method of pytest session, pytest hooks are not invoked during collection.
-def pytest_ignore_collect(module, session, filenames_from_full_filters, accept_filename_predicate):
- if session.config.option.mode == 'list':
- return not accept_filename_predicate(module.name) or _is_skipped_module_level(module)
+ if config.option.mode == 'list':
+ return not accept_filename_predicate(legacy_name)
- if filenames_from_full_filters is not None and module.name not in filenames_from_full_filters:
+ if filenames_from_full_filters is not None and legacy_name not in filenames_from_full_filters:
return True
- test_file_filter = getattr(session.config.option, 'test_file_filter', None)
- if test_file_filter and module.name != test_file_filter.replace('/', '.'):
+ test_file_filter = getattr(config.option, 'test_file_filter', None)
+ if test_file_filter and legacy_name != test_file_filter.replace('/', '.'):
return True
- return _is_skipped_module_level(module)
+ return False
+
+
+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
+
+ pluginmanager._set_initial_conftests = _set_initial_conftests_py2
class CollectionPlugin(object):
@@ -128,27 +150,37 @@ class CollectionPlugin(object):
self._test_modules = test_modules
self._doctest_modules = doctest_modules
+ @pytest.hookimpl(tryfirst=True)
+ def pytest_load_initial_conftests(self, early_config):
+ _patch_set_initial_conftests(early_config.pluginmanager)
+
def pytest_sessionstart(self, session):
def collect(*args, **kwargs):
- accept_filename_predicate = test_filter.make_py_file_filter(session.config.option.test_filter)
- full_test_names_file_path = session.config.option.test_list_path
+ 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()
+
+ accept_filename_predicate = test_filter.make_py_file_filter(config.option.test_filter)
+ full_test_names_file_path = config.option.test_list_path
filenames_filter = None
if full_test_names_file_path and os.path.exists(full_test_names_file_path):
with open(full_test_names_file_path, 'r') as afile:
# in afile stored 2 dimensional array such that array[modulo_index] contains tests which should be run in this test suite
- full_names_filter = set(json.load(afile)[int(session.config.option.modulo_index)])
+ full_names_filter = set(json.load(afile)[int(config.option.modulo_index)])
filenames_filter = set(map(lambda x: x.split('::')[0], full_names_filter))
for test_module in self._test_modules:
module = LoadedModule.from_parent(name=test_module, parent=session)
- if not pytest_ignore_collect(module, session, filenames_filter, accept_filename_predicate):
+ if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
yield module
if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no':
module = DoctestModule.from_parent(name=test_module, parent=session)
- if not pytest_ignore_collect(module, session, filenames_filter, accept_filename_predicate):
+ if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
yield module
if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no':
diff --git a/library/python/pytest/plugins/conftests.py b/library/python/pytest/plugins/conftests.py
index 4af60b1a049..7d8c13d93ee 100644
--- a/library/python/pytest/plugins/conftests.py
+++ b/library/python/pytest/plugins/conftests.py
@@ -1,62 +1,144 @@
-import os
+import collections
+import functools
import importlib
-import sys
import inspect
+import os
+import six
+import sys
-import yatest.common as yc
-
-from pytest import hookimpl
-from yatest_lib.ya import Ya
+import pytest
from library.python.pytest.plugins.fixtures import metrics, links # noqa
+from library.python.pytest import module_utils
+import yatest.common
+import yatest_lib.ya
-orig_getfile = inspect.getfile
+ConftestInfo = collections.namedtuple('ConftestInfo', ['module_name', 'proper_path'])
+LoadedConftestInfo = collections.namedtuple('LoadedConftestInfo', ['conftest_dir', 'module'])
+
+
+def _patch_inspect_getfile():
+ orig_getfile = inspect.getfile
+
+ def getfile(object):
+ if inspect.ismodule(object) and getattr(object, '__orig_file__', None):
+ res = object.__orig_file__
+ else:
+ res = orig_getfile(object)
+ return res
+ inspect.getfile = getfile
-def getfile(object):
- if inspect.ismodule(object) and getattr(object, '__orig_file__', None):
- res = object.__orig_file__
+
+def _consider_conftest(pluginmanager, module, registration_name):
+ if six.PY3:
+ pluginmanager.consider_conftest(module, registration_name=registration_name)
else:
- res = orig_getfile(object)
- return res
+ pluginmanager.consider_conftest(module)
-inspect.getfile = getfile
-conftest_modules = []
+def _import_conftest(pluginmanager, module_name, proper_path):
+ """Essentially PytestPluginManager._importconftest, adapted to resource-based conftest modules."""
+ module = importlib.import_module(module_name)
+ if six.PY3:
+ pluginmanager._check_non_top_pytest_plugins(module, proper_path)
-@hookimpl(trylast=True)
-def pytest_load_initial_conftests(early_config, parser, args):
- yc.runtime._set_ya_config(ya=Ya())
+ _consider_conftest(pluginmanager, module, proper_path)
+ conftest_info = LoadedConftestInfo(conftest_dir=os.path.dirname(proper_path), module=module)
+ pluginmanager._ya_loaded_conftest_infos.append(conftest_info)
- if hasattr(sys, 'extra_modules'):
- conftests = filter(lambda name: name.endswith(".conftest"), sys.extra_modules)
- else:
- conftests = []
- def conftest_key(name):
- if not name.startswith("__tests__."):
- # Make __tests__ come last
- return "_." + name
- return name
+def _getconftestmodules_local(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.
+ We patch pytest's implementation to avoid fs access when discovering conftests and in is_file() checks.
+ """
+ path = str(path)
+ if path.endswith('.py'):
+ path = os.path.dirname(path)
- for name in sorted(conftests, key=conftest_key):
- mod = importlib.import_module(name)
- if os.getenv("CONFTEST_LOAD_POLICY") != "LOCAL":
- mod.__orig_file__ = mod.__file__
- mod.__file__ = ""
- conftest_modules.append(mod)
- if sys.version_info > (3,):
- early_config.pluginmanager.consider_conftest(mod, registration_name=mod.__file__)
- else:
- early_config.pluginmanager.consider_conftest(mod)
+ cache = self._ya_getconftestmodules_cache
+ if path not in cache:
+ cache[path] = [
+ conftest_info.module
+ for conftest_info in self._ya_loaded_conftest_infos
+ if module_utils.is_relative_to(path, conftest_info.conftest_dir)
+ ]
+
+ return cache[path]
+
+
+def _getconftestmodules_non_local(path):
+ return _conftest_modules
+
+
+_conftest_modules = []
+
+
[email protected](trylast=True)
+def pytest_load_initial_conftests(early_config):
+ pluginmanager = early_config.pluginmanager
+
+ yatest.common.runtime._set_ya_config(ya=yatest_lib.ya.Ya())
+
+ conftests = filter(lambda name: name.endswith('.conftest'), getattr(sys, 'extra_modules', []))
+
+ if os.getenv('CONFTEST_LOAD_POLICY') == 'LOCAL':
+ test_dir = str(yatest.common.context.project_path)
+
+ initial_conftests = []
+ non_initial_conftests = []
+
+ for module_name in conftests:
+ proper_path = module_utils.get_proper_module_path(module_name)
+ assert proper_path is not None
+ conftest_dir = os.path.dirname(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))
+ elif module_utils.is_relative_to(conftest_dir, test_dir):
+ non_initial_conftests.append(ConftestInfo(module_name=module_name, proper_path=proper_path))
+
+ # 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()
+
+ def conftest_key(name):
+ if not name.startswith('__tests__.'):
+ # Make __tests__ come last
+ return '_.' + name
+ return name
+ for name in sorted(conftests, key=conftest_key):
+ module = importlib.import_module(name)
-def getconftestmodules(*args, **kwargs):
- return conftest_modules
+ setattr(module, '__orig_file__', module.__file__)
+ module.__file__ = ''
+ _conftest_modules.append(module)
+ _consider_conftest(pluginmanager, module, module.__file__)
-def pytest_sessionstart(session):
- # Override filesystem based relevant conftest discovery on the call path
- assert session.config.pluginmanager
- session.config.pluginmanager._getconftestmodules = getconftestmodules
+ pluginmanager._getconftestmodules = _getconftestmodules_non_local
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/conftest.py b/library/python/pytest/ut/conftest_local/initial_conftests/conftest.py
new file mode 100644
index 00000000000..c63568e2aca
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/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
+
+
+sessionstart_called = False
+
+
+def pytest_sessionstart(session):
+ global sessionstart_called
+ sessionstart_called = True
+
+
+def is_sessionstart_called_root():
+ return sessionstart_called
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/conftest.py b/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/conftest.py
new file mode 100644
index 00000000000..3fb6b3f64d6
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/conftest.py
@@ -0,0 +1,21 @@
+"""
+This conftest should NOT 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 subdirectory of the test module.
+"""
+
+import pytest
+
+
+sessionstart_called = False
+
+
+def pytest_sessionstart(session):
+ global sessionstart_called
+ sessionstart_called = True
+
+
+def is_sessionstart_called_bar():
+ return sessionstart_called
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/test_initial_conftests.py b/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/test_initial_conftests.py
new file mode 100644
index 00000000000..88fcaf28062
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/foo/bar/test_initial_conftests.py
@@ -0,0 +1,4 @@
+def test_initial_conftests(is_sessionstart_called_root, is_sessionstart_called_foo, is_sessionstart_called_bar):
+ assert is_sessionstart_called_root
+ assert is_sessionstart_called_foo
+ assert not is_sessionstart_called_bar
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/foo/conftest.py b/library/python/pytest/ut/conftest_local/initial_conftests/foo/conftest.py
new file mode 100644
index 00000000000..fd826e553a6
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/foo/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
+
+
+sessionstart_called = False
+
+
+def pytest_sessionstart(session):
+ global sessionstart_called
+ sessionstart_called = True
+
+
+def is_sessionstart_called_foo():
+ return sessionstart_called
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/foo/ya.make b/library/python/pytest/ut/conftest_local/initial_conftests/foo/ya.make
new file mode 100644
index 00000000000..d63c7f19f49
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/foo/ya.make
@@ -0,0 +1,16 @@
+PY23_TEST()
+
+TEST_SRCS(
+ bar/test_initial_conftests.py
+)
+
+PEERDIR(
+ library/python/pytest
+ library/python/pytest/ut/conftest_local/initial_conftests
+)
+
+CONFTEST_LOAD_POLICY_LOCAL()
+
+STYLE_PYTHON()
+
+END()
diff --git a/library/python/pytest/ut/conftest_local/initial_conftests/ya.make b/library/python/pytest/ut/conftest_local/initial_conftests/ya.make
new file mode 100644
index 00000000000..083c7dcc33c
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/initial_conftests/ya.make
@@ -0,0 +1,15 @@
+PY23_LIBRARY()
+
+TEST_SRCS(
+ conftest.py
+ foo/conftest.py
+ foo/bar/conftest.py
+)
+
+STYLE_PYTHON()
+
+END()
+
+RECURSE_FOR_TESTS(
+ foo
+)
diff --git a/library/python/pytest/ut/conftest_local/single_test_module/foo/conftest.py b/library/python/pytest/ut/conftest_local/single_test_module/foo/conftest.py
new file mode 100644
index 00000000000..2f685ec112e
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/single_test_module/foo/conftest.py
@@ -0,0 +1,9 @@
+"""Conftest for foo directory - should NOT be loaded by foo_bar tests."""
+
+import pytest
+
+
+def foo_fixture():
+ """Fixture from foo/conftest.py that should NOT be available in foo_bar."""
+ return 'foo_fixture_value'
diff --git a/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/conftest.py b/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/conftest.py
new file mode 100644
index 00000000000..1e1bba2425d
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/conftest.py
@@ -0,0 +1,9 @@
+"""Conftest for foo_bar directory - should be loaded by foo_bar tests."""
+
+import pytest
+
+
+def foo_bar_fixture():
+ """Fixture from foo_bar/conftest.py that should be available in foo_bar tests."""
+ return 'foo_bar_fixture_value'
diff --git a/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/test_something.py b/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/test_something.py
new file mode 100644
index 00000000000..01de1d36a60
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/single_test_module/foo_bar/test_something.py
@@ -0,0 +1,23 @@
+"""Test file to reproduce pytest conftest bug.
+
+This test should only load foo_bar/conftest.py, but pytest incorrectly
+also loads foo/conftest.py because 'foo_bar' starts with 'foo'.
+"""
+
+import pytest
+
+
+def test_fixture_availability(request):
+ """Test which fixtures are available - should only have foo_bar_fixture."""
+ # This should work - foo_bar/conftest.py should be loaded
+ foo_bar_value = request.getfixturevalue('foo_bar_fixture')
+ assert foo_bar_value == 'foo_bar_fixture_value'
+
+ # This should fail - foo/conftest.py should NOT be loaded
+ with pytest.raises(LookupError):
+ request.getfixturevalue('foo_fixture')
+
+
+def test_simple():
+ """Simple test to verify basic functionality."""
+ assert True
diff --git a/library/python/pytest/ut/conftest_local/single_test_module/ya.make b/library/python/pytest/ut/conftest_local/single_test_module/ya.make
new file mode 100644
index 00000000000..e7ec5637e2a
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/single_test_module/ya.make
@@ -0,0 +1,13 @@
+PY23_TEST()
+
+ALL_PYTEST_SRCS(RECURSIVE)
+
+PEERDIR(
+ library/python/pytest
+)
+
+CONFTEST_LOAD_POLICY_LOCAL()
+
+STYLE_PYTHON()
+
+END()
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/foo/conftest.py b/library/python/pytest/ut/conftest_local/split_test_modules/foo/conftest.py
new file mode 100644
index 00000000000..2f685ec112e
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/foo/conftest.py
@@ -0,0 +1,9 @@
+"""Conftest for foo directory - should NOT be loaded by foo_bar tests."""
+
+import pytest
+
+
+def foo_fixture():
+ """Fixture from foo/conftest.py that should NOT be available in foo_bar."""
+ return 'foo_fixture_value'
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/foo/ya.make b/library/python/pytest/ut/conftest_local/split_test_modules/foo/ya.make
new file mode 100644
index 00000000000..e8373b8be86
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/foo/ya.make
@@ -0,0 +1,16 @@
+PY23_TEST()
+
+SIZE(SMALL)
+
+ALL_PYTEST_SRCS(ONLY_TEST_FILES)
+
+PEERDIR(
+ library/python/pytest
+ library/python/pytest/ut/conftest_local/split_test_modules
+)
+
+CONFTEST_LOAD_POLICY_LOCAL()
+
+STYLE_PYTHON()
+
+END()
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/conftest.py b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/conftest.py
new file mode 100644
index 00000000000..1e1bba2425d
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/conftest.py
@@ -0,0 +1,9 @@
+"""Conftest for foo_bar directory - should be loaded by foo_bar tests."""
+
+import pytest
+
+
+def foo_bar_fixture():
+ """Fixture from foo_bar/conftest.py that should be available in foo_bar tests."""
+ return 'foo_bar_fixture_value'
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/test_something.py b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/test_something.py
new file mode 100644
index 00000000000..01de1d36a60
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/test_something.py
@@ -0,0 +1,23 @@
+"""Test file to reproduce pytest conftest bug.
+
+This test should only load foo_bar/conftest.py, but pytest incorrectly
+also loads foo/conftest.py because 'foo_bar' starts with 'foo'.
+"""
+
+import pytest
+
+
+def test_fixture_availability(request):
+ """Test which fixtures are available - should only have foo_bar_fixture."""
+ # This should work - foo_bar/conftest.py should be loaded
+ foo_bar_value = request.getfixturevalue('foo_bar_fixture')
+ assert foo_bar_value == 'foo_bar_fixture_value'
+
+ # This should fail - foo/conftest.py should NOT be loaded
+ with pytest.raises(LookupError):
+ request.getfixturevalue('foo_fixture')
+
+
+def test_simple():
+ """Simple test to verify basic functionality."""
+ assert True
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/ya.make b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/ya.make
new file mode 100644
index 00000000000..e8373b8be86
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/foo_bar/ya.make
@@ -0,0 +1,16 @@
+PY23_TEST()
+
+SIZE(SMALL)
+
+ALL_PYTEST_SRCS(ONLY_TEST_FILES)
+
+PEERDIR(
+ library/python/pytest
+ library/python/pytest/ut/conftest_local/split_test_modules
+)
+
+CONFTEST_LOAD_POLICY_LOCAL()
+
+STYLE_PYTHON()
+
+END()
diff --git a/library/python/pytest/ut/conftest_local/split_test_modules/ya.make b/library/python/pytest/ut/conftest_local/split_test_modules/ya.make
new file mode 100644
index 00000000000..9067439b690
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/split_test_modules/ya.make
@@ -0,0 +1,15 @@
+PY23_LIBRARY()
+
+TEST_SRCS(
+ foo/conftest.py
+ foo_bar/conftest.py
+)
+
+STYLE_PYTHON()
+
+END()
+
+RECURSE_FOR_TESTS(
+ foo
+ foo_bar
+)
diff --git a/library/python/pytest/ut/conftest_local/ya.make b/library/python/pytest/ut/conftest_local/ya.make
new file mode 100644
index 00000000000..ba918e4d9cb
--- /dev/null
+++ b/library/python/pytest/ut/conftest_local/ya.make
@@ -0,0 +1,5 @@
+RECURSE_FOR_TESTS(
+ initial_conftests
+ single_test_module
+ split_test_modules
+)
diff --git a/library/python/pytest/ut/test_tools.py b/library/python/pytest/ut/test_tools.py
index 212a20fb174..3b23e550b22 100644
--- a/library/python/pytest/ut/test_tools.py
+++ b/library/python/pytest/ut/test_tools.py
@@ -48,10 +48,11 @@ def test_split_node_id_without_path(parameters, node_id, expected_class_name, ex
),
),
)
-def test_split_node_id_with_path(mocker, parameters, node_id, expected_class_name, expected_test_name):
- mocker.patch.object(sys, 'extra_modules', sys.extra_modules | {'__tests__.package.test_script'})
- got = yatest_tools.split_node_id(node_id + parameters)
- assert (expected_class_name, expected_test_name + parameters) == got
+def test_split_node_id_with_path(monkeypatch, parameters, node_id, expected_class_name, expected_test_name):
+ with monkeypatch.context() as context:
+ context.setattr(sys, 'extra_modules', sys.extra_modules | {'__tests__.package.test_script'})
+ got = yatest_tools.split_node_id(node_id + parameters)
+ assert (expected_class_name, expected_test_name + parameters) == got
def test_missing_module(parameters):
@@ -84,7 +85,7 @@ def test_split_node_id_with_test_suffix(parameters, node_id, expected_class_name
],
)
def test_path_resolving_for_local_conftest_load_policy(
- mocker, parameters, node_id, expected_class_name, expected_test_name
+ monkeypatch, parameters, node_id, expected_class_name, expected_test_name
):
# Order matters
extra_modules = [
@@ -92,9 +93,10 @@ def test_path_resolving_for_local_conftest_load_policy(
'__tests__.test',
'__tests__.a.test',
]
- mocker.patch.object(sys, 'extra_modules', extra_modules)
- got = yatest_tools.split_node_id(node_id + parameters)
- assert (expected_class_name, expected_test_name + parameters) == got
+ with monkeypatch.context() as context:
+ context.setattr(sys, 'extra_modules', extra_modules)
+ got = yatest_tools.split_node_id(node_id + parameters)
+ assert (expected_class_name, expected_test_name + parameters) == got
DATA = [
diff --git a/library/python/pytest/ut/ya.make b/library/python/pytest/ut/ya.make
index bb94c24c70b..8a93f4ae690 100644
--- a/library/python/pytest/ut/ya.make
+++ b/library/python/pytest/ut/ya.make
@@ -12,3 +12,7 @@ PEERDIR(
STYLE_PYTHON()
END()
+
+RECURSE_FOR_TESTS(
+ conftest_local
+)
diff --git a/library/python/pytest/ya.make b/library/python/pytest/ya.make
index 607aeb8218c..127d5851137 100644
--- a/library/python/pytest/ya.make
+++ b/library/python/pytest/ya.make
@@ -5,6 +5,7 @@ PY_SRCS(
config.py
context.py
main.py
+ module_utils.py
rewrite.py
yatest_tools.py
)