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)