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