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