diff options
| author | robot-piglet <[email protected]> | 2026-02-05 02:05:50 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-02-05 02:33:59 +0300 |
| commit | 9bcffe0e91eb3b15ae3b223ddb9c46742b579076 (patch) | |
| tree | 38a818996ca468f0e68f4075d9a6ea6034bd3f51 /library/python/pytest | |
| parent | 00b2bc2db9a43c4cb6bf6c3eb714323b8a56ab92 (diff) | |
Intermediate changes
commit_hash:e6d634bd59f23a5fa99d8c54932926f775dab895
Diffstat (limited to 'library/python/pytest')
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 ) |
