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