1"""Helper functions to perform checks.""" 2 3load("@bazel_skylib//lib:types.bzl", "types") 4load(":compare_util.bzl", "MatchResult", "compare_contains_exactly_predicates") 5load(":failure_messages.bzl", "format_failure_unexpected_values") 6load(":matching.bzl", "matching") 7load(":ordered.bzl", "IN_ORDER", "OrderedIncorrectly") 8load(":truth_common.bzl", "enumerate_list_as_lines", "maybe_sorted", "to_list") 9 10def check_contains_exactly( 11 *, 12 expect_contains, 13 actual_container, 14 format_actual, 15 format_expected, 16 format_missing, 17 format_unexpected, 18 format_out_of_order, 19 meta): 20 """Check that a collection contains exactly the given values and no more. 21 22 This checks that the collection contains exactly the given values. Extra 23 values are not allowed. Multiplicity of the expected values is respected. 24 Ordering is not checked; call `in_order()` to also check the order 25 of the actual values matches the order of the expected values. 26 27 Args: 28 expect_contains: the values that must exist (and no more). 29 actual_container: the values to check within. 30 format_actual: (callable) accepts no args and returns [`str`] (the 31 description of the actual values). 32 format_expected: (callable) accepts no args and returns [`str`] ( 33 description of the expected values). 34 format_missing: (callable) accepts 1 position arg (list of values from 35 `expect_contains` that were missing), and returns [`str`] (description of 36 the missing values). 37 format_unexpected: (callable) accepts 1 positional arg (list of values from 38 `actual_container` that weren't expected), and returns [`str`] (description of 39 the unexpected values). 40 format_out_of_order: (callable) accepts 1 arg (a list of "MatchResult" 41 structs, see above) and returns a string (the problem message 42 reported on failure). The order of match results is the expected 43 order. 44 meta: ([`ExpectMeta`]) to record failures. 45 46 Returns: 47 [`Ordered`] object. 48 """ 49 result = compare_contains_exactly_predicates( 50 expect_contains = [ 51 matching.equals_wrapper(raw_expected) 52 for raw_expected in expect_contains 53 ], 54 actual_container = actual_container, 55 ) 56 if not result.contains_exactly: 57 problems = [] 58 if result.missing: 59 problems.append(format_missing([m.desc for m in result.missing])) 60 if result.unexpected: 61 problems.append(format_unexpected(result.unexpected)) 62 problems.append(format_expected()) 63 64 meta.add_failure("\n".join(problems), format_actual()) 65 66 # We already recorded an error, so just pretend order is correct to 67 # avoid spamming another error. 68 return IN_ORDER 69 elif result.is_in_order: 70 return IN_ORDER 71 else: 72 return OrderedIncorrectly.new( 73 format_problem = lambda: format_out_of_order(result.matches), 74 format_actual = format_actual, 75 meta = meta, 76 ) 77 78def check_contains_exactly_predicates( 79 *, 80 expect_contains, 81 actual_container, 82 format_actual, 83 format_expected, 84 format_missing, 85 format_unexpected, 86 format_out_of_order, 87 meta): 88 """Check that a collection contains values matching the given predicates and no more. 89 90 todo doc to describe behavior 91 This checks that the collection contains values that match the given exactly the given values. 92 Extra values that do not match a predicate are not allowed. Multiplicity of 93 the expected predicates is respected. Ordering is not checked; call 94 `in_order()` to also check the order of the actual values matches the order 95 of the expected predicates. 96 97 Args: 98 expect_contains: the predicates that must match (and no more). 99 actual_container: the values to check within. 100 format_actual: (callable) accepts no args and returns [`str`] (the 101 description of the actual values). 102 format_expected: (callable) accepts no args and returns [`str`] ( 103 description of the expected values). 104 format_missing: (callable) accepts 1 position arg (list of values from 105 `expect_contains` that were missing), and returns [`str`] (description of 106 the missing values). 107 format_unexpected: (callable) accepts 1 positional arg (list of values from 108 `actual_container` that weren't expected), and returns [`str`] (description of 109 the unexpected values). 110 format_out_of_order: (callable) accepts 1 arg (a list of "MatchResult" 111 structs, see above) and returns a string (the problem message 112 reported on failure). The order of match results is the expected 113 order. 114 meta: ([`ExpectMeta`]) to record failures. 115 116 Returns: 117 [`Ordered`] object. 118 """ 119 result = compare_contains_exactly_predicates( 120 expect_contains = expect_contains, 121 actual_container = actual_container, 122 ) 123 if not result.contains_exactly: 124 problems = [] 125 if result.missing: 126 problems.append(format_missing(result.missing)) 127 if result.unexpected: 128 problems.append(format_unexpected(result.unexpected)) 129 problems.append(format_expected()) 130 131 meta.add_failure("\n".join(problems), format_actual()) 132 133 # We already recorded an error, so just pretend order is correct to 134 # avoid spamming another error. 135 return IN_ORDER 136 elif result.is_in_order: 137 return IN_ORDER 138 else: 139 return OrderedIncorrectly.new( 140 format_problem = lambda: format_out_of_order(result.matches), 141 format_actual = format_actual, 142 meta = meta, 143 ) 144 145def check_contains_predicate(collection, matcher, *, format_problem, format_actual, meta): 146 """Check that `matcher` matches any value in `collection`, and record an error if not. 147 148 Args: 149 collection: ([`collection`]) the collection whose values are compared against. 150 matcher: ([`Matcher`]) that must match. 151 format_problem: ([`str`] | callable) If a string, then the problem message 152 to use when failing. If a callable, a no-arg callable that returns 153 the problem string; see `_format_problem_*` for existing helpers. 154 format_actual: ([`str`] | callable) If a string, then the actual message 155 to use when failing. If a callable, a no-arg callable that returns 156 the actual string; see `_format_actual_*` for existing helpers. 157 meta: ([`ExpectMeta`]) to record failures 158 """ 159 for value in collection: 160 if matcher.match(value): 161 return 162 meta.add_failure( 163 format_problem if types.is_string(format_problem) else format_problem(), 164 format_actual if types.is_string(format_actual) else format_actual(), 165 ) 166 167def check_contains_at_least_predicates( 168 collection, 169 matchers, 170 *, 171 format_missing, 172 format_out_of_order, 173 format_actual, 174 meta): 175 """Check that the collection is a subset of the predicates. 176 177 The collection must match all the predicates. It can contain extra elements. 178 The multiplicity of matchers is respected. Checking that the relative order 179 of matches is the same as the passed-in matchers order can done by calling 180 `in_order()`. 181 182 Args: 183 collection: [`collection`] of values to check within. 184 matchers: [`collection`] of [`Matcher`] objects to match (see `matchers` struct) 185 format_missing: (callable) accepts 1 positional arg (a list of the 186 `matchers` that did not match) and returns a string (the problem 187 message reported on failure). 188 format_out_of_order: (callable) accepts 1 arg (a list of `MatchResult`s) 189 and returns a string (the problem message reported on failure). The 190 order of match results is the expected order. 191 format_actual: callable: accepts no args and returns a string (the 192 text describing the actual value reported on failure). 193 meta: ([`ExpectMeta`]) used for reporting errors. 194 195 Returns: 196 [`Ordered`] object to allow checking the order of matches. 197 """ 198 199 # We'll later update this list in-place with results. We keep the order 200 # so that, on failure, the formatters receive the expected order of matches. 201 matches = [None for _ in matchers] 202 203 # A list of (original position, matcher) tuples. This allows 204 # mapping a matcher back to its original order and respecting 205 # the multiplicity of matchers. 206 remaining_matchers = enumerate(matchers) 207 ordered = True 208 for absolute_pos, value in enumerate(collection): 209 if not remaining_matchers: 210 break 211 found_i = -1 212 for cur_i, (_, matcher) in enumerate(remaining_matchers): 213 if matcher.match(value): 214 found_i = cur_i 215 break 216 if found_i > -1: 217 ordered = ordered and (found_i == 0) 218 orig_matcher_pos, matcher = remaining_matchers.pop(found_i) 219 matches[orig_matcher_pos] = MatchResult.new( 220 matched_value = value, 221 found_at = absolute_pos, 222 matcher = matcher, 223 ) 224 225 if remaining_matchers: 226 meta.add_failure( 227 format_missing([v[1] for v in remaining_matchers]), 228 format_actual if types.is_string(format_actual) else format_actual(), 229 ) 230 231 # We've added a failure, so no need to spam another error message, so 232 # just pretend things are in order. 233 return IN_ORDER 234 elif ordered: 235 return IN_ORDER 236 else: 237 return OrderedIncorrectly.new( 238 format_problem = lambda: format_out_of_order(matches), 239 format_actual = format_actual, 240 meta = meta, 241 ) 242 243def check_contains_none_of(*, collection, none_of, meta, sort = True): 244 """Check that a collection does not have any of the `none_of` values. 245 246 Args: 247 collection: ([`collection`]) the values to check within. 248 none_of: the values that should not exist. 249 meta: ([`ExpectMeta`]) to record failures. 250 sort: ([`bool`]) If true, sort the values for display. 251 """ 252 unexpected = [] 253 for value in none_of: 254 if value in collection: 255 unexpected.append(value) 256 if not unexpected: 257 return 258 259 unexpected = maybe_sorted(unexpected, sort) 260 problem, actual = format_failure_unexpected_values( 261 none_of = "\n" + enumerate_list_as_lines(unexpected, prefix = " "), 262 unexpected = unexpected, 263 actual = collection, 264 sort = sort, 265 ) 266 meta.add_failure(problem, actual) 267 268def check_not_contains_predicate(collection, matcher, *, meta, sort = True): 269 """Check that `matcher` matches no values in `collection`. 270 271 Args: 272 collection: ([`collection`]) the collection whose values are compared against. 273 matcher: ([`Matcher`]) that must not match. 274 meta: ([`ExpectMeta`]) to record failures 275 sort: ([`bool`]) If `True`, the collection will be sorted for display. 276 """ 277 matches = maybe_sorted([v for v in collection if matcher.match(v)], sort) 278 if not matches: 279 return 280 problem, actual = format_failure_unexpected_values( 281 none_of = matcher.desc, 282 unexpected = matches, 283 actual = collection, 284 sort = sort, 285 ) 286 meta.add_failure(problem, actual) 287 288def common_subject_is_in(self, any_of): 289 """Generic implementation of `Subject.is_in` 290 291 Args: 292 self: The subject object. It must provide `actual` and `meta` 293 attributes. 294 any_of: [`collection`] of values. 295 """ 296 return _check_is_in(self.actual, to_list(any_of), self.meta) 297 298def _check_is_in(actual, any_of, meta): 299 """Check that `actual` is one of the values in `any_of`. 300 301 Args: 302 actual: value to check for in `any_of` 303 any_of: [`collection`] of values to check within. 304 meta: ([`ExpectMeta`]) to record failures 305 """ 306 if actual in any_of: 307 return 308 meta.add_failure( 309 "expected any of:\n{}".format( 310 enumerate_list_as_lines(any_of, prefix = " "), 311 ), 312 "actual: {}".format(actual), 313 ) 314 315def check_not_equals(*, unexpected, actual, meta): 316 """Check that the values are the same type and not equal (according to !=). 317 318 NOTE: This requires the same type for both values. This is to prevent 319 mistakes where different data types (usually) can never be equal. 320 321 Args: 322 unexpected: (object) the value that actual cannot equal 323 actual: (object) the observed value 324 meta: ([`ExpectMeta`]) to record failures 325 """ 326 same_type = type(actual) == type(unexpected) 327 equal = not (actual != unexpected) # Use != to preserve semantics 328 if same_type and not equal: 329 return 330 if not same_type: 331 meta.add_failure( 332 "expected not to be: {} (type: {})".format(unexpected, type(unexpected)), 333 "actual: {} (type: {})".format(actual, type(actual)), 334 ) 335 else: 336 meta.add_failure( 337 "expected not to be: {}".format(unexpected), 338 "actual: {}".format(actual), 339 ) 340