543 A helper class for computing metric differences.
545 @see ClientMonitor.metrics_diff
547 Example with @ref pytest_userver.client.ClientMonitor.metrics_diff "await monitor_client.metrics_diff()":
548 @snippet samples/testsuite-support/tests/test_metrics.py metrics diff
550 @ingroup userver_testsuite
556 _client: ClientMonitor,
559 _labels: dict[str, str] |
None,
563 self._client = _client
565 self._prefix = _prefix
566 self._labels = _labels
567 self._sliced = _sliced
568 self._diff_gauge = _diff_gauge
576 def baseline(self) -> pytest_userver.metrics.MetricsSnapshot:
584 self.
_diff = _subtract_metrics_snapshots(
591 def current(self) -> pytest_userver.metrics.MetricsSnapshot:
592 assert self.
_current is not None,
'Set self.current first'
598 assert self.
_baseline is not None,
'Set self.baseline first'
599 self.
_diff = _subtract_metrics_snapshots(
606 def diff(self) -> pytest_userver.metrics.MetricsSnapshot:
607 assert self.
_diff is not None,
'Set self.current first'
613 subpath: str |
None =
None,
614 add_labels: dict[str, str] |
None =
None,
615 ) -> pytest_userver.metrics.MetricValue: ...
621 add_labels: dict[str, str] |
None,
624 ) -> pytest_userver.metrics.MetricValue | T: ...
628 subpath: str |
None =
None,
629 add_labels: dict[str, str] |
None =
None,
631 default: Any = _MISSING,
632 ) -> pytest_userver.metrics.MetricValue | Any:
634 Returns a single metric value at the specified path, prepending
635 the path provided at construction. If a dict of labels is provided,
636 does an exact match of labels, prepending the labels provided at construction.
638 If `default` is provided, it is returned instead of asserting when
639 the metric is not found.
641 @param subpath Suffix of the metric path; the path provided at construction is prepended
642 @param add_labels Labels that the metric must have in addition to the labels provided at construction
643 @param default An optional default value in case the metric is missing
644 @throws AssertionError if not one metric by path and no `default` is given
647 if default
is _MISSING:
648 return self.
diff.value_at(path, add_labels)
649 return self.
diff.value_at(path, add_labels, default=default)
651 async def fetch(self) -> pytest_userver.metrics.MetricsSnapshot:
653 Returns metric values from the service without mutating the differ.
655 return await self._client.metrics(
664 Fetches metric values from the service and stores them as `self.baseline`.
666 Useful as an alternative to `async with monitor_client.metrics_diff(...)`
667 when the diffing scope doesn't map cleanly onto a single `with` block.
673 Fetches metric values from the service and stores them as `self.current`,
674 updating `self.diff` accordingly.
676 @throws AssertionError if `self.baseline` was not set first
680 async def __aenter__(self) -> MetricsDiffer:
685 async def __aexit__(self, exc_type, exc, exc_tb) -> None:
692def _subtract_metrics_snapshots(
697 assert current._sliced_prefix == initial._sliced_prefix
698 assert current._sliced_labels == initial._sliced_labels
700 path: {_subtract_metrics(path, current_metric, initial, diff_gauge)
for current_metric
in current_group}
701 for path, current_group
in current.items()
703 diff._sliced_prefix = current._sliced_prefix
704 diff._sliced_labels = current._sliced_labels
708def _subtract_metrics(
714 initial_group = initial.get(path,
None)
715 if initial_group
is None:
716 return current_metric
717 initial_metric = next(
718 (x
for x
in initial_group
if x.labels == current_metric.labels),
721 if initial_metric
is None:
722 return current_metric
725 labels=current_metric.labels,
726 value=_subtract_metric_values(
727 current=current_metric,
728 initial=initial_metric,
729 diff_gauge=diff_gauge,
731 _type=current_metric.type(),
735def _subtract_metric_values(
739) -> pytest_userver.metrics.MetricValue:
740 assert current.type()
is not pytest_userver.metrics.MetricType.UNSPECIFIED
741 assert initial.type()
is not pytest_userver.metrics.MetricType.UNSPECIFIED
742 assert current.type() == initial.type()
746 return _subtract_metric_values_hist(current=current, initial=initial)
749 return _subtract_metric_values_num(
752 diff_gauge=diff_gauge,
756def _subtract_metric_values_num(
761 current_value = typing.cast(float, current.value)
762 initial_value = typing.cast(float, initial.value)
764 current.type()
is pytest_userver.metrics.MetricType.RATE
765 or initial.type()
is pytest_userver.metrics.MetricType.RATE
768 return current_value - initial_value
if should_diff
else current_value
771def _subtract_metric_values_hist(
777 assert current_value.bounds == initial_value.bounds
779 bounds=current_value.bounds,
780 buckets=[t[0] - t[1]
for t
in zip(current_value.buckets, initial_value.buckets, strict=
True)],
781 inf=current_value.inf - initial_value.inf,
785class AiohttpClient(service_client.AiohttpClient):
786 PeriodicTaskFailed = PeriodicTaskFailed
787 TestsuiteActionFailed = TestsuiteActionFailed
788 TestsuiteTaskNotFound = TestsuiteTaskNotFound
789 TestsuiteTaskConflict = TestsuiteTaskConflict
790 TestsuiteTaskFailed = TestsuiteTaskFailed
796 config: TestsuiteClientConfig,
798 log_capture_fixture: logcapture.CaptureServer,
801 cache_invalidation_state,
803 api_coverage_report=
None,
804 periodic_tasks_state: PeriodicTasksState |
None =
None,
805 allow_all_caches_invalidation: bool =
True,
810 super().__init__(base_url, span_id_header=span_id_header, **kwargs)
811 self._config = config
812 self._periodic_tasks = periodic_tasks_state
813 self._testpoint = testpoint
814 self._log_capture_fixture = log_capture_fixture
816 mocked_time=mocked_time,
817 testpoint=self._testpoint,
818 testpoint_control=testpoint_control,
819 invalidation_state=cache_invalidation_state,
820 cache_control=cache_control,
822 self._api_coverage_report = api_coverage_report
823 self._allow_all_caches_invalidation = allow_all_caches_invalidation
824 self._asyncexc_check = asyncexc_check
826 async def run_periodic(self, name) -> None:
827 await self.run_task(f
'periodic/{name}')
829 async def run_periodic_task(self, name):
830 warnings.warn(userver_warnings.WARN_PERIODIC_DEPRECATION, DeprecationWarning)
831 response = await self._testsuite_action(
'run_periodic_task', name=name)
832 if not response[
'status']:
833 raise self.PeriodicTaskFailed(f
'Periodic task {name} failed')
835 async def suspend_periodic_tasks(self, names: list[str]) ->
None:
836 if not self._periodic_tasks:
838 self._periodic_tasks.tasks_to_suspend.update(names)
839 await self._suspend_periodic_tasks()
841 async def resume_periodic_tasks(self, names: list[str]) ->
None:
842 warnings.warn(userver_warnings.WARN_PERIODIC_DEPRECATION, DeprecationWarning)
843 if not self._periodic_tasks:
845 self._periodic_tasks.tasks_to_suspend.difference_update(names)
846 await self._suspend_periodic_tasks()
848 async def resume_all_periodic_tasks(self) -> None:
849 if not self._periodic_tasks:
851 self._periodic_tasks.tasks_to_suspend.clear()
852 await self._suspend_periodic_tasks()
854 async def write_cache_dumps(
858 testsuite_skip_prepare=
False,
860 await self._testsuite_action(
863 testsuite_skip_prepare=testsuite_skip_prepare,
866 async def read_cache_dumps(
870 testsuite_skip_prepare=
False,
872 await self._testsuite_action(
875 testsuite_skip_prepare=testsuite_skip_prepare,
878 async def run_distlock_task(self, name: str) ->
None:
879 await self.run_task(f
'distlock/{name}')
881 async def reset_metrics(self) -> None:
882 await self._testsuite_action(
'reset_metrics')
884 async def metrics_portability(
887 prefix: str |
None =
None,
888 ) -> dict[str, list[dict[str, str]]]:
889 return await self._testsuite_action(
890 'metrics_portability',
894 async def list_tasks(self) -> list[str]:
895 response = await self._do_testsuite_action(
'tasks_list')
897 response.raise_for_status()
898 body = await response.json(content_type=
None)
901 async def run_task(self, name: str) ->
None:
902 response = await self._do_testsuite_action(
906 await _task_check_response(name, response)
908 @contextlib.asynccontextmanager
909 async def spawn_task(self, name: str):
910 task_id = await self._task_spawn(name)
914 await self._task_stop_spawned(task_id)
916 async def _task_spawn(self, name: str) -> str:
917 response = await self._do_testsuite_action(
921 data = await _task_check_response(name, response)
922 return data[
'task_id']
924 async def _task_stop_spawned(self, task_id: str) ->
None:
925 response = await self._do_testsuite_action(
927 json={
'task_id': task_id},
929 await _task_check_response(task_id, response)
931 async def http_allowed_urls_extra(
933 http_allowed_urls_extra: list[str],
935 await self._do_testsuite_action(
936 'http_allowed_urls_extra',
937 json={
'allowed_urls_extra': http_allowed_urls_extra},
938 testsuite_skip_prepare=
True,
941 @contextlib.asynccontextmanager
942 async def capture_logs(
945 log_level: str =
'DEBUG',
946 testsuite_skip_prepare: bool =
False,
948 async with self._log_capture_fixture.capture(
949 log_level=logcapture.LogLevel.from_string(log_level),
951 logger.debug(
'Starting logcapture')
952 await self._testsuite_action(
955 socket_logging_duplication=
True,
956 testsuite_skip_prepare=testsuite_skip_prepare,
960 await self._log_capture_fixture.wait_for_client()
963 await self._testsuite_action(
965 log_level=self._log_capture_fixture.default_log_level.name,
966 socket_logging_duplication=
False,
967 testsuite_skip_prepare=testsuite_skip_prepare,
970 async def log_flush(self, logger_name: str |
None =
None):
971 await self._testsuite_action(
973 logger_name=logger_name,
974 testsuite_skip_prepare=
True,
977 async def invalidate_caches(
980 clean_update: bool =
True,
981 cache_names: list[str] |
None =
None,
982 testsuite_skip_prepare: bool =
False,
984 if cache_names
is None and clean_update:
985 if self._allow_all_caches_invalidation:
986 warnings.warn(CACHE_INVALIDATION_MESSAGE, DeprecationWarning, stacklevel=2)
988 __tracebackhide__ =
True
989 raise RuntimeError(CACHE_INVALIDATION_MESSAGE)
991 if testsuite_skip_prepare:
992 await self._tests_control({
993 'invalidate_caches': {
994 'update_type': (
'full' if clean_update
else 'incremental'),
995 **({
'names': cache_names}
if cache_names
else {}),
999 await self.tests_control(
1000 invalidate_caches=
True,
1001 clean_update=clean_update,
1002 cache_names=cache_names,
1005 async def tests_control(
1008 invalidate_caches: bool =
True,
1009 clean_update: bool =
True,
1010 cache_names: list[str] |
None =
None,
1011 http_allowed_urls_extra: list[str] |
None =
None,
1012 ) -> dict[str, Any]:
1013 body: dict[str, Any] = self._state_manager.get_pending_update()
1015 if 'invalidate_caches' in body
and invalidate_caches:
1016 if not clean_update
or cache_names:
1018 'Manual cache invalidation leads to indirect initial full cache invalidation',
1020 await self._prepare()
1023 if invalidate_caches:
1024 body[
'invalidate_caches'] = {
1025 'update_type': (
'full' if clean_update
else 'incremental'),
1028 body[
'invalidate_caches'][
'names'] = cache_names
1030 if http_allowed_urls_extra
is not None:
1031 await self.http_allowed_urls_extra(http_allowed_urls_extra)
1033 return await self._tests_control(body)
1035 async def update_server_state(self) -> None:
1036 await self._prepare()
1038 async def enable_testpoints(self, *, no_auto_cache_cleanup=False) -> None:
1039 if not self._testpoint:
1041 if no_auto_cache_cleanup:
1042 await self._tests_control({
1043 'testpoints': sorted(self._testpoint.keys()),
1046 await self.update_server_state()
1048 async def get_dynamic_config_defaults(
1050 ) -> dict[str, Any]:
1051 return await self._testsuite_action(
1052 'get_dynamic_config_defaults',
1053 testsuite_skip_prepare=
True,
1056 async def _tests_control(self, body: dict) -> dict[str, Any]:
1057 with self._state_manager.updating_state(body):
1058 async with await self._do_testsuite_action(
1061 testsuite_skip_prepare=
True,
1063 if response.status == 404:
1065 'It seems that testsuite support is not enabled for your service',
1067 response.raise_for_status()
1068 return await response.json(content_type=
None)
1070 async def _suspend_periodic_tasks(self):
1071 if self._periodic_tasks.tasks_to_suspend != self._periodic_tasks.suspended_tasks:
1072 await self._testsuite_action(
1073 'suspend_periodic_tasks',
1074 names=sorted(self._periodic_tasks.tasks_to_suspend),
1076 self._periodic_tasks.suspended_tasks = set(
1077 self._periodic_tasks.tasks_to_suspend,
1080 def _do_testsuite_action(self, action, **kwargs):
1081 if not self._config.testsuite_action_path:
1083 'tests-control component is not properly configured',
1085 path = self._config.testsuite_action_path.format(action=action)
1086 return self.post(path, **kwargs)
1088 async def _testsuite_action(
1092 testsuite_skip_prepare=False,
1095 async with await self._do_testsuite_action(
1098 testsuite_skip_prepare=testsuite_skip_prepare,
1100 if response.status == 500:
1101 raise TestsuiteActionFailed
1102 response.raise_for_status()
1103 return await response.json(content_type=
None)
1105 async def _prepare(self) -> None:
1106 with self._state_manager.cache_control_update()
as pending_update:
1108 await self._tests_control(pending_update)
1114 headers: dict[str, str] |
None =
None,
1115 bearer: str |
None =
None,
1116 x_real_ip: str |
None =
None,
1118 testsuite_skip_prepare: bool =
False,
1120 ) -> aiohttp.ClientResponse:
1121 if self._asyncexc_check:
1123 self._asyncexc_check()
1125 if not testsuite_skip_prepare:
1126 await self._prepare()
1128 response = await super()._request(
1136 if self._api_coverage_report:
1137 self._api_coverage_report.update_usage_stat(
1141 response.content_type,
1144 if self._asyncexc_check:
1146 self._asyncexc_check()
1156 Asyncio userver client, typically retrieved from
1157 @ref service_client "plugins.service_client.service_client"
1160 Compatible with werkzeug interface.
1162 @ingroup userver_testsuite
1165 PeriodicTaskFailed = PeriodicTaskFailed
1166 TestsuiteActionFailed = TestsuiteActionFailed
1167 TestsuiteTaskNotFound = TestsuiteTaskNotFound
1168 TestsuiteTaskConflict = TestsuiteTaskConflict
1169 TestsuiteTaskFailed = TestsuiteTaskFailed
1171 def _wrap_client_response(self, response: aiohttp.ClientResponse) -> Awaitable[http.ClientResponse]:
1172 return http.wrap_client_response(
1174 json_loads=approx.json_loads,
1178 async def run_periodic(self, name) -> None:
1179 await self.
_client.run_periodic(name)
1182 async def run_periodic_task(self, name):
1183 warnings.warn(userver_warnings.WARN_PERIODIC_DEPRECATION, DeprecationWarning)
1184 await self.
_client.run_periodic_task(name)
1187 async def suspend_periodic_tasks(self, names: list[str]) ->
None:
1188 await self.
_client.suspend_periodic_tasks(names)
1191 async def resume_periodic_tasks(self, names: list[str]) ->
None:
1192 warnings.warn(userver_warnings.WARN_PERIODIC_DEPRECATION, DeprecationWarning)
1193 await self.
_client.resume_periodic_tasks(names)
1196 async def resume_all_periodic_tasks(self) -> None:
1197 await self.
_client.resume_all_periodic_tasks()
1200 async def write_cache_dumps(
1204 testsuite_skip_prepare=
False,
1206 await self.
_client.write_cache_dumps(
1208 testsuite_skip_prepare=testsuite_skip_prepare,
1212 async def read_cache_dumps(
1216 testsuite_skip_prepare=
False,
1218 await self.
_client.read_cache_dumps(
1220 testsuite_skip_prepare=testsuite_skip_prepare,
1223 async def run_task(self, name: str) ->
None:
1224 await self.
_client.run_task(name)
1226 async def run_distlock_task(self, name: str) ->
None:
1227 await self.
_client.run_distlock_task(name)
1231 Calls `ResetMetric(metric);` for each metric that has such C++ function.
1233 Note that using `reset_metrics()` is discouraged, prefer using a more reliable
1234 @ref pytest_userver.client.ClientMonitor.metrics_diff "await monitor_client.metrics_diff()".
1236 @snippet samples/testsuite-support/tests/test_metrics.py metrics reset
1243 prefix: str |
None =
None,
1244 ) -> dict[str, list[dict[str, str]]]:
1246 Reports metrics related issues that could be encountered on
1247 different monitoring systems.
1249 @sa @ref utils::statistics::GetPortabilityWarnings
1253 def list_tasks(self) -> list[str]:
1254 return self.
_client.list_tasks()
1256 def spawn_task(self, name: str):
1257 return self.
_client.spawn_task(name)
1262 log_level: str =
'DEBUG',
1263 testsuite_skip_prepare: bool =
False,
1266 Captures logs from the service.
1268 @param log_level Do not capture logs below this level.
1269 @param testsuite_skip_prepare An advanced parameter to skip auto-`update_server_state`.
1271 Example — filter captured logs after a request:
1273 @snippet samples/testsuite-support/tests/test_logcapture.py select
1275 Example — subscribe to log events as they arrive:
1277 @snippet samples/testsuite-support/tests/test_logcapture.py subscribe
1279 @see @ref testsuite_logs_capture
1282 log_level=log_level,
1283 testsuite_skip_prepare=testsuite_skip_prepare,
1296 clean_update: bool =
True,
1297 cache_names: list[str] |
None =
None,
1298 testsuite_skip_prepare: bool =
False,
1301 Send request to service to update caches.
1303 @param clean_update if False, service will do a faster incremental
1304 update of caches whenever possible.
1305 @param cache_names which caches specifically should be updated;
1307 @param testsuite_skip_prepare if False, service will automatically do
1308 update_server_state().
1310 __tracebackhide__ =
True
1312 clean_update=clean_update,
1313 cache_names=cache_names,
1314 testsuite_skip_prepare=testsuite_skip_prepare,
1318 async def tests_control(
1320 invalidate_caches: bool =
True,
1321 clean_update: bool =
True,
1322 cache_names: list[str] |
None =
None,
1323 http_allowed_urls_extra: list[str] |
None =
None,
1324 ) -> dict[str, Any]:
1325 return await self.
_client.tests_control(
1326 invalidate_caches=invalidate_caches,
1327 clean_update=clean_update,
1328 cache_names=cache_names,
1329 http_allowed_urls_extra=http_allowed_urls_extra,
1335 Update service-side state through http call to 'tests/control':
1336 - clear dirty (from other tests) caches
1337 - set service-side mocked time,
1338 - resume / suspend periodic tasks
1340 If service is up-to-date, does nothing.
1347 Send list of handled testpoint pats to service. For these paths service
1348 will no more skip http calls from TESTPOINT(...) macro.
1350 @param no_auto_cache_cleanup prevent automatic cache cleanup.
1351 When calling service client first time in scope of current test, client
1352 makes additional http call to `tests/control` to update caches, to get
1353 rid of data from previous test.
1358 async def get_dynamic_config_defaults(
1360 ) -> dict[str, Any]:
1361 return await self.
_client.get_dynamic_config_defaults()
1364@dataclasses.dataclass