xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/failure_messages.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"""Functions to aid formatting Truth failure messages."""
16
17load(
18    ":truth_common.bzl",
19    "enumerate_list_as_lines",
20    "guess_format_value",
21    "maybe_sorted",
22)
23
24def format_actual_collection(actual, name = "values", sort = True):
25    """Creates an error message for the observed values of a collection.
26
27    Args:
28        actual: ([`collection`]) the values to show
29        name: ([`str`]) the conceptual name of the collection.
30        sort: ([`bool`]) If true, the collection will be sorted for display.
31    Returns:
32        ([`str`]) the formatted error message.
33    """
34    actual = maybe_sorted(actual, sort)
35    return "actual {name}:\n{actual}".format(
36        name = name,
37        actual = enumerate_list_as_lines(actual, prefix = "  "),
38    )
39
40def format_failure_missing_all_values(
41        element_plural_name,
42        container_name,
43        *,
44        missing,
45        actual,
46        sort = True):
47    """Create error messages when a container is missing all the expected values.
48
49    Args:
50        element_plural_name: ([`str`]) the plural word for the values in the container.
51        container_name: ([`str`]) the conceptual name of the container.
52        missing: the collection of values that are missing.
53        actual: the collection of values observed.
54        sort: ([`bool`]) if True, then missing and actual are sorted. If False, they
55            are not sorted.
56
57    Returns:
58        [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's
59        `add_failure()` method.
60    """
61    missing = maybe_sorted(missing, sort)
62    problem_msg = "{count} expected {name} missing from {container}:\n{missing}".format(
63        count = len(missing),
64        name = element_plural_name,
65        container = container_name,
66        missing = enumerate_list_as_lines(missing, prefix = "  "),
67    )
68    actual_msg = format_actual_collection(actual, name = container_name, sort = sort)
69    return problem_msg, actual_msg
70
71def format_failure_unexpected_values(*, none_of, unexpected, actual, sort = True):
72    """Create error messages when a container has unexpected values.
73
74    Args:
75        none_of: ([`str`]) description of the values that were not expected to be
76            present.
77        unexpected: ([`collection`]) the values that were unexpectedly found.
78        actual: ([`collection`]) the observed values.
79        sort: ([`bool`]) True if the collections should be sorted for output.
80
81    Returns:
82        [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's
83        `add_failure()` method.
84    """
85    unexpected = maybe_sorted(unexpected, sort)
86    problem_msg = "expected not to contain any of: {none_of}\nbut {count} found:\n{unexpected}".format(
87        none_of = none_of,
88        count = len(unexpected),
89        unexpected = enumerate_list_as_lines(unexpected, prefix = "  "),
90    )
91    actual_msg = format_actual_collection(actual, sort = sort)
92    return problem_msg, actual_msg
93
94def format_failure_unexpected_value(container_name, unexpected, actual, sort = True):
95    """Create error messages when a container contains a specific unexpected value.
96
97    Args:
98        container_name: ([`str`]) conceptual name of the container.
99        unexpected: the value that shouldn't have been in `actual`.
100        actual: ([`collection`]) the observed values.
101        sort: ([`bool`]) True if the collections should be sorted for output.
102
103    Returns:
104        [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's
105        `add_failure()` method.
106    """
107    problem_msg = "expected not to contain: {}".format(unexpected)
108    actual_msg = format_actual_collection(actual, name = container_name, sort = sort)
109    return problem_msg, actual_msg
110
111def format_problem_dict_expected(
112        *,
113        expected,
114        missing_keys,
115        unexpected_keys,
116        incorrect_entries,
117        container_name = "dict",
118        key_plural_name = "keys"):
119    """Formats an expected dict, describing what went wrong.
120
121    Args:
122        expected: ([`dict`]) the full expected value.
123        missing_keys: ([`list`]) the keys that were not found.
124        unexpected_keys: ([`list`]) the keys that should not have existed
125        incorrect_entries: ([`list`] of [`DictEntryMismatch`]) (see [`_compare_dict`]).
126        container_name: ([`str`]) conceptual name of the `expected` dict.
127        key_plural_name: ([`str`]) the plural word for the keys of the `expected` dict.
128    Returns:
129        [`str`] that describes the problem.
130    """
131    problem_lines = ["expected {}: {{\n{}\n}}".format(
132        container_name,
133        format_dict_as_lines(expected),
134    )]
135    if missing_keys:
136        problem_lines.append("{count} missing {key_plural_name}:\n{keys}".format(
137            count = len(missing_keys),
138            key_plural_name = key_plural_name,
139            keys = enumerate_list_as_lines(sorted(missing_keys), prefix = "  "),
140        ))
141    if unexpected_keys:
142        problem_lines.append("{count} unexpected {key_plural_name}:\n{keys}".format(
143            count = len(unexpected_keys),
144            key_plural_name = key_plural_name,
145            keys = enumerate_list_as_lines(sorted(unexpected_keys), prefix = "  "),
146        ))
147    if incorrect_entries:
148        problem_lines.append("{} incorrect entries:".format(len(incorrect_entries)))
149        for key, mismatch in incorrect_entries.items():
150            problem_lines.append("key {}:".format(key))
151            problem_lines.append("  expected: {}".format(mismatch.expected))
152            problem_lines.append("  but was : {}".format(mismatch.actual))
153    return "\n".join(problem_lines)
154
155def format_problem_expected_exactly(expected, sort = True):
156    """Creates an error message describing the expected values.
157
158    This is for use when the observed value must have all the values and
159    no more.
160
161    Args:
162        expected: ([`collection`]) the expected values.
163        sort: ([`bool`]) True if to sort the values for display.
164    Returns:
165        ([`str`]) the formatted problem message
166    """
167    expected = maybe_sorted(expected, sort)
168    return "expected exactly:\n{}".format(
169        enumerate_list_as_lines(expected, prefix = "  "),
170    )
171
172def format_problem_missing_any_values(any_of, sort = True):
173    """Create an error message for when any of a collection of values are missing.
174
175    Args:
176        any_of: ([`collection`]) the set of values, any of which were missing.
177        sort: ([`bool`]) True if the collection should be sorted for display.
178    Returns:
179        ([`str`]) the problem description string.
180    """
181    any_of = maybe_sorted(any_of, sort)
182    return "expected to contain any of:\n{}".format(
183        enumerate_list_as_lines(any_of, prefix = "  "),
184    )
185
186def format_problem_missing_required_values(missing, sort = True):
187    """Create an error message for when the missing values must all be present.
188
189    Args:
190        missing: ([`collection`]) the values that must all be present.
191        sort: ([`bool`]) True if to sort the values for display
192    Returns:
193        ([`str`]) the problem description string.
194    """
195    missing = maybe_sorted(missing, sort)
196    return "{count} missing:\n{missing}".format(
197        count = len(missing),
198        missing = enumerate_list_as_lines(missing, prefix = "  "),
199    )
200
201def format_problem_predicates_did_not_match(
202        missing,
203        *,
204        element_plural_name = "elements",
205        container_name = "values"):
206    """Create an error message for when a list of predicates didn't match.
207
208    Args:
209        missing: ([`list`] of [`Matcher`]) (see `_match_custom`).
210        element_plural_name: ([`str`]) the plural word for the values in the container.
211        container_name: ([`str`]) the conceptual name of the container.
212    Returns:
213        ([`str`]) the problem description string.
214    """
215
216    return "{count} expected {name} missing from {container}:\n{missing}".format(
217        count = len(missing),
218        name = element_plural_name,
219        container = container_name,
220        missing = enumerate_list_as_lines(
221            [m.desc for m in missing],
222            prefix = "  ",
223        ),
224    )
225
226def format_problem_matched_out_of_order(matches):
227    """Create an error message for when a expected values matched in the wrong order.
228
229    Args:
230        matches: ([`list`] of [`MatchResult`]) see `_check_contains_at_least_predicates()`.
231    Returns:
232        ([`str`]) the problem description string.
233    """
234    format_matched_value = guess_format_value([m.matched_value for m in matches])
235
236    def format_value(value):
237        # The matcher might be a Matcher object or a plain value.
238        # If the matcher description equals the matched value, then we omit
239        # the extra matcher text because (1) it'd be redundant, and (2) such
240        # matchers are usually wrappers around an underlying value, e.g.
241        # how contains_exactly uses matcher predicates.
242        if hasattr(value.matcher, "desc") and value.matcher.desc != value.matched_value:
243            match_desc = value.matcher.desc
244            match_info = " (matched: {})".format(
245                format_matched_value(value.matched_value),
246            )
247            verb = "matched"
248        else:
249            match_desc = format_matched_value(value.matched_value)
250            match_info = ""
251            verb = "found"
252
253        return "{match_desc} {verb} at offset {at}{match_info}".format(
254            at = value.found_at,
255            verb = verb,
256            match_desc = match_desc,
257            match_info = match_info,
258        )
259
260    return "expected values all found, but with incorrect order:\n{}".format(
261        enumerate_list_as_lines(matches, format_value = format_value, prefix = "  "),
262    )
263
264def format_problem_unexpected_values(unexpected, sort = True):
265    """Create an error message for when there are unexpected values.
266
267    Args:
268        unexpected: ([`list`]) the unexpected values.
269        sort: ([`bool`]) true if the values should be sorted for output.
270
271    Returns:
272        ([`str`]) the problem description string.
273    """
274    unexpected = maybe_sorted(unexpected, sort)
275    return "{count} unexpected:\n{unexpected}".format(
276        count = len(unexpected),
277        unexpected = enumerate_list_as_lines(unexpected, prefix = "  "),
278    )
279
280def format_dict_as_lines(mapping, prefix = "", format_value = None, sort = True):
281    """Format a dictionary as lines of key->value for easier reading.
282
283    Args:
284        mapping: [`dict`] to show
285        prefix: ([`str`]) prefix to prepend to every line.
286        format_value: (optional callable) takes a value from the dictionary
287            to show and returns the string that shown be shown. If not
288            specified, one will be automatically determined from the
289            dictionary's values.
290        sort: ([`bool`]) `True` if the output should be sorted by dict key (if
291            the keys are sortable).
292
293    Returns:
294        ([`str`]) the dictionary formatted into lines
295    """
296    lines = []
297    if not mapping:
298        return "  <empty dict>"
299    format_value = guess_format_value(mapping.values())
300    keys = maybe_sorted(mapping.keys(), sort)
301
302    max_key_width = max([len(str(key)) for key in keys])
303
304    for key in keys:
305        lines.append("{prefix}  {key}{pad}: {value}".format(
306            prefix = prefix,
307            key = key,
308            pad = " " * (max_key_width - len(str(key))),
309            value = format_value(mapping[key]),
310        ))
311    return "\n".join(lines)
312