Coverage for pytest_jsonschema_snapshot/plugin.py: 19%
81 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-02 00:37 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-02 00:37 +0000
1from pathlib import Path
2from typing import Dict, Generator, Optional
4import pytest
5from jsonschema_diff import ConfigMaker, JsonSchemaDiff
6from jsonschema_diff.color import HighlighterPipeline
7from jsonschema_diff.color.stages import (
8 MonoLinesHighlighter,
9 PathHighlighter,
10 ReplaceGenericHighlighter,
11)
13from .core import SchemaShot
14from .stats import GLOBAL_STATS, SchemaStats
16# Global storage of SchemaShot instances for different directories
17_schema_managers: Dict[Path, SchemaShot] = {}
20def pytest_addoption(parser: pytest.Parser) -> None:
21 """Adds --schema-update option to pytest."""
22 parser.addoption(
23 "--schema-update",
24 action="store_true",
25 help=(
26 "Augmenting mode for updating schemas. "
27 "If something is valid for the old schema, then it is valid "
28 "for the new one (and vice versa)."
29 ),
30 )
31 parser.addoption(
32 "--schema-reset",
33 action="store_true",
34 help="New schema does not take into account the old one during update.",
35 )
36 parser.addoption(
37 "--save-original",
38 action="store_true",
39 help="Save original JSON alongside schema (same name, but without `.schema` prefix)",
40 )
41 parser.addoption(
42 "--jsss-debug",
43 action="store_true",
44 help="Show internal exception stack (stops hiding them)",
45 )
47 parser.addoption(
48 "--without-delete",
49 action="store_true",
50 help="Disable deleting unused schemas",
51 )
52 parser.addoption(
53 "--without-update",
54 action="store_true",
55 help="Disable updating schemas",
56 )
57 parser.addoption(
58 "--without-add",
59 action="store_true",
60 help="Disable adding new schemas",
61 )
63 parser.addini(
64 "jsss_dir",
65 default="__snapshots__",
66 help="Directory for storing schemas (default: __snapshots__)",
67 )
68 parser.addini(
69 "jsss_callable_regex",
70 default="{class_method=.}",
71 help="Regex for saving callable part of path",
72 )
73 parser.addini(
74 "jsss_format_mode",
75 default="on",
76 help="Format mode: 'on' (annotate and validate), 'safe' (annotate), 'off' (disable)",
77 )
80@pytest.fixture(scope="function")
81def schemashot(request: pytest.FixtureRequest) -> Generator[SchemaShot, None, None]:
82 """
83 Fixture providing a SchemaShot instance and gathering used schemas.
84 """
86 # Получаем путь к тестовому файлу
87 test_path = Path(request.node.path if hasattr(request.node, "path") else request.node.fspath)
88 root_dir = test_path.parent
90 update_mode = bool(request.config.getoption("--schema-update"))
91 reset_mode = bool(request.config.getoption("--schema-reset"))
92 if update_mode and reset_mode:
93 raise ValueError("Options --schema-update and --schema-reset are mutually exclusive.")
95 save_original = bool(request.config.getoption("--save-original"))
96 debug_mode = bool(request.config.getoption("--jsss-debug"))
98 actions = {
99 "delete": not request.config.getoption("--without-delete"),
100 "update": not request.config.getoption("--without-update"),
101 "add": not request.config.getoption("--without-add"),
102 }
104 # Получаем настраиваемую директорию для схем
105 schema_dir_name = str(request.config.getini("jsss_dir"))
106 callable_regex = str(request.config.getini("jsss_callable_regex"))
107 format_mode = str(request.config.getini("jsss_format_mode")).lower()
108 # examples_limit = int(request.config.getini("jsss_examples_limit"))
110 differ = JsonSchemaDiff(
111 ConfigMaker.make(),
112 HighlighterPipeline(
113 [MonoLinesHighlighter(), PathHighlighter(), ReplaceGenericHighlighter()]
114 ),
115 )
117 # Создаем или получаем экземпляр SchemaShot для этой директории
118 if root_dir not in _schema_managers:
119 _schema_managers[root_dir] = SchemaShot(
120 root_dir,
121 differ,
122 callable_regex,
123 format_mode,
124 # examples_limit,
125 update_mode,
126 reset_mode,
127 actions,
128 save_original,
129 debug_mode,
130 schema_dir_name,
131 )
133 # Создаем локальный экземпляр для теста
134 yield _schema_managers[root_dir]
137@pytest.hookimpl(trylast=True)
138def pytest_unconfigure(config: pytest.Config) -> None:
139 """
140 Hook that runs after all tests have finished.
141 Clears global variables.
142 """
143 global GLOBAL_STATS
145 # Clear the dictionary
146 _schema_managers.clear()
147 # Reset stats for next run
148 GLOBAL_STATS = SchemaStats()
151@pytest.hookimpl(trylast=True)
152def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter, exitstatus: int) -> None:
153 """
154 Adds a summary about schemas to the final pytest report in the terminal.
155 """
156 # Выполняем cleanup перед показом summary
157 if _schema_managers:
159 def get_opt(opt: str) -> bool:
160 return bool(terminalreporter.config.getoption(opt))
162 update_mode = get_opt("--schema-update")
164 actions = {
165 "delete": not get_opt("--without-delete"),
166 "update": not get_opt("--without-update"),
167 "add": not get_opt("--without-add"),
168 }
170 # Вызываем метод очистки неиспользованных схем для каждого экземпляра
171 for _root_dir, manager in _schema_managers.items():
172 cleanup_unused_schemas(manager, update_mode, actions, GLOBAL_STATS)
174 # Используем новую функцию для вывода статистики
175 update_mode = bool(terminalreporter.config.getoption("--schema-update"))
176 GLOBAL_STATS.print_summary(terminalreporter, update_mode)
179def cleanup_unused_schemas(
180 manager: SchemaShot,
181 update_mode: bool,
182 actions: dict[str, bool],
183 stats: Optional[SchemaStats] = None,
184) -> None:
185 """
186 Deletes unused schemas in update mode and collects statistics.
187 Additionally, deletes the pair file `<name>.json` if it exists.
189 Args:
190 manager: SchemaShot instance
191 update_mode: Update mode
192 stats: Optional object for collecting statistics
193 """
194 # Если директория снимков не существует, ничего не делаем
195 if not manager.snapshot_dir.exists():
196 return
198 # Перебираем все файлы схем
199 all_schemas = list(manager.snapshot_dir.glob("*.schema.json"))
201 for schema_file in all_schemas:
202 if schema_file.name not in manager.used_schemas:
203 if update_mode and actions.get("delete"):
204 try:
205 # Удаляем саму схему
206 schema_file.unlink()
207 if stats:
208 stats.add_deleted(schema_file.name)
210 # Пытаемся удалить парный JSON: <name>.json
211 # Преобразуем "<name>.schema.json" -> "<name>.json"
212 base_name = schema_file.name[: -len(".schema.json")]
213 paired_json = schema_file.with_name(f"{base_name}.json")
214 if paired_json.exists():
215 try:
216 paired_json.unlink()
217 if stats:
218 stats.add_deleted(paired_json.name)
219 except OSError as e:
220 manager.logger.warning(
221 f"Failed to delete paired JSON for {schema_file.name}: {e}"
222 )
223 except Exception as e:
224 manager.logger.error(
225 f"Unexpected error deleting paired JSON for {schema_file.name}: {e}"
226 )
228 except OSError as e:
229 # Логируем ошибки удаления, но не прерываем работу
230 manager.logger.warning(
231 f"Failed to delete unused schema {schema_file.name}: {e}"
232 )
233 except Exception as e:
234 # Неожиданные ошибки тоже логируем
235 manager.logger.error(
236 f"Unexpected error deleting schema {schema_file.name}: {e}"
237 )
238 else:
239 if stats:
240 stats.add_unused(schema_file.name)