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