# -*- coding: utf-8 -*- import sys import textwrap import py, pytest import _pytest.assertion as plugin from _pytest.assertion import reinterpret from _pytest.assertion import util PY3 = sys.version_info >= (3, 0) @pytest.fixture def mock_config(): class Config(object): verbose = False def getoption(self, name): if name == 'verbose': return self.verbose raise KeyError('Not mocked out: %s' % name) return Config() def interpret(expr): return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) class TestBinReprIntegration: def test_pytest_assertrepr_compare_called(self, testdir): testdir.makeconftest(""" l = [] def pytest_assertrepr_compare(op, left, right): l.append((op, left, right)) def pytest_funcarg__l(request): return l """) testdir.makepyfile(""" def test_hello(): assert 0 == 1 def test_check(l): assert l == [("==", 0, 1)] """) result = testdir.runpytest("-v") result.stdout.fnmatch_lines([ "*test_hello*FAIL*", "*test_check*PASS*", ]) def callequal(left, right, verbose=False): config = mock_config() config.verbose = verbose return plugin.pytest_assertrepr_compare(config, '==', left, right) class TestAssert_reprcompare: def test_different_types(self): assert callequal([0, 1], 'foo') is None def test_summary(self): summary = callequal([0, 1], [0, 2])[0] assert len(summary) < 65 def test_text_diff(self): diff = callequal('spam', 'eggs')[1:] assert '- spam' in diff assert '+ eggs' in diff def test_text_skipping(self): lines = callequal('a'*50 + 'spam', 'a'*50 + 'eggs') assert 'Skipping' in lines[1] for line in lines: assert 'a'*50 not in line def test_text_skipping_verbose(self): lines = callequal('a'*50 + 'spam', 'a'*50 + 'eggs', verbose=True) assert '- ' + 'a'*50 + 'spam' in lines assert '+ ' + 'a'*50 + 'eggs' in lines def test_multiline_text_diff(self): left = 'foo\nspam\nbar' right = 'foo\neggs\nbar' diff = callequal(left, right) assert '- spam' in diff assert '+ eggs' in diff def test_list(self): expl = callequal([0, 1], [0, 2]) assert len(expl) > 1 @pytest.mark.parametrize( ['left', 'right', 'expected'], [ ([0, 1], [0, 2], """ Full diff: - [0, 1] ? ^ + [0, 2] ? ^ """), ({0: 1}, {0: 2}, """ Full diff: - {0: 1} ? ^ + {0: 2} ? ^ """), (set([0, 1]), set([0, 2]), """ Full diff: - set([0, 1]) ? ^ + set([0, 2]) ? ^ """ if not PY3 else """ Full diff: - {0, 1} ? ^ + {0, 2} ? ^ """) ] ) def test_iterable_full_diff(self, left, right, expected): """Test the full diff assertion failure explanation. When verbose is False, then just a -v notice to get the diff is rendered, when verbose is True, then ndiff of the pprint is returned. """ expl = callequal(left, right, verbose=False) assert expl[-1] == 'Use -v to get the full diff' expl = '\n'.join(callequal(left, right, verbose=True)) assert expl.endswith(textwrap.dedent(expected).strip()) def test_list_different_lenghts(self): expl = callequal([0, 1], [0, 1, 2]) assert len(expl) > 1 expl = callequal([0, 1, 2], [0, 1]) assert len(expl) > 1 def test_dict(self): expl = callequal({'a': 0}, {'a': 1}) assert len(expl) > 1 def test_dict_omitting(self): lines = callequal({'a': 0, 'b': 1}, {'a': 1, 'b': 1}) assert lines[1].startswith('Omitting 1 identical item') assert 'Common items' not in lines for line in lines[1:]: assert 'b' not in line def test_dict_omitting_verbose(self): lines = callequal({'a': 0, 'b': 1}, {'a': 1, 'b': 1}, verbose=True) assert lines[1].startswith('Common items:') assert 'Omitting' not in lines[1] assert lines[2] == "{'b': 1}" def test_set(self): expl = callequal(set([0, 1]), set([0, 2])) assert len(expl) > 1 def test_frozenzet(self): expl = callequal(frozenset([0, 1]), set([0, 2])) assert len(expl) > 1 def test_Sequence(self): col = py.builtin._tryimport( "collections.abc", "collections", "sys") if not hasattr(col, "MutableSequence"): pytest.skip("cannot import MutableSequence") MutableSequence = col.MutableSequence class TestSequence(MutableSequence): # works with a Sequence subclass def __init__(self, iterable): self.elements = list(iterable) def __getitem__(self, item): return self.elements[item] def __len__(self): return len(self.elements) def __setitem__(self, item, value): pass def __delitem__(self, item): pass def insert(self, item, index): pass expl = callequal(TestSequence([0, 1]), list([0, 2])) assert len(expl) > 1 def test_list_tuples(self): expl = callequal([], [(1,2)]) assert len(expl) > 1 expl = callequal([(1,2)], []) assert len(expl) > 1 def test_list_bad_repr(self): class A: def __repr__(self): raise ValueError(42) expl = callequal([], [A()]) assert 'ValueError' in "".join(expl) expl = callequal({}, {'1': A()}) assert 'faulty' in "".join(expl) def test_one_repr_empty(self): """ the faulty empty string repr did trigger a unbound local error in _diff_text """ class A(str): def __repr__(self): return '' expl = callequal(A(), '') assert not expl def test_repr_no_exc(self): expl = ' '.join(callequal('foo', 'bar')) assert 'raised in repr()' not in expl def test_unicode(self): left = py.builtin._totext('£€', 'utf-8') right = py.builtin._totext('£', 'utf-8') expl = callequal(left, right) assert expl[0] == py.builtin._totext("'£€' == '£'", 'utf-8') assert expl[1] == py.builtin._totext('- £€', 'utf-8') assert expl[2] == py.builtin._totext('+ £', 'utf-8') def test_nonascii_text(self): """ :issue: 877 non ascii python2 str caused a UnicodeDecodeError """ class A(str): def __repr__(self): return '\xff' expl = callequal(A(), '1') assert expl def test_mojibake(self): # issue 429 left = 'e' right = '\xc3\xa9' if not isinstance(left, py.builtin.bytes): left = py.builtin.bytes(left, 'utf-8') right = py.builtin.bytes(right, 'utf-8') expl = callequal(left, right) for line in expl: assert isinstance(line, py.builtin.text) msg = py.builtin._totext('\n').join(expl) assert msg class TestFormatExplanation: def test_special_chars_full(self, testdir): # Issue 453, for the bug this would raise IndexError testdir.makepyfile(""" def test_foo(): assert '\\n}' == '' """) result = testdir.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines([ "*AssertionError*", ]) def test_fmt_simple(self): expl = 'assert foo' assert util.format_explanation(expl) == 'assert foo' def test_fmt_where(self): expl = '\n'.join(['assert 1', '{1 = foo', '} == 2']) res = '\n'.join(['assert 1 == 2', ' + where 1 = foo']) assert util.format_explanation(expl) == res def test_fmt_and(self): expl = '\n'.join(['assert 1', '{1 = foo', '} == 2', '{2 = bar', '}']) res = '\n'.join(['assert 1 == 2', ' + where 1 = foo', ' + and 2 = bar']) assert util.format_explanation(expl) == res def test_fmt_where_nested(self): expl = '\n'.join(['assert 1', '{1 = foo', '{foo = bar', '}', '} == 2']) res = '\n'.join(['assert 1 == 2', ' + where 1 = foo', ' + where foo = bar']) assert util.format_explanation(expl) == res def test_fmt_newline(self): expl = '\n'.join(['assert "foo" == "bar"', '~- foo', '~+ bar']) res = '\n'.join(['assert "foo" == "bar"', ' - foo', ' + bar']) assert util.format_explanation(expl) == res def test_fmt_newline_escaped(self): expl = '\n'.join(['assert foo == bar', 'baz']) res = 'assert foo == bar\\nbaz' assert util.format_explanation(expl) == res def test_fmt_newline_before_where(self): expl = '\n'.join(['the assertion message here', '>assert 1', '{1 = foo', '} == 2', '{2 = bar', '}']) res = '\n'.join(['the assertion message here', 'assert 1 == 2', ' + where 1 = foo', ' + and 2 = bar']) assert util.format_explanation(expl) == res def test_fmt_multi_newline_before_where(self): expl = '\n'.join(['the assertion', '~message here', '>assert 1', '{1 = foo', '} == 2', '{2 = bar', '}']) res = '\n'.join(['the assertion', ' message here', 'assert 1 == 2', ' + where 1 = foo', ' + and 2 = bar']) assert util.format_explanation(expl) == res def test_python25_compile_issue257(testdir): testdir.makepyfile(""" def test_rewritten(): assert 1 == 2 # some comment """) result = testdir.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines(""" *E*assert 1 == 2* *1 failed* """) def test_rewritten(testdir): testdir.makepyfile(""" def test_rewritten(): assert "@py_builtins" in globals() """) assert testdir.runpytest().ret == 0 def test_reprcompare_notin(mock_config): detail = plugin.pytest_assertrepr_compare( mock_config, 'not in', 'foo', 'aaafoobbb')[1:] assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++'] def test_pytest_assertrepr_compare_integration(testdir): testdir.makepyfile(""" def test_hello(): x = set(range(100)) y = x.copy() y.remove(50) assert x == y """) result = testdir.runpytest() result.stdout.fnmatch_lines([ "*def test_hello():*", "*assert x == y*", "*E*Extra items*left*", "*E*50*", ]) def test_sequence_comparison_uses_repr(testdir): testdir.makepyfile(""" def test_hello(): x = set("hello x") y = set("hello y") assert x == y """) result = testdir.runpytest() result.stdout.fnmatch_lines([ "*def test_hello():*", "*assert x == y*", "*E*Extra items*left*", "*E*'x'*", "*E*Extra items*right*", "*E*'y'*", ]) def test_assert_compare_truncate_longmessage(monkeypatch, testdir): testdir.makepyfile(r""" def test_long(): a = list(range(200)) b = a[::2] a = '\n'.join(map(str, a)) b = '\n'.join(map(str, b)) assert a == b """) monkeypatch.delenv('CI', raising=False) result = testdir.runpytest() # without -vv, truncate the message showing a few diff lines only result.stdout.fnmatch_lines([ "*- 1", "*- 3", "*- 5", "*- 7", "*truncated (191 more lines)*use*-vv*", ]) result = testdir.runpytest('-vv') result.stdout.fnmatch_lines([ "*- 197", ]) monkeypatch.setenv('CI', '1') result = testdir.runpytest() result.stdout.fnmatch_lines([ "*- 197", ]) def test_assertrepr_loaded_per_dir(testdir): testdir.makepyfile(test_base=['def test_base(): assert 1 == 2']) a = testdir.mkdir('a') a_test = a.join('test_a.py') a_test.write('def test_a(): assert 1 == 2') a_conftest = a.join('conftest.py') a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]') b = testdir.mkdir('b') b_test = b.join('test_b.py') b_test.write('def test_b(): assert 1 == 2') b_conftest = b.join('conftest.py') b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]') result = testdir.runpytest() result.stdout.fnmatch_lines([ '*def test_base():*', '*E*assert 1 == 2*', '*def test_a():*', '*E*assert summary a*', '*def test_b():*', '*E*assert summary b*']) def test_assertion_options(testdir): testdir.makepyfile(""" def test_hello(): x = 3 assert x == 4 """) result = testdir.runpytest() assert "3 == 4" in result.stdout.str() off_options = (("--no-assert",), ("--nomagic",), ("--no-assert", "--nomagic"), ("--assert=plain",), ("--assert=plain", "--no-assert"), ("--assert=plain", "--nomagic"), ("--assert=plain", "--no-assert", "--nomagic")) for opt in off_options: result = testdir.runpytest_subprocess(*opt) assert "3 == 4" not in result.stdout.str() def test_old_assert_mode(testdir): testdir.makepyfile(""" def test_in_old_mode(): assert "@py_builtins" not in globals() """) result = testdir.runpytest_subprocess("--assert=reinterp") assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): assert "" == ''' '''""") result = testdir.runpytest("--fulltrace") result.stdout.fnmatch_lines([ "*1 failed*", ]) assert 'SyntaxError' not in result.stdout.str() def test_traceback_failure(testdir): p1 = testdir.makepyfile(""" def g(): return 2 def f(x): assert x == g() def test_onefails(): f(3) """) result = testdir.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines([ "*test_traceback_failure.py F", "====* FAILURES *====", "____*____", "", " def test_onefails():", "> f(3)", "", "*test_*.py:6: ", "_ _ _ *", #"", " def f(x):", "> assert x == g()", "E assert 3 == 2", "E + where 2 = g()", "", "*test_traceback_failure.py:4: AssertionError" ]) result = testdir.runpytest(p1) # "auto" result.stdout.fnmatch_lines([ "*test_traceback_failure.py F", "====* FAILURES *====", "____*____", "", " def test_onefails():", "> f(3)", "", "*test_*.py:6: ", "", " def f(x):", "> assert x == g()", "E assert 3 == 2", "E + where 2 = g()", "", "*test_traceback_failure.py:4: AssertionError" ]) @pytest.mark.skipif("'__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" ) def test_warn_missing(testdir): testdir.makepyfile("") result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") result.stderr.fnmatch_lines([ "*WARNING*assert statements are not executed*", ]) result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--no-assert") result.stderr.fnmatch_lines([ "*WARNING*assert statements are not executed*", ]) def test_recursion_source_decode(testdir): testdir.makepyfile(""" def test_something(): pass """) testdir.makeini(""" [pytest] python_files = *.py """) result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(""" """) def test_AssertionError_message(testdir): testdir.makepyfile(""" def test_hello(): x,y = 1,2 assert 0, (x,y) """) result = testdir.runpytest() result.stdout.fnmatch_lines(""" *def test_hello* *assert 0, (x,y)* *AssertionError: (1, 2)* """) @pytest.mark.skipif(PY3, reason='This bug does not exist on PY3') def test_set_with_unsortable_elements(): # issue #718 class UnsortableKey(object): def __init__(self, name): self.name = name def __lt__(self, other): raise RuntimeError() def __repr__(self): return 'repr({0})'.format(self.name) def __eq__(self, other): return self.name == other.name def __hash__(self): return hash(self.name) left_set = set(UnsortableKey(str(i)) for i in range(1, 3)) right_set = set(UnsortableKey(str(i)) for i in range(2, 4)) expl = callequal(left_set, right_set, verbose=True) # skip first line because it contains the "construction" of the set, which does not have a guaranteed order expl = expl[1:] dedent = textwrap.dedent(""" Extra items in the left set: repr(1) Extra items in the right set: repr(3) Full diff (fallback to calling repr on each item): - repr(1) repr(2) + repr(3) """).strip() assert '\n'.join(expl) == dedent