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