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