userver: en/testsuite/plugins/common.py Source File
Loading...
Searching...
No Matches
common.py
1import json
2import pathlib
3import typing
4
5import pytest
6
7from testsuite import types
8from testsuite.utils import cached_property, json_util, traceback, yaml_util
9
10
11class BaseError(Exception):
12 """Base class for errors from this module."""
13
14
16 """Unsupported file open mode passed."""
17
18
20 """Json file load or parse failure error."""
21
22
24 """Yaml file load or parse failure error."""
25
26
27__tracebackhide__ = traceback.hide(BaseError, FileNotFoundError)
28
29
30class GetSearchPathsFixture(typing.Protocol):
31 """Generates sequence of paths for static files."""
32
33 def __call__(
34 self,
35 filename: types.PathOrStr,
36 ) -> typing.Iterator[pathlib.Path]: ...
37
38
39class SearchPathFixture(typing.Protocol):
40 def __call__(
41 self,
42 filename: types.PathOrStr,
43 directory: bool = False,
44 ) -> typing.Iterator[pathlib.Path]: ...
45
46
47class GetFilePathFixture(typing.Protocol):
48 """Returns path to static regular file."""
49
50 def __call__(
51 self,
52 filename: types.PathOrStr,
53 *,
54 missing_ok=False,
55 ) -> pathlib.Path | None: ...
56
57
58class GetDirectoryPathFixture(typing.Protocol):
59 """Returns path to static directory."""
60
61 def __call__(
62 self,
63 filename: types.PathOrStr,
64 *,
65 missing_ok=False,
66 ) -> pathlib.Path | None: ...
67
68
69class OpenFileFixture(typing.Protocol):
70 """Open static file by name.
71
72 Only read-only open modes are supported.
73
74 Example:
75
76 .. code-block:: python
77
78 def test_foo(open_file):
79 with open_file('foo') as fp:
80 ...
81 """
82
83 def __call__(
84 self,
85 filename: types.PathOrStr,
86 mode='r',
87 buffering=-1,
88 encoding='utf-8',
89 errors=None,
90 ) -> typing.IO: ...
91
92
93class LoadFixture(typing.Protocol):
94 """Load file from static directory.
95
96 Example:
97
98 .. code-block:: python
99
100 def test_something(load):
101 data = load('filename')
102
103 :return: :py:class:`LoadFixture` callable instance.
104 """
105
106 def __call__(
107 self,
108 filename: types.PathOrStr,
109 encoding='utf-8',
110 errors=None,
111 *,
112 missing_ok=False,
113 ) -> bytes | str | None: ...
114
115
116class LoadBinaryFixture(typing.Protocol):
117 """Load binary data from static directory.
118
119 Example:
120
121 .. code-block:: python
122
123 def test_something(load_binary):
124 bytes_data = load_binary('data.bin')
125 """
126
127 def __call__(self, filename: types.PathOrStr) -> bytes: ...
128
129
130class JsonLoadsFixture(typing.Protocol):
131 """Load json doc from string.
132
133 Json loader runs ``json_util.loads(data, ..., *args, **kwargs)`` hooks.
134 It does:
135 * bson.json_util.object_hook()
136 * mockserver substitution
137
138 Example:
139
140 .. code-block:: python
141
142 def test_something(json_loads):
143 json_obj = json_loads('{"key": "value"}')
144 """
145
146 def __call__(self, content, *args, **kwargs) -> typing.Any: ...
147
148
149class LoadJsonFixture(typing.Protocol):
150 """Load json doc from static directory.
151
152 Json loader runs ``json_util.loads(data, ..., *args, **kwargs)`` hooks.
153 It does:
154 * bson.json_util.object_hook()
155 * mockserver substitution
156
157 Example:
158
159 .. code-block:: python
160
161 def test_something(load_json):
162 json_obj = load_json('filename.json')
163 """
164
165 def __call__(
166 self,
167 filename: types.PathOrStr,
168 *args,
169 missing_ok=False,
170 missing=None,
171 **kwargs,
172 ) -> typing.Any: ...
173
174
175class LoadYamlFixture(typing.Protocol):
176 """Load yaml doc from static directory.
177
178 .. code-block:: python
179
180 def test_something(load_yaml):
181 yaml_obj = load_yaml('filename.yaml')
182 """
183
184 def __call__(
185 self,
186 filename: types.PathOrStr,
187 *args,
188 **kwargs,
189 ) -> typing.Any: ...
190
191
192_MODES_WHITELIST = frozenset(['r', 'rt', 'rb'])
193
194
195@pytest.fixture
196def get_search_pathes(
197 _search_directories_existing: tuple[pathlib.Path, ...],
198 _path_entries_cache: typing.Callable,
199) -> GetSearchPathsFixture:
200 def search(filename: types.PathOrStr) -> typing.Iterator[pathlib.Path]:
201 for directory in _search_directories_existing:
202 entry = _path_entries_cache(directory, filename)
203 if entry.exists():
204 yield entry
205
206 return search
207
208
209@pytest.fixture
210def get_search_paths(get_search_pathes):
211 return get_search_pathes
212
213
214@pytest.fixture
215def search_path(get_search_paths: GetSearchPathsFixture) -> SearchPathFixture:
216 def search_path(
217 filename: types.PathOrStr,
218 directory: bool = False,
219 ) -> typing.Iterator[pathlib.Path]:
220 for abs_filename in get_search_paths(filename):
221 if directory:
222 if abs_filename.is_dir():
223 yield abs_filename
224 else:
225 if abs_filename.is_file():
226 yield abs_filename
227
228 return search_path
229
230
231@pytest.fixture
232def get_file_path(
233 search_path: SearchPathFixture, _testsuite_file_not_found_error
234) -> GetFilePathFixture:
235 def get_file_path(
236 filename: types.PathOrStr,
237 *,
238 missing_ok=False,
239 ) -> pathlib.Path | None:
240 for path in search_path(filename):
241 return path
242 if missing_ok:
243 return None
244 raise _testsuite_file_not_found_error(
245 f'File {filename} was not found',
246 filename,
247 )
248
249 return get_file_path
250
251
252@pytest.fixture
253def get_directory_path(
254 search_path: SearchPathFixture,
255 _testsuite_file_not_found_error,
256) -> GetDirectoryPathFixture:
257 def get_directory_path(
258 filename: types.PathOrStr,
259 *,
260 missing_ok=False,
261 ) -> pathlib.Path | None:
262 for path in search_path(filename, directory=True):
263 return path
264 if missing_ok:
265 return None
266 raise _testsuite_file_not_found_error(
267 f'Directory {filename} was not found',
268 filename,
269 )
270
271 return get_directory_path
272
273
274@pytest.fixture
275def _testsuite_file_not_found_error(_search_directories_existing):
276 def raise_error(message, filename):
277 paths = '\n'.join(
278 f' - {path / filename}' for path in _search_directories_existing
279 )
280 return FileNotFoundError(
281 f'{message}\n\nThe following paths were examined:\n{paths}',
282 )
283
284 return raise_error
285
286
287@pytest.fixture
288def open_file(get_file_path: GetFilePathFixture) -> OpenFileFixture:
289 def open_file(
290 filename: types.PathOrStr,
291 mode='r',
292 buffering=-1,
293 encoding='utf-8',
294 errors=None,
295 ) -> typing.IO:
296 if mode not in _MODES_WHITELIST:
298 f'Incorrect file open mode {mode!r} passed. '
299 f'Only read-only modes are supported.',
300 )
301 return open(
302 get_file_path(filename), # type: ignore[arg-type]
303 mode=mode,
304 buffering=buffering,
305 encoding=encoding,
306 errors=errors,
307 )
308
309 return open_file
310
311
312@pytest.fixture
313def load(get_file_path: GetFilePathFixture) -> LoadFixture:
314 """Returns a function that loads a static file as text.
315
316 Searches the file via @c get_file_path. Returns @c None when
317 @c missing_ok=True and the file is absent.
318
319 @ingroup userver_testsuite_fixtures
320 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L313)
321 """
322
323 def load(
324 filename: types.PathOrStr,
325 encoding='utf-8',
326 errors=None,
327 *,
328 missing_ok=False,
329 ) -> bytes | str | None:
330 path = get_file_path(filename, missing_ok=missing_ok)
331 if path is None:
332 return None
333 return path.read_text(encoding=encoding, errors=errors)
334
335 return load
336
337
338@pytest.fixture
339def load_binary(get_file_path: GetFilePathFixture) -> LoadBinaryFixture:
340 """Returns a function that loads a static file as bytes.
341
342 Searches the file via @c get_file_path.
343
344 @ingroup userver_testsuite_fixtures
345 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L336)
346 """
347
348 def load_binary(filename: types.PathOrStr) -> bytes:
349 path = get_file_path(filename)
350 return path.read_bytes()
351
352 return load_binary
353
354
355@pytest.fixture
356def json_loads(object_hook, load_json_defaults) -> JsonLoadsFixture:
357 def json_loads(content, *args, **kwargs) -> typing.Any:
358 if 'object_hook' not in kwargs:
359 kwargs['object_hook'] = object_hook
360
361 return json_util.loads(
362 content,
363 *args,
364 **load_json_defaults,
365 **kwargs,
366 )
367
368 return json_loads
369
370
371@pytest.fixture
372def load_json(
373 load: LoadFixture, json_loads: JsonLoadsFixture
374) -> LoadJsonFixture:
375 """Returns a function that loads a static file as JSON.
376
377 @ingroup userver_testsuite_fixtures
378 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L366)
379 """
380
381 def load_json(
382 filename: types.PathOrStr,
383 *args,
384 missing_ok=False,
385 missing=None,
386 **kwargs,
387 ) -> typing.Any:
388 content = load(filename, missing_ok=missing_ok)
389 if content is None:
390 return missing
391 try:
392 return json_loads(content, *args, **kwargs)
393 except json.JSONDecodeError as err:
394 raise LoadJsonError(
395 f'Failed to load JSON file {filename}',
396 ) from err
397
398 return load_json
399
400
401@pytest.fixture
402def load_yaml(load: LoadFixture) -> LoadYamlFixture:
403 """Returns a function that loads a static file as YAML.
404
405 @ingroup userver_testsuite_fixtures
406 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L392)
407 """
408
409 def load_yaml(
410 filename: types.PathOrStr,
411 *args,
412 **kwargs,
413 ) -> typing.Any:
414 content = load(filename)
415 try:
416 return yaml_util.load(content, *args, **kwargs)
417 except yaml_util.ParserError as exc:
418 raise LoadYamlError(
419 f'Failed to load YAML file {filename}',
420 ) from exc
421
422 return load_yaml
423
424
425FilePathsCache = dict[pathlib.Path, list[pathlib.Path]]
426
427
428def pytest_configure(config):
429 config.addinivalue_line(
430 'markers',
431 'nofilldb: test does not need db initialization',
432 )
433
434
435@pytest.fixture
436def static_dir(testsuite_request_directory) -> pathlib.Path:
437 """Static directory related to test path.
438
439 Returns static directory relative to test file, e.g.
440 @code
441 |- tests/
442 |- static/ <-- base static directory for test_foo.py
443 |- test_foo.py
444 @endcode
445
446 @ingroup userver_testsuite_fixtures
447 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L422)
448 """
449 return testsuite_request_directory / 'static'
450
451
452@pytest.fixture
453def initial_data_path() -> tuple[pathlib.Path, ...]:
454 """Use this fixture to override base static search path.
455
456 @code{.py}
457 @pytest.fixture
458 def initial_data_path():
459 return (
460 pathlib.Path(PROJECT_ROOT) / 'tests/static',
461 pathlib.Path(PROJECT_ROOT) / 'static',
462 )
463 @endcode
464
465 @ingroup userver_testsuite_fixtures
466 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L436)
467 """
468
469 return ()
470
471
472@pytest.fixture
473def get_all_static_file_paths(
474 static_dir: pathlib.Path,
475 _file_paths_cache: FilePathsCache,
476):
477 def _get_file_paths() -> list[pathlib.Path]:
478 if static_dir not in _file_paths_cache:
479 _file_paths_cache[static_dir] = [
480 path for path in static_dir.rglob('') if path.is_file()
481 ]
482 return _file_paths_cache[static_dir]
483
484 return _get_file_paths
485
486
487@pytest.fixture
488def object_substitute(object_hook):
489 """Perform object substitution as in load_json.
490
491 @ingroup userver_testsuite_fixtures
492 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/plugins/common.py#L468)
493 """
494
495 def _substitute(content, *args, **kwargs):
496 return json_util.substitute(
497 content,
498 object_hook=object_hook,
499 *args,
500 **kwargs,
501 )
502
503 return _substitute
504
505
506@pytest.fixture(scope='session')
507def testsuite_get_source_path():
508 def get_source_path(path) -> pathlib.Path:
509 return pathlib.Path(path)
510
511 return get_source_path
512
513
514@pytest.fixture(scope='session')
515def testsuite_get_source_directory(testsuite_get_source_path):
516 def get_source_directory(path) -> pathlib.Path:
517 return testsuite_get_source_path(path).parent
518
519 return get_source_directory
520
521
522@pytest.fixture
523def testsuite_request_path(request, testsuite_get_source_path) -> pathlib.Path:
524 return testsuite_get_source_path(request.module.__file__)
525
526
527@pytest.fixture
528def testsuite_request_directory(testsuite_request_path) -> pathlib.Path:
529 return testsuite_request_path.parent
530
531
532@pytest.fixture(scope='session')
533def worker_id(request) -> str:
534 if hasattr(request.config, 'workerinput'):
535 return request.config.workerinput['workerid']
536 return 'master'
537
538
539@pytest.fixture(scope='session')
540def _file_paths_cache() -> FilePathsCache:
541 return {}
542
543
544@pytest.fixture
545def _search_directories(
546 request,
547 static_dir: pathlib.Path,
548 initial_data_path: tuple[pathlib.Path, ...],
549 testsuite_request_path,
550 _path_entries_cache,
551) -> tuple[pathlib.Path, ...]:
552 test_module_name = testsuite_request_path.stem
553 node_name = request.node.name
554 if '[' in node_name:
555 node_name = node_name[: node_name.index('[')]
556 search_directories = [
557 _path_entries_cache(static_dir, test_module_name, node_name),
558 _path_entries_cache(static_dir, test_module_name),
559 _path_entries_cache(static_dir, 'default'),
560 _path_entries_cache(static_dir, ''),
561 ]
562 search_directories.extend(
563 _path_entries_cache(path) for path in initial_data_path
564 )
565 return tuple(search_directories)
566
567
568@pytest.fixture
569def _search_directories_existing(_search_directories):
570 return tuple(path for path in _search_directories if path.is_dir())
571
572
573@pytest.fixture(scope='session')
574def load_json_defaults():
575 return {}
576
577
578@pytest.fixture(scope='session')
579def _cached_stat_path():
580 path_type = type(pathlib.Path())
581 stat_cache: dict[str, tuple[bool, FileNotFoundError | typing.Any]] = {}
582 glob_cache: dict[typing.Any, tuple] = {}
583 content_cache: dict[typing.Any, tuple | str | bytes] = {}
584 iterdir_cache: dict[str, tuple] = {}
585
586 class CachedStatPath(path_type): # type: ignore[valid-type]
587 def stat(self, *, follow_symlinks=True):
588 key = str(self)
589 if key in stat_cache:
590 is_exc, value = stat_cache[key]
591 if is_exc:
592 raise value
593 return value
594 try:
595 value = super().stat()
596 stat_cache[key] = (False, value)
597 return value
598 except FileNotFoundError as exc:
599 stat_cache[key] = (True, exc)
600 raise
601
602 def iterdir(self):
603 cache_key = str(self)
604 data = iterdir_cache.get(cache_key)
605 if data is None:
606 data = tuple(super().iterdir())
607 content_cache[cache_key] = data
608 return data
609
610 def read_bytes(self):
611 cache_key = (str(self), 'b')
612 data = content_cache.get(cache_key)
613 if data is None:
614 data = super().read_bytes()
615 content_cache[cache_key] = data
616 return data
617
618 def read_text(self, encoding=None, errors=None):
619 cache_key = (str(self), encoding, errors)
620 data = content_cache.get(cache_key)
621 if data is None:
622 data = super().read_text(encoding=encoding, errors=errors)
623 content_cache[cache_key] = data
624 return data
625
626 def glob(self, pattern):
627 key = (str(self), pattern)
628 if key not in glob_cache:
629 data = glob_cache[key] = tuple(super().glob(pattern))
630 return data
631 return glob_cache[key]
632
633 def rglob(self, pattern):
634 key = ('r', str(self), pattern)
635 if key not in glob_cache:
636 data = glob_cache[key] = tuple(super().rglob(pattern))
637 return data
638 return glob_cache[key]
639
640 def exists(self):
641 return self._exists
642
643 def is_dir(self):
644 return self._is_dir
645
646 def is_file(self):
647 return self._is_file
648
649 @cached_property
650 def _is_dir(self):
651 return super().is_dir()
652
653 @cached_property
654 def _is_file(self):
655 return super().is_file()
656
657 @cached_property
658 def _exists(self):
659 return super().exists()
660
661 return CachedStatPath
662
663
664@pytest.fixture(scope='session')
665def _path_entries_cache(_cached_stat_path):
666 entries_cache = {}
667
668 def get(*parts):
669 result = entries_cache.get(parts)
670 if result:
671 return result
672 result = _cached_stat_path(*parts)
673 entries_cache[parts] = result
674 return result
675
676 return get