userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/plugins/ydb/ydbsupport.py Source File
Loading...
Searching...
No Matches
ydbsupport.py
1# pylint: disable=redefined-outer-name
2import concurrent.futures
3import contextlib
4import dataclasses
5import os
6import pathlib
7import subprocess
8
9import pytest
10import yaml
11
12from testsuite.environment import shell
13
14from pytest_userver import sql
15from . import client
16from . import discover
17from . import service
18
19if hasattr(yaml, 'CLoader'):
20 _YamlLoader = yaml.CLoader # type: ignore
21else:
22 _YamlLoader = yaml.Loader # type: ignore
23
24USERVER_CONFIG_HOOKS = ['userver_config_ydb']
25
26
27@pytest.fixture
28def ydb(_ydb_client, _ydb_init) -> client.YdbClient:
29 """
30 YDB client fixture
31
32 @ingroup userver_testsuite_fixtures
33 """
34 return _ydb_client
35
36
37@pytest.fixture(scope='session')
38def _ydb_client(_ydb_client_pool):
39 with _ydb_client_pool() as ydb_client:
40 yield ydb_client
41
42
43@pytest.fixture(scope='session')
44def _ydb_client_pool(_ydb_service, ydb_service_settings):
45 endpoint = '{}:{}'.format(
46 ydb_service_settings.host,
47 ydb_service_settings.grpc_port,
48 )
49 pool = []
50
51 @contextlib.contextmanager
52 def get_client():
53 try:
54 ydb_client = pool.pop()
55 except IndexError:
56 ydb_client = client.YdbClient(
57 endpoint,
58 ydb_service_settings.database,
59 )
60 try:
61 yield ydb_client
62 finally:
63 pool.append(ydb_client)
64
65 return get_client
66
67
68def pytest_service_register(register_service):
69 register_service('ydb', service.create_ydb_service)
70
71
72@pytest.fixture(scope='session')
73def _ydb_service(pytestconfig, ensure_service_started, ydb_service_settings):
74 if os.environ.get('YDB_ENDPOINT') or pytestconfig.option.ydb_host:
75 return
76 ensure_service_started('ydb', settings=ydb_service_settings)
77
78
79@pytest.fixture(scope='session')
80def ydb_service_settings(pytestconfig) -> service.ServiceSettings:
81 endpoint_from_env = os.environ.get('YDB_ENDPOINT')
82 database = os.environ.get('YDB_DATABASE', 'local')
83
84 if endpoint_from_env:
85 host, grpc_port = endpoint_from_env.split(':', 1)
86 return service.ServiceSettings(
87 host=host,
88 grpc_port=grpc_port,
89 mon_port=None,
90 ic_port=None,
91 database=database,
92 wait_time=0,
93 )
94
95 if pytestconfig.option.ydb_host:
96 return service.ServiceSettings(
97 host=pytestconfig.option.ydb_host,
98 grpc_port=pytestconfig.option.ydb_grpc_port,
99 mon_port=pytestconfig.option.ydb_mon_port,
100 ic_port=pytestconfig.option.ydb_ic_port,
101 database=database,
102 wait_time=pytestconfig.option.ydb_wait_time,
103 )
104 settings = service.get_service_settings()
105 return dataclasses.replace(settings, wait_time=pytestconfig.option.ydb_wait_time)
106
107
108@pytest.fixture(scope='session')
109def _ydb_service_schemas(service_source_dir):
110 service_schemas_ydb = service_source_dir / 'ydb' / 'schemas'
111 return discover.find_schemas([service_schemas_ydb])
112
113
114@pytest.fixture(scope='session')
115def ydb_settings_substitute(ydb_service_settings):
116 def secdist_settings(*args, **kwargs):
117 return {
118 'endpoint': '{}:{}'.format(
119 ydb_service_settings.host,
120 ydb_service_settings.grpc_port,
121 ),
122 'database': '/{}'.format(ydb_service_settings.database),
123 'token': '',
124 }
125
126 return {'ydb_settings': secdist_settings}
127
128
129@pytest.fixture(scope='session')
130def _ydb_state():
131 class State:
132 def __init__(self):
133 self.init = False
134 self.tables = []
135
136 return State()
137
138
139@pytest.fixture(scope='session')
140def ydb_migration_dir(service_source_dir) -> pathlib.Path:
141 """
142 Directory with migration files
143
144 @ingroup userver_testsuite_fixtures
145 """
146 return service_source_dir / 'ydb' / 'migrations'
147
148
149YDB_MIGRATION_TABLE = 'goose_db_version'
150
151
152def _ydb_migrate(ydb_service_settings, ydb_migration_dir, goose_binary_path):
153 if not ydb_migration_dir.exists():
154 return
155 if not list(ydb_migration_dir.iterdir()):
156 return
157
158 host = ydb_service_settings.host
159 port = ydb_service_settings.grpc_port
160
161 command = [
162 str(goose_binary_path),
163 '-dir',
164 str(ydb_migration_dir),
165 '-table',
166 YDB_MIGRATION_TABLE,
167 'ydb',
168 (f'grpc://{host}:{port}/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric'),
169 'up',
170 ]
171 try:
172 shell.execute(command, verbose=True, command_alias='ydb/migrations')
173 except shell.SubprocessFailed as exc:
174 raise Exception(f'YDB run migration failed:\n{exc}')
175
176
177@pytest.fixture(scope='session')
178def goose_binary_path() -> pathlib.Path:
179 """
180 Path to 'goose' migration tool.
181
182 Override this fixture to change the way 'goose' binary is discovered.
183
184 @ingroup userver_testsuite_fixtures
185 """
186 try:
187 import yatest
188
189 return yatest.common.runtime.binary_path(
190 'contrib/go/patched/goose/cmd/goose/goose',
191 )
192 except ImportError:
193 return 'goose'
194
195
196def _ydb_fetch_table_names(ydb_service_settings, ydb_cli) -> list[str]:
197 try:
198 host = ydb_service_settings.host
199 port = ydb_service_settings.grpc_port
200 output = subprocess.check_output(
201 [
202 str(ydb_cli),
203 '-e',
204 f'grpc://{host}:{port}',
205 '-d',
206 '/local',
207 'scheme',
208 'ls',
209 '-lR',
210 ],
211 encoding='utf-8',
212 )
213 tables = []
214
215 for line in output.split('\n'):
216 if ' table ' not in line:
217 continue
218 if '.sys' in line:
219 continue
220 if YDB_MIGRATION_TABLE in line:
221 continue
222 path = line.split('│')[6].strip()
223 tables.append(path)
224 return tables
225 except subprocess.CalledProcessError as exc:
226 raise Exception(f'Could not fetch table names:\n{exc}')
227
228
229@pytest.fixture(scope='session')
230def ydb_cli() -> pathlib.Path:
231 """
232 Path to YDB CLI executable.
233
234 Override this fixture to change the way YDB CLI is discovered.
235
236 @ingroup userver_testsuite_fixtures
237 """
238 try:
239 import yatest
240
241 return yatest.common.runtime.binary_path('contrib/ydb/apps/ydb/ydb')
242 except ImportError:
243 return 'ydb'
244
245
246@pytest.fixture(scope='session')
247def _ydb_prepare(
248 _ydb_client,
249 _ydb_service_schemas,
250 ydb_service_settings,
251 _ydb_state,
252 ydb_migration_dir,
253 goose_binary_path,
254):
255 if _ydb_service_schemas and ydb_migration_dir.exists():
256 raise Exception(
257 'Both ydb/schema and ydb/migrations exist, which are mutually exclusive',
258 )
259
260 # testsuite legacy
261 for schema_path in _ydb_service_schemas:
262 with open(schema_path) as fp:
263 tables_schemas = yaml.load(fp.read(), Loader=_YamlLoader)
264 for table_schema in tables_schemas:
265 client.drop_table(_ydb_client, table_schema['path'])
266 client.create_table(_ydb_client, table_schema)
267 _ydb_state.tables.append(table_schema['path'])
268
269 # goose
270 _ydb_migrate(ydb_service_settings, ydb_migration_dir, goose_binary_path)
271
272 _ydb_state.init = True
273
274
275@pytest.fixture(scope='session')
276def _ydb_tables(_ydb_state, _ydb_prepare, ydb_service_settings, ydb_cli):
277 tables = {
278 *_ydb_state.tables,
279 *_ydb_fetch_table_names(ydb_service_settings, ydb_cli),
280 }
281 return tuple(sorted(tables))
282
283
284@pytest.fixture
285def _ydb_init(
286 request,
287 _ydb_client,
288 _ydb_state,
289 ydb_service_settings,
290 _ydb_prepare,
291 _ydb_tables,
292 _ydb_client_pool,
293 load,
294):
295 def ydb_mark_queries(files=(), queries=()):
296 result_queries = []
297 for path in files:
298 result_queries.append(load(path))
299 result_queries.extend(queries)
300 return result_queries
301
302 def drop_table(table):
303 with _ydb_client_pool() as ydb_client:
304 ydb_client.execute('DELETE FROM `{}`'.format(table))
305
306 if _ydb_tables:
307 with concurrent.futures.ThreadPoolExecutor(
308 max_workers=len(_ydb_tables),
309 ) as executer:
310 executer.map(drop_table, _ydb_tables)
311
312 for mark in request.node.iter_markers('ydb'):
313 queries = ydb_mark_queries(**mark.kwargs)
314 for query in queries:
315 _ydb_client.execute(query)
316
317
318@pytest.fixture
319def userver_ydb_trx(testpoint) -> sql.RegisteredTrx:
320 """
321 The fixture maintains transaction fault injection state using
322 RegisteredTrx class.
323
324 @see pytest_userver.sql.RegisteredTrx
325
326 @snippet integration_tests/tests/test_trx_failure.py fault injection
327
328 @ingroup userver_testsuite_fixtures
329 """
330
331 registered = sql.RegisteredTrx()
332
333 @testpoint('ydb_trx_commit')
334 def _pg_trx_tp(data):
335 should_fail = registered.is_failure_enabled(data['trx_name'])
336 return {'trx_should_fail': should_fail}
337
338 return registered
339
340
341@pytest.fixture(scope='session')
342def userver_config_ydb(ydb_service_settings):
343 """
344 Returns a function that adjusts the static configuration file for testsuite.
345
346 For all `ydb.databases`, sets `endpoint` and `database` to the local test
347 YDB instance.
348
349 @ingroup userver_testsuite_fixtures
350 """
351
352 endpoint = f'{ydb_service_settings.host}:{ydb_service_settings.grpc_port}'
353 database = ('' if ydb_service_settings.database.startswith('/') else '/') + ydb_service_settings.database
354
355 def patch_config(config, config_vars):
356 ydb_component = config['components_manager']['components']['ydb']
357 if isinstance(ydb_component, str):
358 ydb_component = config_vars[ydb_component[1:]]
359 databases = ydb_component['databases']
360 for dbconfig in databases.values():
361 dbconfig['endpoint'] = endpoint
362 dbconfig['database'] = database
363
364 return patch_config