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