2Python module that provides helpers for functional testing of metrics with
4@ref scripts/docs/en/userver/functional_testing.md for an introduction.
6@ingroup userver_testsuite
14@dataclasses.dataclass(frozen=True)
17 Metric type that contains the `labels: typing.Dict[str, str]` and
20 The type is hashable and comparable:
21 @snippet testsuite/tests/test_metrics.py values set
23 @ingroup userver_testsuite
26 labels: typing.Dict[str, str]
29 def __hash__(self) -> int:
33 """ Returns labels as a tuple of sorted items """
34 return tuple(sorted(self.
labels.items()))
39 if isinstance(o, Metric):
40 return dataclasses.asdict(o)
41 if isinstance(o, set):
43 return super().default(o)
48 Snapshot of captured metrics that mimics the dict interface. Metrics have
49 the 'Dict[str(path), Set[Metric]]' format.
51 @snippet samples/testsuite-support/tests/test_metrics.py metrics labels
53 @ingroup userver_testsuite
56 def __init__(self, values: typing.Mapping[str, typing.Set[Metric]]):
60 """ Returns a list of metrics by specified path """
64 """ Returns count of metrics paths """
68 """ Returns a (path, list) iterable over the metrics """
73 Returns True if metric with specified path is in the snapshot,
78 def __eq__(self, other: object) -> bool:
80 Compares the snapshot with a dict of metrics or with
85 def __repr__(self) -> str:
88 def get(self, path: str, default=
None):
90 Returns an list of metrics by path or default if there's no
96 """ Returns a (path, list) iterable over the metrics """
100 """ Returns an iterable over paths of metrics """
104 """ Returns an iterable over lists of metrics """
110 labels: typing.Optional[typing.Dict] =
None,
112 default: typing.Optional[float] =
None,
115 Returns a single metric value at specified path. If a dict of labels
116 is provided, does en exact match of labels (i.e. {} stands for no
117 labels; {'a': 'b', 'c': 'd'} matches only {'a': 'b', 'c': 'd'} or
118 {'c': 'd', 'a': 'b'} but neither match {'a': 'b'} nor
119 {'a': 'b', 'c': 'd', 'e': 'f'}).
121 @throws AssertionError if not one metric by path
123 entry = self.
get(path, set())
125 entry
or default
is not None
126 ), f
'No metrics found by path "{path}"'
128 if labels
is not None:
129 entry = {x
for x
in entry
if x.labels == labels}
131 entry
or default
is not None
132 ), f
'No metrics found by path "{path}" and labels {labels}'
133 assert len(entry) <= 1, (
134 f
'Multiple metrics found by path "{path}" and labels {labels}:'
140 ), f
'Multiple metrics found by path "{path}": {entry}'
142 if default
is not None and not entry:
144 return next(iter(entry)).value
148 other: typing.Mapping[str, typing.Set[Metric]],
150 ignore_zeros: bool =
False,
153 Compares the snapshot with a dict of metrics or with
154 another snapshot, displaying a nice diff on mismatch
156 lhs = _flatten_snapshot(self, ignore_zeros=ignore_zeros)
157 rhs = _flatten_snapshot(other, ignore_zeros=ignore_zeros)
158 assert lhs == rhs, _diff_metric_snapshots(lhs, rhs, ignore_zeros)
163 Construct MetricsSnapshot from a JSON string
167 Metric(labels=element[
'labels'], value=element[
'value'])
168 for element
in metrics_list
170 for path, metrics_list
in json.loads(json_str).
items()
176 Serialize to a JSON string
178 return json.dumps(self.
_values, cls=_MetricsJSONEncoder)
181_FlattenedSnapshot = typing.Set[typing.Tuple[str, Metric]]
184def _flatten_snapshot(values, ignore_zeros: bool) -> _FlattenedSnapshot:
187 for path, metrics
in values.items()
188 for metric
in metrics
189 if metric.value != 0
or not ignore_zeros
193def _diff_metric_snapshots(
194 lhs: _FlattenedSnapshot, rhs: _FlattenedSnapshot, ignore_zeros: bool,
196 def extra_metrics_message(extra, base):
198 f
' path={path!r} labels={metric.labels!r} value={metric.value}'
199 for path, metric
in sorted(extra, key=
lambda pair: pair[0])
200 if (path, metric)
not in base
204 lines = [
'left.assert_equals(right, ignore_zeros=True) failed']
206 lines = [
'left.assert_equals(right) failed']
207 actual_extra = extra_metrics_message(lhs, rhs)
209 lines.append(
' extra in left:')
210 lines += actual_extra
212 actual_gt = extra_metrics_message(rhs, lhs)
214 lines.append(
' missing in left:')
217 return '\n'.join(lines)