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