| # 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. |
| |
| """Functions to aid formatting Truth failure messages.""" |
| |
| load( |
| ":truth_common.bzl", |
| "enumerate_list_as_lines", |
| "guess_format_value", |
| "maybe_sorted", |
| ) |
| |
| def format_actual_collection(actual, name = "values", sort = True): |
| """Creates an error message for the observed values of a collection. |
| |
| Args: |
| actual: ([`collection`]) the values to show |
| name: ([`str`]) the conceptual name of the collection. |
| sort: ([`bool`]) If true, the collection will be sorted for display. |
| Returns: |
| ([`str`]) the formatted error message. |
| """ |
| actual = maybe_sorted(actual, sort) |
| return "actual {name}:\n{actual}".format( |
| name = name, |
| actual = enumerate_list_as_lines(actual, prefix = " "), |
| ) |
| |
| def format_failure_missing_all_values( |
| element_plural_name, |
| container_name, |
| *, |
| missing, |
| actual, |
| sort = True): |
| """Create error messages when a container is missing all the expected values. |
| |
| Args: |
| element_plural_name: ([`str`]) the plural word for the values in the container. |
| container_name: ([`str`]) the conceptual name of the container. |
| missing: the collection of values that are missing. |
| actual: the collection of values observed. |
| sort: ([`bool`]) if True, then missing and actual are sorted. If False, they |
| are not sorted. |
| |
| Returns: |
| [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's |
| `add_failure()` method. |
| """ |
| missing = maybe_sorted(missing, sort) |
| problem_msg = "{count} expected {name} missing from {container}:\n{missing}".format( |
| count = len(missing), |
| name = element_plural_name, |
| container = container_name, |
| missing = enumerate_list_as_lines(missing, prefix = " "), |
| ) |
| actual_msg = format_actual_collection(actual, name = container_name, sort = sort) |
| return problem_msg, actual_msg |
| |
| def format_failure_unexpected_values(*, none_of, unexpected, actual, sort = True): |
| """Create error messages when a container has unexpected values. |
| |
| Args: |
| none_of: ([`str`]) description of the values that were not expected to be |
| present. |
| unexpected: ([`collection`]) the values that were unexpectedly found. |
| actual: ([`collection`]) the observed values. |
| sort: ([`bool`]) True if the collections should be sorted for output. |
| |
| Returns: |
| [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's |
| `add_failure()` method. |
| """ |
| unexpected = maybe_sorted(unexpected, sort) |
| problem_msg = "expected not to contain any of: {none_of}\nbut {count} found:\n{unexpected}".format( |
| none_of = none_of, |
| count = len(unexpected), |
| unexpected = enumerate_list_as_lines(unexpected, prefix = " "), |
| ) |
| actual_msg = format_actual_collection(actual, sort = sort) |
| return problem_msg, actual_msg |
| |
| def format_failure_unexpected_value(container_name, unexpected, actual, sort = True): |
| """Create error messages when a container contains a specific unexpected value. |
| |
| Args: |
| container_name: ([`str`]) conceptual name of the container. |
| unexpected: the value that shouldn't have been in `actual`. |
| actual: ([`collection`]) the observed values. |
| sort: ([`bool`]) True if the collections should be sorted for output. |
| |
| Returns: |
| [`tuple`] of ([`str`] problem, [`str`] actual), suitable for passing to ExpectMeta's |
| `add_failure()` method. |
| """ |
| problem_msg = "expected not to contain: {}".format(unexpected) |
| actual_msg = format_actual_collection(actual, name = container_name, sort = sort) |
| return problem_msg, actual_msg |
| |
| def format_problem_dict_expected( |
| *, |
| expected, |
| missing_keys, |
| unexpected_keys, |
| incorrect_entries, |
| container_name = "dict", |
| key_plural_name = "keys"): |
| """Formats an expected dict, describing what went wrong. |
| |
| Args: |
| expected: ([`dict`]) the full expected value. |
| missing_keys: ([`list`]) the keys that were not found. |
| unexpected_keys: ([`list`]) the keys that should not have existed |
| incorrect_entries: ([`list`] of [`DictEntryMismatch`]) (see [`_compare_dict`]). |
| container_name: ([`str`]) conceptual name of the `expected` dict. |
| key_plural_name: ([`str`]) the plural word for the keys of the `expected` dict. |
| Returns: |
| [`str`] that describes the problem. |
| """ |
| problem_lines = ["expected {}: {{\n{}\n}}".format( |
| container_name, |
| format_dict_as_lines(expected), |
| )] |
| if missing_keys: |
| problem_lines.append("{count} missing {key_plural_name}:\n{keys}".format( |
| count = len(missing_keys), |
| key_plural_name = key_plural_name, |
| keys = enumerate_list_as_lines(sorted(missing_keys), prefix = " "), |
| )) |
| if unexpected_keys: |
| problem_lines.append("{count} unexpected {key_plural_name}:\n{keys}".format( |
| count = len(unexpected_keys), |
| key_plural_name = key_plural_name, |
| keys = enumerate_list_as_lines(sorted(unexpected_keys), prefix = " "), |
| )) |
| if incorrect_entries: |
| problem_lines.append("{} incorrect entries:".format(len(incorrect_entries))) |
| for key, mismatch in incorrect_entries.items(): |
| problem_lines.append("key {}:".format(key)) |
| problem_lines.append(" expected: {}".format(mismatch.expected)) |
| problem_lines.append(" but was : {}".format(mismatch.actual)) |
| return "\n".join(problem_lines) |
| |
| def format_problem_expected_exactly(expected, sort = True): |
| """Creates an error message describing the expected values. |
| |
| This is for use when the observed value must have all the values and |
| no more. |
| |
| Args: |
| expected: ([`collection`]) the expected values. |
| sort: ([`bool`]) True if to sort the values for display. |
| Returns: |
| ([`str`]) the formatted problem message |
| """ |
| expected = maybe_sorted(expected, sort) |
| return "expected exactly:\n{}".format( |
| enumerate_list_as_lines(expected, prefix = " "), |
| ) |
| |
| def format_problem_missing_any_values(any_of, sort = True): |
| """Create an error message for when any of a collection of values are missing. |
| |
| Args: |
| any_of: ([`collection`]) the set of values, any of which were missing. |
| sort: ([`bool`]) True if the collection should be sorted for display. |
| Returns: |
| ([`str`]) the problem description string. |
| """ |
| any_of = maybe_sorted(any_of, sort) |
| return "expected to contain any of:\n{}".format( |
| enumerate_list_as_lines(any_of, prefix = " "), |
| ) |
| |
| def format_problem_missing_required_values(missing, sort = True): |
| """Create an error message for when the missing values must all be present. |
| |
| Args: |
| missing: ([`collection`]) the values that must all be present. |
| sort: ([`bool`]) True if to sort the values for display |
| Returns: |
| ([`str`]) the problem description string. |
| """ |
| missing = maybe_sorted(missing, sort) |
| return "{count} missing:\n{missing}".format( |
| count = len(missing), |
| missing = enumerate_list_as_lines(missing, prefix = " "), |
| ) |
| |
| def format_problem_predicates_did_not_match( |
| missing, |
| *, |
| element_plural_name = "elements", |
| container_name = "values"): |
| """Create an error message for when a list of predicates didn't match. |
| |
| Args: |
| missing: ([`list`] of [`Matcher`]) (see `_match_custom`). |
| element_plural_name: ([`str`]) the plural word for the values in the container. |
| container_name: ([`str`]) the conceptual name of the container. |
| Returns: |
| ([`str`]) the problem description string. |
| """ |
| |
| return "{count} expected {name} missing from {container}:\n{missing}".format( |
| count = len(missing), |
| name = element_plural_name, |
| container = container_name, |
| missing = enumerate_list_as_lines( |
| [m.desc for m in missing], |
| prefix = " ", |
| ), |
| ) |
| |
| def format_problem_matched_out_of_order(matches): |
| """Create an error message for when a expected values matched in the wrong order. |
| |
| Args: |
| matches: ([`list`] of [`MatchResult`]) see `_check_contains_at_least_predicates()`. |
| Returns: |
| ([`str`]) the problem description string. |
| """ |
| format_matched_value = guess_format_value([m.matched_value for m in matches]) |
| |
| def format_value(value): |
| # The matcher might be a Matcher object or a plain value. |
| # If the matcher description equals the matched value, then we omit |
| # the extra matcher text because (1) it'd be redundant, and (2) such |
| # matchers are usually wrappers around an underlying value, e.g. |
| # how contains_exactly uses matcher predicates. |
| if hasattr(value.matcher, "desc") and value.matcher.desc != value.matched_value: |
| match_desc = value.matcher.desc |
| match_info = " (matched: {})".format( |
| format_matched_value(value.matched_value), |
| ) |
| verb = "matched" |
| else: |
| match_desc = format_matched_value(value.matched_value) |
| match_info = "" |
| verb = "found" |
| |
| return "{match_desc} {verb} at offset {at}{match_info}".format( |
| at = value.found_at, |
| verb = verb, |
| match_desc = match_desc, |
| match_info = match_info, |
| ) |
| |
| return "expected values all found, but with incorrect order:\n{}".format( |
| enumerate_list_as_lines(matches, format_value = format_value, prefix = " "), |
| ) |
| |
| def format_problem_unexpected_values(unexpected, sort = True): |
| """Create an error message for when there are unexpected values. |
| |
| Args: |
| unexpected: ([`list`]) the unexpected values. |
| sort: ([`bool`]) true if the values should be sorted for output. |
| |
| Returns: |
| ([`str`]) the problem description string. |
| """ |
| unexpected = maybe_sorted(unexpected, sort) |
| return "{count} unexpected:\n{unexpected}".format( |
| count = len(unexpected), |
| unexpected = enumerate_list_as_lines(unexpected, prefix = " "), |
| ) |
| |
| def format_dict_as_lines(mapping, prefix = "", format_value = None, sort = True): |
| """Format a dictionary as lines of key->value for easier reading. |
| |
| Args: |
| mapping: [`dict`] to show |
| prefix: ([`str`]) prefix to prepend to every line. |
| format_value: (optional callable) takes a value from the dictionary |
| to show and returns the string that shown be shown. If not |
| specified, one will be automatically determined from the |
| dictionary's values. |
| sort: ([`bool`]) `True` if the output should be sorted by dict key (if |
| the keys are sortable). |
| |
| Returns: |
| ([`str`]) the dictionary formatted into lines |
| """ |
| lines = [] |
| if not mapping: |
| return " <empty dict>" |
| format_value = guess_format_value(mapping.values()) |
| keys = maybe_sorted(mapping.keys(), sort) |
| |
| max_key_width = max([len(str(key)) for key in keys]) |
| |
| for key in keys: |
| lines.append("{prefix} {key}{pad}: {value}".format( |
| prefix = prefix, |
| key = key, |
| pad = " " * (max_key_width - len(str(key))), |
| value = format_value(mapping[key]), |
| )) |
| return "\n".join(lines) |