| # Copyright 2023 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """# CollectionSubject""" |
| |
| load( |
| ":check_util.bzl", |
| "check_contains_at_least_predicates", |
| "check_contains_exactly", |
| "check_contains_exactly_predicates", |
| "check_contains_none_of", |
| "check_contains_predicate", |
| "check_not_contains_predicate", |
| ) |
| load( |
| ":failure_messages.bzl", |
| "format_actual_collection", |
| "format_problem_expected_exactly", |
| "format_problem_matched_out_of_order", |
| "format_problem_missing_required_values", |
| "format_problem_predicates_did_not_match", |
| "format_problem_unexpected_values", |
| ) |
| load(":int_subject.bzl", "IntSubject") |
| load(":matching.bzl", "matching") |
| load(":truth_common.bzl", "to_list") |
| load(":util.bzl", "get_function_name") |
| |
| def _identity(v): |
| return v |
| |
| def _always_true(v): |
| _ = v # @unused |
| return True |
| |
| def _collection_subject_new( |
| values, |
| meta, |
| container_name = "values", |
| sortable = True, |
| element_plural_name = "elements"): |
| """Creates a "CollectionSubject" struct. |
| |
| Method: CollectionSubject.new |
| |
| Public Attributes: |
| * `actual`: The wrapped collection. |
| |
| Args: |
| values: ([`collection`]) the values to assert against. |
| meta: ([`ExpectMeta`]) the metadata about the call chain. |
| container_name: ([`str`]) conceptual name of the container. |
| sortable: ([`bool`]) True if output should be sorted for display, False if not. |
| element_plural_name: ([`str`]) the plural word for the values in the container. |
| |
| Returns: |
| [`CollectionSubject`]. |
| """ |
| |
| # buildifier: disable=uninitialized |
| public = struct( |
| # keep sorted start |
| actual = values, |
| contains = lambda *a, **k: _collection_subject_contains(self, *a, **k), |
| contains_at_least = lambda *a, **k: _collection_subject_contains_at_least(self, *a, **k), |
| contains_at_least_predicates = lambda *a, **k: _collection_subject_contains_at_least_predicates(self, *a, **k), |
| contains_exactly = lambda *a, **k: _collection_subject_contains_exactly(self, *a, **k), |
| contains_exactly_predicates = lambda *a, **k: _collection_subject_contains_exactly_predicates(self, *a, **k), |
| contains_none_of = lambda *a, **k: _collection_subject_contains_none_of(self, *a, **k), |
| contains_predicate = lambda *a, **k: _collection_subject_contains_predicate(self, *a, **k), |
| has_size = lambda *a, **k: _collection_subject_has_size(self, *a, **k), |
| not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k), |
| not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k), |
| offset = lambda *a, **k: _collection_subject_offset(self, *a, **k), |
| transform = lambda *a, **k: _collection_subject_transform(self, *a, **k), |
| # keep sorted end |
| ) |
| self = struct( |
| actual = values, |
| meta = meta, |
| element_plural_name = element_plural_name, |
| container_name = container_name, |
| sortable = sortable, |
| contains_predicate = public.contains_predicate, |
| contains_at_least_predicates = public.contains_at_least_predicates, |
| ) |
| return public |
| |
| def _collection_subject_has_size(self, expected): |
| """Asserts that `expected` is the size of the collection. |
| |
| Method: CollectionSubject.has_size |
| |
| Args: |
| self: implicitly added. |
| expected: ([`int`]) the expected size of the collection. |
| """ |
| return IntSubject.new( |
| len(self.actual), |
| meta = self.meta.derive("size()"), |
| ).equals(expected) |
| |
| def _collection_subject_contains(self, expected): |
| """Asserts that `expected` is within the collection. |
| |
| Method: CollectionSubject.contains |
| |
| Args: |
| self: implicitly added. |
| expected: ([`str`]) the value that must be present. |
| """ |
| matcher = matching.equals_wrapper(expected) |
| return self.contains_predicate(matcher) |
| |
| def _collection_subject_contains_exactly(self, expected): |
| """Check that a collection contains exactly the given elements. |
| |
| Method: CollectionSubject.contains_exactly |
| |
| * Multiplicity is respected. |
| * The collection must contain all the values, no more or less. |
| * Checking that the order of matches is the same as the passed-in matchers |
| order can be done by call `in_order()`. |
| |
| The collection must contain all the values and no more. Multiplicity of |
| values is respected. Checking that the order of matches is the same as the |
| passed-in matchers order can done by calling `in_order()`. |
| |
| Args: |
| self: implicitly added. |
| expected: ([`list`]) values that must exist. |
| |
| Returns: |
| [`Ordered`] (see `_ordered_incorrectly_new`). |
| """ |
| expected = to_list(expected) |
| return check_contains_exactly( |
| actual_container = self.actual, |
| expect_contains = expected, |
| meta = self.meta, |
| format_actual = lambda: format_actual_collection( |
| self.actual, |
| name = self.container_name, |
| sort = False, # Don't sort; this might be rendered by the in_order() error. |
| ), |
| format_expected = lambda: format_problem_expected_exactly( |
| expected, |
| sort = False, # Don't sort; this might be rendered by the in_order() error. |
| ), |
| format_missing = lambda missing: format_problem_missing_required_values( |
| missing, |
| sort = self.sortable, |
| ), |
| format_unexpected = lambda unexpected: format_problem_unexpected_values( |
| unexpected, |
| sort = self.sortable, |
| ), |
| format_out_of_order = format_problem_matched_out_of_order, |
| ) |
| |
| def _collection_subject_contains_exactly_predicates(self, expected): |
| """Check that the values correspond 1:1 to the predicates. |
| |
| Method: CollectionSubject.contains_exactly_predicates |
| |
| * There must be a 1:1 correspondence between the container values and the |
| predicates. |
| * Multiplicity is respected (i.e., if the same predicate occurs twice, then |
| two distinct elements must match). |
| * Matching occurs in first-seen order. That is, a predicate will "consume" |
| the first value in `actual_container` it matches. |
| * The collection must match all the predicates, no more or less. |
| * Checking that the order of matches is the same as the passed-in matchers |
| order can be done by call `in_order()`. |
| |
| Note that confusing results may occur if predicates with overlapping |
| match conditions are used. For example, given: |
| actual=["a", "ab", "abc"], |
| predicates=[<contains a>, <contains b>, <equals a>] |
| |
| Then the result will be they aren't equal: the first two predicates |
| consume "a" and "ab", leaving only "abc" for the <equals a> predicate |
| to match against, which fails. |
| |
| Args: |
| self: implicitly added. |
| expected: ([`list`] of [`Matcher`]) that must match. |
| |
| Returns: |
| [`Ordered`] (see `_ordered_incorrectly_new`). |
| """ |
| expected = to_list(expected) |
| return check_contains_exactly_predicates( |
| actual_container = self.actual, |
| expect_contains = expected, |
| meta = self.meta, |
| format_actual = lambda: format_actual_collection( |
| self.actual, |
| name = self.container_name, |
| sort = False, # Don't sort; this might be rendered by the in_order() error. |
| ), |
| format_expected = lambda: format_problem_expected_exactly( |
| [e.desc for e in expected], |
| sort = False, # Don't sort; this might be rendered by the in_order() error. |
| ), |
| format_missing = lambda missing: format_problem_missing_required_values( |
| [m.desc for m in missing], |
| sort = self.sortable, |
| ), |
| format_unexpected = lambda unexpected: format_problem_unexpected_values( |
| unexpected, |
| sort = self.sortable, |
| ), |
| format_out_of_order = format_problem_matched_out_of_order, |
| ) |
| |
| def _collection_subject_contains_none_of(self, values): |
| """Asserts the collection contains none of `values`. |
| |
| Method: CollectionSubject.contains_none_of |
| |
| Args: |
| self: implicitly added |
| values: ([`collection`]) values of which none of are allowed to exist. |
| """ |
| check_contains_none_of( |
| collection = self.actual, |
| none_of = values, |
| meta = self.meta, |
| sort = self.sortable, |
| ) |
| |
| def _collection_subject_contains_predicate(self, matcher): |
| """Asserts that `matcher` matches at least one value. |
| |
| Method: CollectionSubject.contains_predicate |
| |
| Args: |
| self: implicitly added. |
| matcher: ([`Matcher`]) (see `matchers` struct). |
| """ |
| check_contains_predicate( |
| self.actual, |
| matcher = matcher, |
| format_problem = "expected to contain: {}".format(matcher.desc), |
| format_actual = lambda: format_actual_collection( |
| self.actual, |
| name = self.container_name, |
| sort = self.sortable, |
| ), |
| meta = self.meta, |
| ) |
| |
| def _collection_subject_contains_at_least(self, expect_contains): |
| """Assert that the collection is a subset of the given predicates. |
| |
| Method: CollectionSubject.contains_at_least |
| |
| The collection must contain all the values. It can contain extra elements. |
| The multiplicity of values is respected. Checking that the relative order |
| of matches is the same as the passed-in expected values order can done by |
| calling `in_order()`. |
| |
| Args: |
| self: implicitly added. |
| expect_contains: ([`list`]) values that must be in the collection. |
| |
| Returns: |
| [`Ordered`] (see `_ordered_incorrectly_new`). |
| """ |
| matchers = [ |
| matching.equals_wrapper(expected) |
| for expected in to_list(expect_contains) |
| ] |
| return self.contains_at_least_predicates(matchers) |
| |
| def _collection_subject_contains_at_least_predicates(self, matchers): |
| """Assert that the collection is a subset of the given predicates. |
| |
| Method: CollectionSubject.contains_at_least_predicates |
| |
| The collection must match all the predicates. It can contain extra elements. |
| The multiplicity of matchers is respected. Checking that the relative order |
| of matches is the same as the passed-in matchers order can done by calling |
| `in_order()`. |
| |
| Args: |
| self: implicitly added. |
| matchers: ([`list`] of [`Matcher`]) (see `matchers` struct). |
| |
| Returns: |
| [`Ordered`] (see `_ordered_incorrectly_new`). |
| """ |
| ordered = check_contains_at_least_predicates( |
| self.actual, |
| matchers, |
| format_missing = lambda missing: format_problem_predicates_did_not_match( |
| missing, |
| element_plural_name = self.element_plural_name, |
| container_name = self.container_name, |
| ), |
| format_out_of_order = format_problem_matched_out_of_order, |
| format_actual = lambda: format_actual_collection( |
| self.actual, |
| name = self.container_name, |
| sort = self.sortable, |
| ), |
| meta = self.meta, |
| ) |
| return ordered |
| |
| def _collection_subject_not_contains(self, value): |
| check_not_contains_predicate( |
| self.actual, |
| matcher = matching.equals_wrapper(value), |
| meta = self.meta, |
| sort = self.sortable, |
| ) |
| |
| def _collection_subject_not_contains_predicate(self, matcher): |
| """Asserts that `matcher` matches no values in the collection. |
| |
| Method: CollectionSubject.not_contains_predicate |
| |
| Args: |
| self: implicitly added. |
| matcher: [`Matcher`] object (see `matchers` struct). |
| """ |
| check_not_contains_predicate( |
| self.actual, |
| matcher = matcher, |
| meta = self.meta, |
| sort = self.sortable, |
| ) |
| |
| def _collection_subject_offset(self, offset, factory): |
| """Fetches an element from the collection as a subject. |
| |
| Args: |
| self: implicitly added. |
| offset: ([`int`]) the offset to fetch |
| factory: ([`callable`]). The factory function to use to create |
| the subject for the offset's value. It must have the following |
| signature: `def factory(value, *, meta)`. |
| |
| Returns: |
| Object created by `factory`. |
| """ |
| value = self.actual[offset] |
| return factory( |
| value, |
| meta = self.meta.derive("offset({})".format(offset)), |
| ) |
| |
| def _collection_subject_transform( |
| self, |
| desc = None, |
| *, |
| map_each = None, |
| loop = None, |
| filter = None): |
| """Transforms a collections's value and returns another CollectionSubject. |
| |
| This is equivalent to applying a list comprehension over the collection values, |
| but takes care of propagating context information and wrapping the value |
| in a `CollectionSubject`. |
| |
| `transform(map_each=M, loop=L, filter=F)` is equivalent to |
| `[M(v) for v in L(collection) if F(v)]`. |
| |
| Args: |
| self: implicitly added. |
| desc: (optional [`str`]) a human-friendly description of the transform |
| for use in error messages. Required when a description can't be |
| inferred from the other args. The description can be inferred if the |
| filter arg is a named function (non-lambda) or Matcher object. |
| map_each: (optional [`callable`]) function to transform an element in |
| the collection. It takes one positional arg, the loop's |
| current iteration value, and its return value will be the element's |
| new value. If not specified, the values from the loop iteration are |
| returned unchanged. |
| loop: (optional [`callable`]) function to produce values from the |
| original collection and whose values are iterated over. It takes one |
| positional arg, which is the original collection. If not specified, |
| the original collection values are iterated over. |
| filter: (optional [`callable`]) function that decides what values are |
| passed onto `map_each` for inclusion in the final result. It takes |
| one positional arg, the value to match (which is the current |
| iteration value before `map_each` is applied), and returns a bool |
| (True if the value should be included in the result, False if it |
| should be skipped). |
| |
| Returns: |
| [`CollectionSubject`] of the transformed values. |
| """ |
| if not desc: |
| if map_each or loop: |
| fail("description required when map_each or loop used") |
| |
| if matching.is_matcher(filter): |
| desc = "filter=" + filter.desc |
| else: |
| func_name = get_function_name(filter) |
| if func_name == "lambda": |
| fail("description required: description cannot be " + |
| "inferred from lambdas. Explicitly specify the " + |
| "description, use a named function for the filter, " + |
| "or use a Matcher for the filter.") |
| else: |
| desc = "filter={}(...)".format(func_name) |
| |
| map_each = map_each or _identity |
| loop = loop or _identity |
| |
| if filter: |
| if matching.is_matcher(filter): |
| filter_func = filter.match |
| else: |
| filter_func = filter |
| else: |
| filter_func = _always_true |
| |
| new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)] |
| |
| return _collection_subject_new( |
| new_values, |
| meta = self.meta.derive( |
| "transform()", |
| details = ["transform: {}".format(desc)], |
| ), |
| container_name = self.container_name, |
| sortable = self.sortable, |
| element_plural_name = self.element_plural_name, |
| ) |
| |
| # We use this name so it shows up nice in docs. |
| # buildifier: disable=name-conventions |
| CollectionSubject = struct( |
| # keep sorted start |
| contains = _collection_subject_contains, |
| contains_at_least = _collection_subject_contains_at_least, |
| contains_at_least_predicates = _collection_subject_contains_at_least_predicates, |
| contains_exactly = _collection_subject_contains_exactly, |
| contains_exactly_predicates = _collection_subject_contains_exactly_predicates, |
| contains_none_of = _collection_subject_contains_none_of, |
| contains_predicate = _collection_subject_contains_predicate, |
| has_size = _collection_subject_has_size, |
| new = _collection_subject_new, |
| not_contains_predicate = _collection_subject_not_contains_predicate, |
| offset = _collection_subject_offset, |
| transform = _collection_subject_transform, |
| # keep sorted end |
| ) |