1# Copyright 2023 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""# CollectionSubject""" 16 17load( 18 ":check_util.bzl", 19 "check_contains_at_least_predicates", 20 "check_contains_exactly", 21 "check_contains_exactly_predicates", 22 "check_contains_none_of", 23 "check_contains_predicate", 24 "check_not_contains_predicate", 25) 26load( 27 ":failure_messages.bzl", 28 "format_actual_collection", 29 "format_problem_expected_exactly", 30 "format_problem_matched_out_of_order", 31 "format_problem_missing_required_values", 32 "format_problem_predicates_did_not_match", 33 "format_problem_unexpected_values", 34) 35load(":int_subject.bzl", "IntSubject") 36load(":matching.bzl", "matching") 37load(":truth_common.bzl", "to_list") 38load(":util.bzl", "get_function_name") 39 40def _identity(v): 41 return v 42 43def _always_true(v): 44 _ = v # @unused 45 return True 46 47def _collection_subject_new( 48 values, 49 meta, 50 container_name = "values", 51 sortable = True, 52 element_plural_name = "elements"): 53 """Creates a "CollectionSubject" struct. 54 55 Method: CollectionSubject.new 56 57 Public Attributes: 58 * `actual`: The wrapped collection. 59 60 Args: 61 values: ([`collection`]) the values to assert against. 62 meta: ([`ExpectMeta`]) the metadata about the call chain. 63 container_name: ([`str`]) conceptual name of the container. 64 sortable: ([`bool`]) True if output should be sorted for display, False if not. 65 element_plural_name: ([`str`]) the plural word for the values in the container. 66 67 Returns: 68 [`CollectionSubject`]. 69 """ 70 71 # buildifier: disable=uninitialized 72 public = struct( 73 # keep sorted start 74 actual = values, 75 contains = lambda *a, **k: _collection_subject_contains(self, *a, **k), 76 contains_at_least = lambda *a, **k: _collection_subject_contains_at_least(self, *a, **k), 77 contains_at_least_predicates = lambda *a, **k: _collection_subject_contains_at_least_predicates(self, *a, **k), 78 contains_exactly = lambda *a, **k: _collection_subject_contains_exactly(self, *a, **k), 79 contains_exactly_predicates = lambda *a, **k: _collection_subject_contains_exactly_predicates(self, *a, **k), 80 contains_none_of = lambda *a, **k: _collection_subject_contains_none_of(self, *a, **k), 81 contains_predicate = lambda *a, **k: _collection_subject_contains_predicate(self, *a, **k), 82 has_size = lambda *a, **k: _collection_subject_has_size(self, *a, **k), 83 not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k), 84 not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k), 85 offset = lambda *a, **k: _collection_subject_offset(self, *a, **k), 86 transform = lambda *a, **k: _collection_subject_transform(self, *a, **k), 87 # keep sorted end 88 ) 89 self = struct( 90 actual = values, 91 meta = meta, 92 element_plural_name = element_plural_name, 93 container_name = container_name, 94 sortable = sortable, 95 contains_predicate = public.contains_predicate, 96 contains_at_least_predicates = public.contains_at_least_predicates, 97 ) 98 return public 99 100def _collection_subject_has_size(self, expected): 101 """Asserts that `expected` is the size of the collection. 102 103 Method: CollectionSubject.has_size 104 105 Args: 106 self: implicitly added. 107 expected: ([`int`]) the expected size of the collection. 108 """ 109 return IntSubject.new( 110 len(self.actual), 111 meta = self.meta.derive("size()"), 112 ).equals(expected) 113 114def _collection_subject_contains(self, expected): 115 """Asserts that `expected` is within the collection. 116 117 Method: CollectionSubject.contains 118 119 Args: 120 self: implicitly added. 121 expected: ([`str`]) the value that must be present. 122 """ 123 matcher = matching.equals_wrapper(expected) 124 return self.contains_predicate(matcher) 125 126def _collection_subject_contains_exactly(self, expected): 127 """Check that a collection contains exactly the given elements. 128 129 Method: CollectionSubject.contains_exactly 130 131 * Multiplicity is respected. 132 * The collection must contain all the values, no more or less. 133 * Checking that the order of matches is the same as the passed-in matchers 134 order can be done by call `in_order()`. 135 136 The collection must contain all the values and no more. Multiplicity of 137 values is respected. Checking that the order of matches is the same as the 138 passed-in matchers order can done by calling `in_order()`. 139 140 Args: 141 self: implicitly added. 142 expected: ([`list`]) values that must exist. 143 144 Returns: 145 [`Ordered`] (see `_ordered_incorrectly_new`). 146 """ 147 expected = to_list(expected) 148 return check_contains_exactly( 149 actual_container = self.actual, 150 expect_contains = expected, 151 meta = self.meta, 152 format_actual = lambda: format_actual_collection( 153 self.actual, 154 name = self.container_name, 155 sort = False, # Don't sort; this might be rendered by the in_order() error. 156 ), 157 format_expected = lambda: format_problem_expected_exactly( 158 expected, 159 sort = False, # Don't sort; this might be rendered by the in_order() error. 160 ), 161 format_missing = lambda missing: format_problem_missing_required_values( 162 missing, 163 sort = self.sortable, 164 ), 165 format_unexpected = lambda unexpected: format_problem_unexpected_values( 166 unexpected, 167 sort = self.sortable, 168 ), 169 format_out_of_order = format_problem_matched_out_of_order, 170 ) 171 172def _collection_subject_contains_exactly_predicates(self, expected): 173 """Check that the values correspond 1:1 to the predicates. 174 175 Method: CollectionSubject.contains_exactly_predicates 176 177 * There must be a 1:1 correspondence between the container values and the 178 predicates. 179 * Multiplicity is respected (i.e., if the same predicate occurs twice, then 180 two distinct elements must match). 181 * Matching occurs in first-seen order. That is, a predicate will "consume" 182 the first value in `actual_container` it matches. 183 * The collection must match all the predicates, no more or less. 184 * Checking that the order of matches is the same as the passed-in matchers 185 order can be done by call `in_order()`. 186 187 Note that confusing results may occur if predicates with overlapping 188 match conditions are used. For example, given: 189 actual=["a", "ab", "abc"], 190 predicates=[<contains a>, <contains b>, <equals a>] 191 192 Then the result will be they aren't equal: the first two predicates 193 consume "a" and "ab", leaving only "abc" for the <equals a> predicate 194 to match against, which fails. 195 196 Args: 197 self: implicitly added. 198 expected: ([`list`] of [`Matcher`]) that must match. 199 200 Returns: 201 [`Ordered`] (see `_ordered_incorrectly_new`). 202 """ 203 expected = to_list(expected) 204 return check_contains_exactly_predicates( 205 actual_container = self.actual, 206 expect_contains = expected, 207 meta = self.meta, 208 format_actual = lambda: format_actual_collection( 209 self.actual, 210 name = self.container_name, 211 sort = False, # Don't sort; this might be rendered by the in_order() error. 212 ), 213 format_expected = lambda: format_problem_expected_exactly( 214 [e.desc for e in expected], 215 sort = False, # Don't sort; this might be rendered by the in_order() error. 216 ), 217 format_missing = lambda missing: format_problem_missing_required_values( 218 [m.desc for m in missing], 219 sort = self.sortable, 220 ), 221 format_unexpected = lambda unexpected: format_problem_unexpected_values( 222 unexpected, 223 sort = self.sortable, 224 ), 225 format_out_of_order = format_problem_matched_out_of_order, 226 ) 227 228def _collection_subject_contains_none_of(self, values): 229 """Asserts the collection contains none of `values`. 230 231 Method: CollectionSubject.contains_none_of 232 233 Args: 234 self: implicitly added 235 values: ([`collection`]) values of which none of are allowed to exist. 236 """ 237 check_contains_none_of( 238 collection = self.actual, 239 none_of = values, 240 meta = self.meta, 241 sort = self.sortable, 242 ) 243 244def _collection_subject_contains_predicate(self, matcher): 245 """Asserts that `matcher` matches at least one value. 246 247 Method: CollectionSubject.contains_predicate 248 249 Args: 250 self: implicitly added. 251 matcher: ([`Matcher`]) (see `matchers` struct). 252 """ 253 check_contains_predicate( 254 self.actual, 255 matcher = matcher, 256 format_problem = "expected to contain: {}".format(matcher.desc), 257 format_actual = lambda: format_actual_collection( 258 self.actual, 259 name = self.container_name, 260 sort = self.sortable, 261 ), 262 meta = self.meta, 263 ) 264 265def _collection_subject_contains_at_least(self, expect_contains): 266 """Assert that the collection is a subset of the given predicates. 267 268 Method: CollectionSubject.contains_at_least 269 270 The collection must contain all the values. It can contain extra elements. 271 The multiplicity of values is respected. Checking that the relative order 272 of matches is the same as the passed-in expected values order can done by 273 calling `in_order()`. 274 275 Args: 276 self: implicitly added. 277 expect_contains: ([`list`]) values that must be in the collection. 278 279 Returns: 280 [`Ordered`] (see `_ordered_incorrectly_new`). 281 """ 282 matchers = [ 283 matching.equals_wrapper(expected) 284 for expected in to_list(expect_contains) 285 ] 286 return self.contains_at_least_predicates(matchers) 287 288def _collection_subject_contains_at_least_predicates(self, matchers): 289 """Assert that the collection is a subset of the given predicates. 290 291 Method: CollectionSubject.contains_at_least_predicates 292 293 The collection must match all the predicates. It can contain extra elements. 294 The multiplicity of matchers is respected. Checking that the relative order 295 of matches is the same as the passed-in matchers order can done by calling 296 `in_order()`. 297 298 Args: 299 self: implicitly added. 300 matchers: ([`list`] of [`Matcher`]) (see `matchers` struct). 301 302 Returns: 303 [`Ordered`] (see `_ordered_incorrectly_new`). 304 """ 305 ordered = check_contains_at_least_predicates( 306 self.actual, 307 matchers, 308 format_missing = lambda missing: format_problem_predicates_did_not_match( 309 missing, 310 element_plural_name = self.element_plural_name, 311 container_name = self.container_name, 312 ), 313 format_out_of_order = format_problem_matched_out_of_order, 314 format_actual = lambda: format_actual_collection( 315 self.actual, 316 name = self.container_name, 317 sort = self.sortable, 318 ), 319 meta = self.meta, 320 ) 321 return ordered 322 323def _collection_subject_not_contains(self, value): 324 check_not_contains_predicate( 325 self.actual, 326 matcher = matching.equals_wrapper(value), 327 meta = self.meta, 328 sort = self.sortable, 329 ) 330 331def _collection_subject_not_contains_predicate(self, matcher): 332 """Asserts that `matcher` matches no values in the collection. 333 334 Method: CollectionSubject.not_contains_predicate 335 336 Args: 337 self: implicitly added. 338 matcher: [`Matcher`] object (see `matchers` struct). 339 """ 340 check_not_contains_predicate( 341 self.actual, 342 matcher = matcher, 343 meta = self.meta, 344 sort = self.sortable, 345 ) 346 347def _collection_subject_offset(self, offset, factory): 348 """Fetches an element from the collection as a subject. 349 350 Args: 351 self: implicitly added. 352 offset: ([`int`]) the offset to fetch 353 factory: ([`callable`]). The factory function to use to create 354 the subject for the offset's value. It must have the following 355 signature: `def factory(value, *, meta)`. 356 357 Returns: 358 Object created by `factory`. 359 """ 360 value = self.actual[offset] 361 return factory( 362 value, 363 meta = self.meta.derive("offset({})".format(offset)), 364 ) 365 366def _collection_subject_transform( 367 self, 368 desc = None, 369 *, 370 map_each = None, 371 loop = None, 372 filter = None): 373 """Transforms a collections's value and returns another CollectionSubject. 374 375 This is equivalent to applying a list comprehension over the collection values, 376 but takes care of propagating context information and wrapping the value 377 in a `CollectionSubject`. 378 379 `transform(map_each=M, loop=L, filter=F)` is equivalent to 380 `[M(v) for v in L(collection) if F(v)]`. 381 382 Args: 383 self: implicitly added. 384 desc: (optional [`str`]) a human-friendly description of the transform 385 for use in error messages. Required when a description can't be 386 inferred from the other args. The description can be inferred if the 387 filter arg is a named function (non-lambda) or Matcher object. 388 map_each: (optional [`callable`]) function to transform an element in 389 the collection. It takes one positional arg, the loop's 390 current iteration value, and its return value will be the element's 391 new value. If not specified, the values from the loop iteration are 392 returned unchanged. 393 loop: (optional [`callable`]) function to produce values from the 394 original collection and whose values are iterated over. It takes one 395 positional arg, which is the original collection. If not specified, 396 the original collection values are iterated over. 397 filter: (optional [`callable`]) function that decides what values are 398 passed onto `map_each` for inclusion in the final result. It takes 399 one positional arg, the value to match (which is the current 400 iteration value before `map_each` is applied), and returns a bool 401 (True if the value should be included in the result, False if it 402 should be skipped). 403 404 Returns: 405 [`CollectionSubject`] of the transformed values. 406 """ 407 if not desc: 408 if map_each or loop: 409 fail("description required when map_each or loop used") 410 411 if matching.is_matcher(filter): 412 desc = "filter=" + filter.desc 413 else: 414 func_name = get_function_name(filter) 415 if func_name == "lambda": 416 fail("description required: description cannot be " + 417 "inferred from lambdas. Explicitly specify the " + 418 "description, use a named function for the filter, " + 419 "or use a Matcher for the filter.") 420 else: 421 desc = "filter={}(...)".format(func_name) 422 423 map_each = map_each or _identity 424 loop = loop or _identity 425 426 if filter: 427 if matching.is_matcher(filter): 428 filter_func = filter.match 429 else: 430 filter_func = filter 431 else: 432 filter_func = _always_true 433 434 new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)] 435 436 return _collection_subject_new( 437 new_values, 438 meta = self.meta.derive( 439 "transform()", 440 details = ["transform: {}".format(desc)], 441 ), 442 container_name = self.container_name, 443 sortable = self.sortable, 444 element_plural_name = self.element_plural_name, 445 ) 446 447# We use this name so it shows up nice in docs. 448# buildifier: disable=name-conventions 449CollectionSubject = struct( 450 # keep sorted start 451 contains = _collection_subject_contains, 452 contains_at_least = _collection_subject_contains_at_least, 453 contains_at_least_predicates = _collection_subject_contains_at_least_predicates, 454 contains_exactly = _collection_subject_contains_exactly, 455 contains_exactly_predicates = _collection_subject_contains_exactly_predicates, 456 contains_none_of = _collection_subject_contains_none_of, 457 contains_predicate = _collection_subject_contains_predicate, 458 has_size = _collection_subject_has_size, 459 new = _collection_subject_new, 460 not_contains_predicate = _collection_subject_not_contains_predicate, 461 offset = _collection_subject_offset, 462 transform = _collection_subject_transform, 463 # keep sorted end 464) 465