github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/testing/metric_result_matchers.py (about)

     1  #
     2  # Licensed to the Apache Software Foundation (ASF) under one or more
     3  # contributor license agreements.  See the NOTICE file distributed with
     4  # this work for additional information regarding copyright ownership.
     5  # The ASF licenses this file to You under the Apache License, Version 2.0
     6  # (the "License"); you may not use this file except in compliance with
     7  # the License.  You may obtain a copy of the License at
     8  #
     9  #    http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  #
    17  
    18  """MetricResult matchers for validating metrics in PipelineResults.
    19  
    20  example usage:
    21  ::
    22  
    23      result = my_pipeline.run()
    24      all_metrics = result.metrics().all_metrics()
    25  
    26      matchers = [
    27        MetricResultMatcher(
    28            namespace='myNamespace',
    29            name='myName',
    30            step='myStep',
    31            labels={
    32                'pcollection': 'myCollection',
    33                'myCustomKey': 'myCustomValue'
    34            },
    35            attempted=42,
    36            committed=42
    37        )
    38      ]
    39      errors = metric_result_matchers.verify_all(all_metrics, matchers)
    40      self.assertFalse(errors, errors)
    41  
    42  """
    43  
    44  # pytype: skip-file
    45  
    46  from hamcrest import equal_to
    47  from hamcrest.core import string_description
    48  from hamcrest.core.base_matcher import BaseMatcher
    49  from hamcrest.core.matcher import Matcher
    50  
    51  from apache_beam.metrics.cells import DistributionResult
    52  
    53  
    54  def _matcher_or_equal_to(value_or_matcher):
    55    """Pass-thru for matchers, and wraps value inputs in an equal_to matcher."""
    56    if value_or_matcher is None:
    57      return None
    58    if isinstance(value_or_matcher, Matcher):
    59      return value_or_matcher
    60    return equal_to(value_or_matcher)
    61  
    62  
    63  class MetricResultMatcher(BaseMatcher):
    64    """A PyHamcrest matcher that validates counter MetricResults."""
    65    def __init__(
    66        self,
    67        namespace=None,
    68        name=None,
    69        step=None,
    70        labels=None,
    71        attempted=None,
    72        committed=None,
    73        sum_value=None,
    74        count_value=None,
    75        min_value=None,
    76        max_value=None,
    77    ):
    78      self.namespace = _matcher_or_equal_to(namespace)
    79      self.name = _matcher_or_equal_to(name)
    80      self.step = _matcher_or_equal_to(step)
    81      self.attempted = _matcher_or_equal_to(attempted)
    82      self.committed = _matcher_or_equal_to(committed)
    83      labels = labels or {}
    84      self.label_matchers = {}
    85      for (k, v) in labels.items():
    86        self.label_matchers[_matcher_or_equal_to(k)] = _matcher_or_equal_to(v)
    87  
    88    def _matches(self, metric_result):
    89      if self.namespace is not None and not self.namespace.matches(
    90          metric_result.key.metric.namespace):
    91        return False
    92      if self.name and not self.name.matches(metric_result.key.metric.name):
    93        return False
    94      if self.step and not self.step.matches(metric_result.key.step):
    95        return False
    96      if (self.attempted is not None and
    97          not self.attempted.matches(metric_result.attempted)):
    98        return False
    99      if (self.committed is not None and
   100          not self.committed.matches(metric_result.committed)):
   101        return False
   102      for (k_matcher, v_matcher) in self.label_matchers.items():
   103        matched_keys = [
   104            key for key in metric_result.key.labels.keys()
   105            if k_matcher.matches(key)
   106        ]
   107        matched_key = matched_keys[0] if matched_keys else None
   108        if not matched_key:
   109          return False
   110        label_value = metric_result.key.labels[matched_key]
   111        if not v_matcher.matches(label_value):
   112          return False
   113      return True
   114  
   115    def describe_to(self, description):
   116      if self.namespace:
   117        description.append_text(' namespace: ')
   118        self.namespace.describe_to(description)
   119      if self.name:
   120        description.append_text(' name: ')
   121        self.name.describe_to(description)
   122      if self.step:
   123        description.append_text(' step: ')
   124        self.step.describe_to(description)
   125      for (k_matcher, v_matcher) in self.label_matchers.items():
   126        description.append_text(' (label_key: ')
   127        k_matcher.describe_to(description)
   128        description.append_text(' label_value: ')
   129        v_matcher.describe_to(description)
   130        description.append_text('). ')
   131      if self.attempted is not None:
   132        description.append_text(' attempted: ')
   133        self.attempted.describe_to(description)
   134      if self.committed is not None:
   135        description.append_text(' committed: ')
   136        self.committed.describe_to(description)
   137  
   138    def describe_mismatch(self, metric_result, mismatch_description):
   139      mismatch_description.append_text("was").append_value(metric_result)
   140  
   141  
   142  class DistributionMatcher(BaseMatcher):
   143    """A PyHamcrest matcher that validates counter distributions."""
   144    def __init__(
   145        self, sum_value=None, count_value=None, min_value=None, max_value=None):
   146      self.sum_value = _matcher_or_equal_to(sum_value)
   147      self.count_value = _matcher_or_equal_to(count_value)
   148      self.min_value = _matcher_or_equal_to(min_value)
   149      self.max_value = _matcher_or_equal_to(max_value)
   150  
   151    def _matches(self, distribution_result):
   152      if not isinstance(distribution_result, DistributionResult):
   153        return False
   154      if self.sum_value and not self.sum_value.matches(distribution_result.sum):
   155        return False
   156      if self.count_value and not self.count_value.matches(
   157          distribution_result.count):
   158        return False
   159      if self.min_value and not self.min_value.matches(distribution_result.min):
   160        return False
   161      if self.max_value and not self.max_value.matches(distribution_result.max):
   162        return False
   163      return True
   164  
   165    def describe_to(self, description):
   166      if self.sum_value:
   167        description.append_text(' sum_value: ')
   168        self.sum_value.describe_to(description)
   169      if self.count_value:
   170        description.append_text(' count_value: ')
   171        self.count_value.describe_to(description)
   172      if self.min_value:
   173        description.append_text(' min_value: ')
   174        self.min_value.describe_to(description)
   175      if self.max_value:
   176        description.append_text(' max_value: ')
   177        self.max_value.describe_to(description)
   178  
   179    def describe_mismatch(self, distribution_result, mismatch_description):
   180      mismatch_description.append_text('was').append_value(distribution_result)
   181  
   182  
   183  def verify_all(all_metrics, matchers):
   184    """Verified that every matcher matches a metric result in all_metrics."""
   185    errors = []
   186    matched_metrics = []
   187    for matcher in matchers:
   188      matched_metrics = [mr for mr in all_metrics if matcher.matches(mr)]
   189      if not matched_metrics:
   190        errors.append(
   191            'Unable to match metrics for matcher %s' %
   192            (string_description.tostring(matcher)))
   193    if errors:
   194      errors.append(
   195          '\nActual MetricResults:\n' + '\n'.join([str(mr)
   196                                                   for mr in all_metrics]))
   197    return ''.join(errors)