xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/collection_subject.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
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