userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/plugins/generated_tests.py Source File
Loading...
Searching...
No Matches
generated_tests.py
1"""
2Pytest infrastructure plugin for collecting virtual generated tests.
3
4The plugin adds a synthetic file named `__test_generated__.py` under the first
5collection directory from `config.args` and exposes a custom pytest hook,
6`pytest_generate_virtual_tests`.
7
8Other plugins can implement that hook to return generated `pytest.Item`
9objects, usually `pytest.Function` items.
10
11Generated tests are not backed by a real Python file, but they get a normal
12pytest parent chain and can use pytest fixtures when implemented as
13`pytest.Function` objects.
14"""
15
16from collections.abc import Iterable
17from collections.abc import Sequence
18import pathlib
19
20import pytest
21from typing_extensions import override
22
23try:
24 import yatest.common # noqa: F401
25
26 _IS_ARCADIA = True
27except ModuleNotFoundError:
28 _IS_ARCADIA = False
29
30_GENERATED_FILENAME = '__test_generated__.py'
31
32_GENERATED_TESTS_DIR_KEY = pytest.StashKey[pathlib.Path | None]()
33_COLLECTED_ITEMS_KEY = pytest.StashKey[list[pytest.Item]]()
34
35
37 """
38 Custom pytest (pluggy) hooks customizing generated tests.
39
40 @ingroup userver_testsuite
41 """
42
43 @pytest.hookspec
45 self,
46 parent: pytest.File,
47 config: pytest.Config,
48 existing_items: Sequence[pytest.Item],
49 ) -> Iterable[pytest.Item]:
50 """
51 Returns generated pytest items to be collected under the virtual `__test_generated__.py` file.
52 """
53 raise NotImplementedError
54
55
56def pytest_addhooks(pluginmanager: pytest.PytestPluginManager) -> None:
57 pluginmanager.add_hookspecs(GeneratedTestsPluginHooks)
58
59
60def pytest_configure(config: pytest.Config) -> None:
61 if _IS_ARCADIA:
62 # Skip trying to touch disk instead of resfs.
63 return
64
65 config.stash[_GENERATED_TESTS_DIR_KEY] = _get_first_collection_directory(config)
66 config.stash[_COLLECTED_ITEMS_KEY] = []
67
68
69def pytest_itemcollected(item: pytest.Item) -> None:
70 if _IS_ARCADIA:
71 return
72
73 # Accumulate the real items as pytest collects them. Since the wrapped
74 # directory yields the virtual `__test_generated__.py` file last, every real
75 # item has already fired this hook by the time the virtual file is collected.
76 item.config.stash[_COLLECTED_ITEMS_KEY].append(item)
77
78
79def _get_first_collection_directory(config: pytest.Config) -> pathlib.Path | None:
80 if not config.args:
81 return None
82
83 base = pathlib.Path(config.invocation_params.dir).resolve()
84
85 raw = config.args[0].split('::', 1)[0]
86 path = pathlib.Path(raw)
87
88 if not path.is_absolute():
89 path = base / path
90
91 path = path.resolve()
92
93 if path.is_file():
94 return path.parent
95
96 if path.is_dir():
97 return path
98
99 return None
100
101
102def pytest_collect_directory(path: pathlib.Path, parent: pytest.Collector):
103 if _IS_ARCADIA:
104 # The OSS way of injecting generated tests does not work there, because:
105 # * pytest_collect_directory hook currently does not work (could be worked around)
106 # * only some of the targets need injected tests (could be worked around too)
107 return None
108
109 generated_tests_dir = parent.config.stash[_GENERATED_TESTS_DIR_KEY]
110
111 if generated_tests_dir is not None and path.resolve() == generated_tests_dir:
112 return _create_wrapped_directory(parent=parent, path=path)
113
114 return None
115
116
117def _create_wrapped_directory(*, parent: pytest.Collector, path: pathlib.Path) -> pytest.Directory:
118 if path.joinpath('__init__.py').is_file():
119 return _GeneratedTestsPackage.from_parent(parent, path=path)
120
121 return _GeneratedTestsDir.from_parent(parent, path=path)
122
123
124class _GeneratedTestsDirectoryMixin(pytest.Directory):
125 @override
126 def collect(self) -> Iterable[pytest.Item | pytest.Collector]:
127 # Yield the real children first and let pytest collect them normally; the
128 # session fires `pytest_itemcollected` for each resulting item, and our
129 # hook accumulates them into the config stash. The virtual file is yielded
130 # last, so by the time it is collected all real items are already known.
131 yield from super().collect()
132
133 yield _GeneratedTestsFile.from_parent(
134 self,
135 path=self.path / _GENERATED_FILENAME,
136 )
137
138
140 pass
141
142
143class _GeneratedTestsPackage(_GeneratedTestsDirectoryMixin, pytest.Package):
144 pass
145
146
148 def __init__(self, *, path: pathlib.Path) -> None:
149 self.__file__ = str(path)
150
151
152class _GeneratedTestsFile(pytest.Module):
153 @override
154 def _getobj(self) -> object:
155 return _FakeModule(path=self.path)
156
157 @override
158 def collect(self) -> Iterable[pytest.Item | pytest.Collector]:
159 existing_items = self.config.stash[_COLLECTED_ITEMS_KEY]
160
161 hook_results = self.config.hook.pytest_generate_virtual_tests(
162 parent=self,
163 config=self.config,
164 existing_items=existing_items,
165 )
166
167 for items in hook_results:
168 yield from items