1"""Utilities for assertion debugging""" 2from __future__ import absolute_import, division, print_function 3import pprint 4 5import _pytest._code 6import py 7import six 8from ..compat import Sequence 9 10u = six.text_type 11 12# The _reprcompare attribute on the util module is used by the new assertion 13# interpretation code and assertion rewriter to detect this plugin was 14# loaded and in turn call the hooks defined here as part of the 15# DebugInterpreter. 16_reprcompare = None 17 18 19# the re-encoding is needed for python2 repr 20# with non-ascii characters (see issue 877 and 1379) 21def ecu(s): 22 try: 23 return u(s, "utf-8", "replace") 24 except TypeError: 25 return s 26 27 28def format_explanation(explanation): 29 """This formats an explanation 30 31 Normally all embedded newlines are escaped, however there are 32 three exceptions: \n{, \n} and \n~. The first two are intended 33 cover nested explanations, see function and attribute explanations 34 for examples (.visit_Call(), visit_Attribute()). The last one is 35 for when one explanation needs to span multiple lines, e.g. when 36 displaying diffs. 37 """ 38 explanation = ecu(explanation) 39 lines = _split_explanation(explanation) 40 result = _format_lines(lines) 41 return u("\n").join(result) 42 43 44def _split_explanation(explanation): 45 """Return a list of individual lines in the explanation 46 47 This will return a list of lines split on '\n{', '\n}' and '\n~'. 48 Any other newlines will be escaped and appear in the line as the 49 literal '\n' characters. 50 """ 51 raw_lines = (explanation or u("")).split("\n") 52 lines = [raw_lines[0]] 53 for values in raw_lines[1:]: 54 if values and values[0] in ["{", "}", "~", ">"]: 55 lines.append(values) 56 else: 57 lines[-1] += "\\n" + values 58 return lines 59 60 61def _format_lines(lines): 62 """Format the individual lines 63 64 This will replace the '{', '}' and '~' characters of our mini 65 formatting language with the proper 'where ...', 'and ...' and ' + 66 ...' text, taking care of indentation along the way. 67 68 Return a list of formatted lines. 69 """ 70 result = lines[:1] 71 stack = [0] 72 stackcnt = [0] 73 for line in lines[1:]: 74 if line.startswith("{"): 75 if stackcnt[-1]: 76 s = u("and ") 77 else: 78 s = u("where ") 79 stack.append(len(result)) 80 stackcnt[-1] += 1 81 stackcnt.append(0) 82 result.append(u(" +") + u(" ") * (len(stack) - 1) + s + line[1:]) 83 elif line.startswith("}"): 84 stack.pop() 85 stackcnt.pop() 86 result[stack[-1]] += line[1:] 87 else: 88 assert line[0] in ["~", ">"] 89 stack[-1] += 1 90 indent = len(stack) if line.startswith("~") else len(stack) - 1 91 result.append(u(" ") * indent + line[1:]) 92 assert len(stack) == 1 93 return result 94 95 96# Provide basestring in python3 97try: 98 basestring = basestring 99except NameError: 100 basestring = str 101 102 103def assertrepr_compare(config, op, left, right): 104 """Return specialised explanations for some operators/operands""" 105 width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op 106 left_repr = py.io.saferepr(left, maxsize=int(width // 2)) 107 right_repr = py.io.saferepr(right, maxsize=width - len(left_repr)) 108 109 summary = u("%s %s %s") % (ecu(left_repr), op, ecu(right_repr)) 110 111 def issequence(x): 112 return isinstance(x, Sequence) and not isinstance(x, basestring) 113 114 def istext(x): 115 return isinstance(x, basestring) 116 117 def isdict(x): 118 return isinstance(x, dict) 119 120 def isset(x): 121 return isinstance(x, (set, frozenset)) 122 123 def isiterable(obj): 124 try: 125 iter(obj) 126 return not istext(obj) 127 except TypeError: 128 return False 129 130 verbose = config.getoption("verbose") 131 explanation = None 132 try: 133 if op == "==": 134 if istext(left) and istext(right): 135 explanation = _diff_text(left, right, verbose) 136 else: 137 if issequence(left) and issequence(right): 138 explanation = _compare_eq_sequence(left, right, verbose) 139 elif isset(left) and isset(right): 140 explanation = _compare_eq_set(left, right, verbose) 141 elif isdict(left) and isdict(right): 142 explanation = _compare_eq_dict(left, right, verbose) 143 if isiterable(left) and isiterable(right): 144 expl = _compare_eq_iterable(left, right, verbose) 145 if explanation is not None: 146 explanation.extend(expl) 147 else: 148 explanation = expl 149 elif op == "not in": 150 if istext(left) and istext(right): 151 explanation = _notin_text(left, right, verbose) 152 except Exception: 153 explanation = [ 154 u( 155 "(pytest_assertion plugin: representation of details failed. " 156 "Probably an object has a faulty __repr__.)" 157 ), 158 u(_pytest._code.ExceptionInfo()), 159 ] 160 161 if not explanation: 162 return None 163 164 return [summary] + explanation 165 166 167def _diff_text(left, right, verbose=False): 168 """Return the explanation for the diff between text or bytes 169 170 Unless --verbose is used this will skip leading and trailing 171 characters which are identical to keep the diff minimal. 172 173 If the input are bytes they will be safely converted to text. 174 """ 175 from difflib import ndiff 176 177 explanation = [] 178 179 def escape_for_readable_diff(binary_text): 180 """ 181 Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode. 182 This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape 183 newlines and carriage returns (#429). 184 """ 185 r = six.text_type(repr(binary_text)[1:-1]) 186 r = r.replace(r"\n", "\n") 187 r = r.replace(r"\r", "\r") 188 return r 189 190 if isinstance(left, six.binary_type): 191 left = escape_for_readable_diff(left) 192 if isinstance(right, six.binary_type): 193 right = escape_for_readable_diff(right) 194 if not verbose: 195 i = 0 # just in case left or right has zero length 196 for i in range(min(len(left), len(right))): 197 if left[i] != right[i]: 198 break 199 if i > 42: 200 i -= 10 # Provide some context 201 explanation = [ 202 u("Skipping %s identical leading " "characters in diff, use -v to show") 203 % i 204 ] 205 left = left[i:] 206 right = right[i:] 207 if len(left) == len(right): 208 for i in range(len(left)): 209 if left[-i] != right[-i]: 210 break 211 if i > 42: 212 i -= 10 # Provide some context 213 explanation += [ 214 u( 215 "Skipping %s identical trailing " 216 "characters in diff, use -v to show" 217 ) 218 % i 219 ] 220 left = left[:-i] 221 right = right[:-i] 222 keepends = True 223 if left.isspace() or right.isspace(): 224 left = repr(str(left)) 225 right = repr(str(right)) 226 explanation += [u"Strings contain only whitespace, escaping them using repr()"] 227 explanation += [ 228 line.strip("\n") 229 for line in ndiff(left.splitlines(keepends), right.splitlines(keepends)) 230 ] 231 return explanation 232 233 234def _compare_eq_iterable(left, right, verbose=False): 235 if not verbose: 236 return [u("Use -v to get the full diff")] 237 # dynamic import to speedup pytest 238 import difflib 239 240 try: 241 left_formatting = pprint.pformat(left).splitlines() 242 right_formatting = pprint.pformat(right).splitlines() 243 explanation = [u("Full diff:")] 244 except Exception: 245 # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling 246 # sorted() on a list would raise. See issue #718. 247 # As a workaround, the full diff is generated by using the repr() string of each item of each container. 248 left_formatting = sorted(repr(x) for x in left) 249 right_formatting = sorted(repr(x) for x in right) 250 explanation = [u("Full diff (fallback to calling repr on each item):")] 251 explanation.extend( 252 line.strip() for line in difflib.ndiff(left_formatting, right_formatting) 253 ) 254 return explanation 255 256 257def _compare_eq_sequence(left, right, verbose=False): 258 explanation = [] 259 for i in range(min(len(left), len(right))): 260 if left[i] != right[i]: 261 explanation += [u("At index %s diff: %r != %r") % (i, left[i], right[i])] 262 break 263 if len(left) > len(right): 264 explanation += [ 265 u("Left contains more items, first extra item: %s") 266 % py.io.saferepr(left[len(right)]) 267 ] 268 elif len(left) < len(right): 269 explanation += [ 270 u("Right contains more items, first extra item: %s") 271 % py.io.saferepr(right[len(left)]) 272 ] 273 return explanation 274 275 276def _compare_eq_set(left, right, verbose=False): 277 explanation = [] 278 diff_left = left - right 279 diff_right = right - left 280 if diff_left: 281 explanation.append(u("Extra items in the left set:")) 282 for item in diff_left: 283 explanation.append(py.io.saferepr(item)) 284 if diff_right: 285 explanation.append(u("Extra items in the right set:")) 286 for item in diff_right: 287 explanation.append(py.io.saferepr(item)) 288 return explanation 289 290 291def _compare_eq_dict(left, right, verbose=False): 292 explanation = [] 293 common = set(left).intersection(set(right)) 294 same = {k: left[k] for k in common if left[k] == right[k]} 295 if same and verbose < 2: 296 explanation += [u("Omitting %s identical items, use -vv to show") % len(same)] 297 elif same: 298 explanation += [u("Common items:")] 299 explanation += pprint.pformat(same).splitlines() 300 diff = {k for k in common if left[k] != right[k]} 301 if diff: 302 explanation += [u("Differing items:")] 303 for k in diff: 304 explanation += [ 305 py.io.saferepr({k: left[k]}) + " != " + py.io.saferepr({k: right[k]}) 306 ] 307 extra_left = set(left) - set(right) 308 if extra_left: 309 explanation.append(u("Left contains more items:")) 310 explanation.extend( 311 pprint.pformat({k: left[k] for k in extra_left}).splitlines() 312 ) 313 extra_right = set(right) - set(left) 314 if extra_right: 315 explanation.append(u("Right contains more items:")) 316 explanation.extend( 317 pprint.pformat({k: right[k] for k in extra_right}).splitlines() 318 ) 319 return explanation 320 321 322def _notin_text(term, text, verbose=False): 323 index = text.find(term) 324 head = text[:index] 325 tail = text[index + len(term):] 326 correct_text = head + tail 327 diff = _diff_text(correct_text, text, verbose) 328 newdiff = [u("%s is contained here:") % py.io.saferepr(term, maxsize=42)] 329 for line in diff: 330 if line.startswith(u("Skipping")): 331 continue 332 if line.startswith(u("- ")): 333 continue 334 if line.startswith(u("+ ")): 335 newdiff.append(u(" ") + line[2:]) 336 else: 337 newdiff.append(line) 338 return newdiff 339