util.py 11 KB

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