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