# mypy: allow-untyped-defs
from __future__ import annotations

from collections.abc import MutableSequence
import sys
import textwrap
from typing import Any
from typing import NamedTuple

import attr

from _pytest import outcomes
import _pytest.assertion as plugin
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.config import Config as _Config
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
import pytest


def mock_config(verbose: int = 0, assertion_override: int | None = None):
    class TerminalWriter:
        def _highlight(self, source, lexer="python"):
            return source

    class Config:
        def get_terminal_writer(self):
            return TerminalWriter()

        def get_verbosity(self, verbosity_type: str | None = None) -> int:
            if verbosity_type is None:
                return verbose
            if verbosity_type == _Config.VERBOSITY_ASSERTIONS:
                if assertion_override is not None:
                    return assertion_override
                return verbose

            raise KeyError(f"Not mocked out: {verbosity_type}")

    return Config()


class TestMockConfig:
    SOME_VERBOSITY_LEVEL = 3
    SOME_OTHER_VERBOSITY_LEVEL = 10

    def test_verbose_exposes_value(self):
        config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

        assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL

    def test_get_assertion_override_not_set_verbose_value(self):
        config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

        assert (
            config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
            == TestMockConfig.SOME_VERBOSITY_LEVEL
        )

    def test_get_assertion_override_set_custom_value(self):
        config = mock_config(
            verbose=TestMockConfig.SOME_VERBOSITY_LEVEL,
            assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL,
        )

        assert (
            config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
            == TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL
        )

    def test_get_unsupported_type_error(self):
        config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

        with pytest.raises(KeyError):
            config.get_verbosity("--- NOT A VERBOSITY LEVEL ---")


class TestImportHookInstallation:
    @pytest.mark.parametrize("initial_conftest", [True, False])
    @pytest.mark.parametrize("mode", ["plain", "rewrite"])
    def test_conftest_assertion_rewrite(
        self, pytester: Pytester, initial_conftest, mode
    ) -> None:
        """Test that conftest files are using assertion rewrite on import (#1619)."""
        pytester.mkdir("foo")
        pytester.mkdir("foo/tests")
        conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py"
        contents = {
            conftest_path: """
                import pytest
                @pytest.fixture
                def check_first():
                    def check(values, value):
                        assert values.pop(0) == value
                    return check
            """,
            "foo/tests/test_foo.py": """
                def test(check_first):
                    check_first([10, 30], 30)
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.runpytest_subprocess(f"--assert={mode}")
        if mode == "plain":
            expected = "E       AssertionError"
        elif mode == "rewrite":
            expected = "*assert 10 == 30*"
        else:
            assert 0
        result.stdout.fnmatch_lines([expected])

    def test_rewrite_assertions_pytester_plugin(self, pytester: Pytester) -> None:
        """
        Assertions in the pytester plugin must also benefit from assertion
        rewriting (#1920).
        """
        pytester.makepyfile(
            """
            pytest_plugins = ['pytester']
            def test_dummy_failure(pytester):  # how meta!
                pytester.makepyfile('def test(): assert 0')
                r = pytester.inline_run()
                r.assertoutcome(passed=1)
        """
        )
        result = pytester.runpytest_subprocess()
        result.stdout.fnmatch_lines(
            [
                ">       r.assertoutcome(passed=1)",
                "E       AssertionError: ([[][]], [[][]], [[]<TestReport *>[]])*",
                "E       assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}",
                "E         Omitting 1 identical items, use -vv to show",
                "E         Differing items:",
                "E         Use -v to get more diff",
            ]
        )
        # XXX: unstable output.
        result.stdout.fnmatch_lines_random(
            [
                "E         {'failed': 1} != {'failed': 0}",
                "E         {'passed': 0} != {'passed': 1}",
            ]
        )

    @pytest.mark.parametrize("mode", ["plain", "rewrite"])
    def test_pytest_plugins_rewrite(self, pytester: Pytester, mode) -> None:
        contents = {
            "conftest.py": """
                pytest_plugins = ['ham']
            """,
            "ham.py": """
                import pytest
                @pytest.fixture
                def check_first():
                    def check(values, value):
                        assert values.pop(0) == value
                    return check
            """,
            "test_foo.py": """
                def test_foo(check_first):
                    check_first([10, 30], 30)
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.runpytest_subprocess(f"--assert={mode}")
        if mode == "plain":
            expected = "E       AssertionError"
        elif mode == "rewrite":
            expected = "*assert 10 == 30*"
        else:
            assert 0
        result.stdout.fnmatch_lines([expected])

    @pytest.mark.parametrize("mode", ["str", "list"])
    def test_pytest_plugins_rewrite_module_names(
        self, pytester: Pytester, mode
    ) -> None:
        """Test that pluginmanager correct marks pytest_plugins variables
        for assertion rewriting if they are defined as plain strings or
        list of strings (#1888).
        """
        plugins = '"ham"' if mode == "str" else '["ham"]'
        contents = {
            "conftest.py": f"""
                pytest_plugins = {plugins}
            """,
            "ham.py": """
                import pytest
            """,
            "test_foo.py": """
                def test_foo(pytestconfig):
                    assert 'ham' in pytestconfig.pluginmanager.rewrite_hook._must_rewrite
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.runpytest_subprocess("--assert=rewrite")
        assert result.ret == 0

    def test_pytest_plugins_rewrite_module_names_correctly(
        self, pytester: Pytester
    ) -> None:
        """Test that we match files correctly when they are marked for rewriting (#2939)."""
        contents = {
            "conftest.py": """\
                pytest_plugins = "ham"
            """,
            "ham.py": "",
            "hamster.py": "",
            "test_foo.py": """\
                def test_foo(pytestconfig):
                    assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None
                    assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.runpytest_subprocess("--assert=rewrite")
        assert result.ret == 0

    @pytest.mark.parametrize("mode", ["plain", "rewrite"])
    @pytest.mark.parametrize("disable_plugin_autoload", ["env_var", "cli", ""])
    @pytest.mark.parametrize("explicit_specify", ["env_var", "cli", ""])
    def test_installed_plugin_rewrite(
        self,
        pytester: Pytester,
        mode: str,
        monkeypatch: pytest.MonkeyPatch,
        disable_plugin_autoload: str,
        explicit_specify: str,
    ) -> None:
        args = ["mainwrapper.py", "-s", f"--assert={mode}"]
        if disable_plugin_autoload == "env_var":
            monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
        elif disable_plugin_autoload == "cli":
            monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
            args.append("--disable-plugin-autoload")
        else:
            assert disable_plugin_autoload == ""
            monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)

        name = "spamplugin"

        if explicit_specify == "env_var":
            monkeypatch.setenv("PYTEST_PLUGINS", name)
        elif explicit_specify == "cli":
            args.append("-p")
            args.append(name)
        else:
            assert explicit_specify == ""

        # Make sure the hook is installed early enough so that plugins
        # installed via distribution package are rewritten.
        pytester.mkdir("hampkg")
        contents = {
            "hampkg/__init__.py": """\
                import pytest

                @pytest.fixture
                def check_first2():
                    def check(values, value):
                        assert values.pop(0) == value
                    return check
            """,
            "spamplugin.py": """\
            import pytest
            from hampkg import check_first2

            @pytest.fixture
            def check_first():
                def check(values, value):
                    assert values.pop(0) == value
                return check
            """,
            "mainwrapper.py": """\
            import importlib.metadata
            import pytest

            class DummyEntryPoint(object):
                name = 'spamplugin'
                module_name = 'spam.py'
                group = 'pytest11'

                def load(self):
                    import spamplugin
                    return spamplugin

            class DummyDistInfo(object):
                version = '1.0'
                files = ('spamplugin.py', 'hampkg/__init__.py')
                entry_points = (DummyEntryPoint(),)
                metadata = {'name': 'foo'}

            def distributions():
                return (DummyDistInfo(),)

            importlib.metadata.distributions = distributions
            pytest.main()
            """,
            "test_foo.py": """\
            def test(check_first):
                check_first([10, 30], 30)

            def test2(check_first2):
                check_first2([10, 30], 30)
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.run(sys.executable, *args)
        if mode == "plain":
            expected = "E       AssertionError"
        elif mode == "rewrite":
            expected = "*assert 10 == 30*"
        else:
            assert 0

        if not disable_plugin_autoload or explicit_specify:
            result.assert_outcomes(failed=2)
            result.stdout.fnmatch_lines([expected, expected])
        else:
            result.assert_outcomes(errors=2)
            result.stdout.fnmatch_lines(
                [
                    "E       fixture 'check_first' not found",
                    "E       fixture 'check_first2' not found",
                ]
            )

    def test_rewrite_ast(self, pytester: Pytester) -> None:
        pytester.mkdir("pkg")
        contents = {
            "pkg/__init__.py": """
                import pytest
                pytest.register_assert_rewrite('pkg.helper')
            """,
            "pkg/helper.py": """
                def tool():
                    a, b = 2, 3
                    assert a == b
            """,
            "pkg/plugin.py": """
                import pytest, pkg.helper
                @pytest.fixture
                def tool():
                    return pkg.helper.tool
            """,
            "pkg/other.py": """
                values = [3, 2]
                def tool():
                    assert values.pop() == 3
            """,
            "conftest.py": """
                pytest_plugins = ['pkg.plugin']
            """,
            "test_pkg.py": """
                import pkg.other
                def test_tool(tool):
                    tool()
                def test_other():
                    pkg.other.tool()
            """,
        }
        pytester.makepyfile(**contents)
        result = pytester.runpytest_subprocess("--assert=rewrite")
        result.stdout.fnmatch_lines(
            [
                ">*assert a == b*",
                "E*assert 2 == 3*",
                ">*assert values.pop() == 3*",
                "E*AssertionError",
            ]
        )

    def test_register_assert_rewrite_checks_types(self) -> None:
        with pytest.raises(TypeError):
            pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"])  # type: ignore
        pytest.register_assert_rewrite(
            "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2"
        )


class TestBinReprIntegration:
    def test_pytest_assertrepr_compare_called(self, pytester: Pytester) -> None:
        pytester.makeconftest(
            """
            import pytest
            values = []
            def pytest_assertrepr_compare(op, left, right):
                values.append((op, left, right))

            @pytest.fixture
            def list(request):
                return values
        """
        )
        pytester.makepyfile(
            """
            def test_hello():
                assert 0 == 1
            def test_check(list):
                assert list == [("==", 0, 1)]
        """
        )
        result = pytester.runpytest("-v")
        result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])


def callop(op: str, left: Any, right: Any, verbose: int = 0) -> list[str] | None:
    config = mock_config(verbose=verbose)
    return plugin.pytest_assertrepr_compare(config, op, left, right)


def callequal(left: Any, right: Any, verbose: int = 0) -> list[str] | None:
    return callop("==", left, right, verbose)


class TestAssert_reprcompare:
    def test_different_types(self) -> None:
        assert callequal([0, 1], "foo") is None

    def test_summary(self) -> None:
        lines = callequal([0, 1], [0, 2])
        assert lines is not None
        summary = lines[0]
        assert len(summary) < 65

    def test_text_diff(self) -> None:
        assert callequal("spam", "eggs") == [
            "'spam' == 'eggs'",
            "",
            "- eggs",
            "+ spam",
        ]

    def test_text_skipping(self) -> None:
        lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs")
        assert lines is not None
        assert "Skipping" in lines[2]
        for line in lines:
            assert "a" * 50 not in line

    def test_text_skipping_verbose(self) -> None:
        lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=1)
        assert lines is not None
        assert "- " + "a" * 50 + "eggs" in lines
        assert "+ " + "a" * 50 + "spam" in lines

    def test_multiline_text_diff(self) -> None:
        left = "foo\nspam\nbar"
        right = "foo\neggs\nbar"
        diff = callequal(left, right)
        assert diff is not None
        assert "- eggs" in diff
        assert "+ spam" in diff

    def test_bytes_diff_normal(self) -> None:
        """Check special handling for bytes diff (#5260)"""
        diff = callequal(b"spam", b"eggs")

        assert diff == [
            "b'spam' == b'eggs'",
            "",
            "At index 0 diff: b's' != b'e'",
            "Use -v to get more diff",
        ]

    def test_bytes_diff_verbose(self) -> None:
        """Check special handling for bytes diff (#5260)"""
        diff = callequal(b"spam", b"eggs", verbose=1)
        assert diff == [
            "b'spam' == b'eggs'",
            "",
            "At index 0 diff: b's' != b'e'",
            "",
            "Full diff:",
            "- b'eggs'",
            "+ b'spam'",
        ]

    def test_list(self) -> None:
        expl = callequal([0, 1], [0, 2])
        assert expl is not None
        assert len(expl) > 1

    @pytest.mark.parametrize(
        ["left", "right", "expected"],
        [
            pytest.param(
                [0, 1],
                [0, 2],
                """
                Full diff:
                  [
                      0,
                -     2,
                ?     ^
                +     1,
                ?     ^
                  ]
                """,
                id="lists",
            ),
            pytest.param(
                {0: 1},
                {0: 2},
                """
                Full diff:
                  {
                -     0: 2,
                ?        ^
                +     0: 1,
                ?        ^
                  }
            """,
                id="dicts",
            ),
            pytest.param(
                {0, 1},
                {0, 2},
                """
                Full diff:
                  {
                      0,
                -     2,
                ?     ^
                +     1,
                ?     ^
                  }
            """,
                id="sets",
            ),
        ],
    )
    def test_iterable_full_diff(self, left, right, expected) -> None:
        """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=0)
        assert expl is not None
        assert expl[-1] == "Use -v to get more diff"
        verbose_expl = callequal(left, right, verbose=1)
        assert verbose_expl is not None
        assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())

    def test_iterable_quiet(self) -> None:
        expl = callequal([1, 2], [10, 2], verbose=-1)
        assert expl == [
            "[1, 2] == [10, 2]",
            "",
            "At index 0 diff: 1 != 10",
            "Use -v to get more diff",
        ]

    def test_iterable_full_diff_ci(
        self, monkeypatch: MonkeyPatch, pytester: Pytester
    ) -> None:
        pytester.makepyfile(
            r"""
            def test_full_diff():
                left = [0, 1]
                right = [0, 2]
                assert left == right
        """
        )
        monkeypatch.setenv("CI", "true")
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(["E         Full diff:"])

        # Setting CI to empty string is same as having it undefined
        monkeypatch.setenv("CI", "")
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(["E         Use -v to get more diff"])

        monkeypatch.delenv("CI", raising=False)
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(["E         Use -v to get more diff"])

    def test_list_different_lengths(self) -> None:
        expl = callequal([0, 1], [0, 1, 2])
        assert expl is not None
        assert len(expl) > 1
        expl = callequal([0, 1, 2], [0, 1])
        assert expl is not None
        assert len(expl) > 1

    def test_list_wrap_for_multiple_lines(self) -> None:
        long_d = "d" * 80
        l1 = ["a", "b", "c"]
        l2 = ["a", "b", "c", long_d]
        diff = callequal(l1, l2, verbose=True)
        assert diff == [
            "['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']",
            "",
            "Right contains one more item: '" + long_d + "'",
            "",
            "Full diff:",
            "  [",
            "      'a',",
            "      'b',",
            "      'c',",
            "-     '" + long_d + "',",
            "  ]",
        ]

        diff = callequal(l2, l1, verbose=True)
        assert diff == [
            "['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']",
            "",
            "Left contains one more item: '" + long_d + "'",
            "",
            "Full diff:",
            "  [",
            "      'a',",
            "      'b',",
            "      'c',",
            "+     '" + long_d + "',",
            "  ]",
        ]

    def test_list_wrap_for_width_rewrap_same_length(self) -> None:
        long_a = "a" * 30
        long_b = "b" * 30
        long_c = "c" * 30
        l1 = [long_a, long_b, long_c]
        l2 = [long_b, long_c, long_a]
        diff = callequal(l1, l2, verbose=True)
        assert diff == [
            "['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']",
            "",
            "At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
            "",
            "Full diff:",
            "  [",
            "+     'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
            "      'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
            "      'cccccccccccccccccccccccccccccc',",
            "-     'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
            "  ]",
        ]

    def test_list_dont_wrap_strings(self) -> None:
        long_a = "a" * 10
        l1 = ["a"] + [long_a for _ in range(7)]
        l2 = ["should not get wrapped"]
        diff = callequal(l1, l2, verbose=True)
        assert diff == [
            "['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']",
            "",
            "At index 0 diff: 'a' != 'should not get wrapped'",
            "Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
            "",
            "Full diff:",
            "  [",
            "-     'should not get wrapped',",
            "+     'a',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "+     'aaaaaaaaaa',",
            "  ]",
        ]

    def test_dict_wrap(self) -> None:
        d1 = {"common": 1, "env": {"env1": 1, "env2": 2}}
        d2 = {"common": 1, "env": {"env1": 1}}

        diff = callequal(d1, d2, verbose=True)
        assert diff == [
            "{'common': 1,...1, 'env2': 2}} == {'common': 1,...: {'env1': 1}}",
            "",
            "Omitting 1 identical items, use -vv to show",
            "Differing items:",
            "{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}",
            "",
            "Full diff:",
            "  {",
            "      'common': 1,",
            "      'env': {",
            "          'env1': 1,",
            "+         'env2': 2,",
            "      },",
            "  }",
        ]

        long_a = "a" * 80
        sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}}
        d1 = {"env": {"sub": sub}}
        d2 = {"env": {"sub": sub}, "new": 1}
        diff = callequal(d1, d2, verbose=True)
        assert diff == [
            "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}",
            "",
            "Omitting 1 identical items, use -vv to show",
            "Right contains 1 more item:",
            "{'new': 1}",
            "",
            "Full diff:",
            "  {",
            "      'env': {",
            "          'sub': {",
            f"              'long_a': '{long_a}',",
            "              'sub1': {",
            "                  'long_a': 'substring that gets wrapped substring that gets wrapped '",
            "                  'substring that gets wrapped ',",
            "              },",
            "          },",
            "      },",
            "-     'new': 1,",
            "  }",
        ]

    def test_dict(self) -> None:
        expl = callequal({"a": 0}, {"a": 1})
        assert expl is not None
        assert len(expl) > 1

    def test_dict_omitting(self) -> None:
        lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1})
        assert lines is not None
        assert lines[2].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_with_verbosity_1(self) -> None:
        """Ensure differing items are visible for verbosity=1 (#1512)."""
        lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1)
        assert lines is not None
        assert lines[1] == ""
        assert lines[2].startswith("Omitting 1 identical item")
        assert lines[3].startswith("Differing items")
        assert lines[4] == "{'a': 0} != {'a': 1}"
        assert "Common items" not in lines

    def test_dict_omitting_with_verbosity_2(self) -> None:
        lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=2)
        assert lines is not None
        assert lines[2].startswith("Common items:")
        assert "Omitting" not in lines[2]
        assert lines[3] == "{'b': 1}"

    def test_dict_different_items(self) -> None:
        lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2)
        assert lines == [
            "{'a': 0} == {'b': 1, 'c': 2}",
            "",
            "Left contains 1 more item:",
            "{'a': 0}",
            "Right contains 2 more items:",
            "{'b': 1, 'c': 2}",
            "",
            "Full diff:",
            "  {",
            "-     'b': 1,",
            "?      ^   ^",
            "+     'a': 0,",
            "?      ^   ^",
            "-     'c': 2,",
            "  }",
        ]
        lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2)
        assert lines == [
            "{'b': 1, 'c': 2} == {'a': 0}",
            "",
            "Left contains 2 more items:",
            "{'b': 1, 'c': 2}",
            "Right contains 1 more item:",
            "{'a': 0}",
            "",
            "Full diff:",
            "  {",
            "-     'a': 0,",
            "?      ^   ^",
            "+     'b': 1,",
            "?      ^   ^",
            "+     'c': 2,",
            "  }",
        ]

    def test_sequence_different_items(self) -> None:
        lines = callequal((1, 2), (3, 4, 5), verbose=2)
        assert lines == [
            "(1, 2) == (3, 4, 5)",
            "",
            "At index 0 diff: 1 != 3",
            "Right contains one more item: 5",
            "",
            "Full diff:",
            "  (",
            "-     3,",
            "?     ^",
            "+     1,",
            "?     ^",
            "-     4,",
            "?     ^",
            "+     2,",
            "?     ^",
            "-     5,",
            "  )",
        ]
        lines = callequal((1, 2, 3), (4,), verbose=2)
        assert lines == [
            "(1, 2, 3) == (4,)",
            "",
            "At index 0 diff: 1 != 4",
            "Left contains 2 more items, first extra item: 2",
            "",
            "Full diff:",
            "  (",
            "-     4,",
            "?     ^",
            "+     1,",
            "?     ^",
            "+     2,",
            "+     3,",
            "  )",
        ]
        lines = callequal((1, 2, 3), (1, 20, 3), verbose=2)
        assert lines == [
            "(1, 2, 3) == (1, 20, 3)",
            "",
            "At index 1 diff: 2 != 20",
            "",
            "Full diff:",
            "  (",
            "      1,",
            "-     20,",
            "?      -",
            "+     2,",
            "      3,",
            "  )",
        ]

    def test_set(self) -> None:
        expl = callequal({0, 1}, {0, 2})
        assert expl is not None
        assert len(expl) > 1

    def test_frozenzet(self) -> None:
        expl = callequal(frozenset([0, 1]), {0, 2})
        assert expl is not None
        assert len(expl) > 1

    def test_Sequence(self) -> None:
        # Test comparing with a Sequence subclass.
        class TestSequence(MutableSequence[int]):
            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, index, value):
                pass

        expl = callequal(TestSequence([0, 1]), list([0, 2]))
        assert expl is not None
        assert len(expl) > 1

    def test_list_tuples(self) -> None:
        expl = callequal([], [(1, 2)])
        assert expl is not None
        assert len(expl) > 1
        expl = callequal([(1, 2)], [])
        assert expl is not None
        assert len(expl) > 1

    def test_list_bad_repr(self) -> None:
        class A:
            def __repr__(self):
                raise ValueError(42)

        expl = callequal([], [A()])
        assert expl is not None
        assert "ValueError" in "".join(expl)
        expl = callequal({}, {"1": A()}, verbose=2)
        assert expl is not None
        assert expl[0].startswith("{} == <[ValueError")
        assert "raised in repr" in expl[0]
        assert expl[2:] == [
            "(pytest_assertion plugin: representation of details failed:"
            f" {__file__}:{A.__repr__.__code__.co_firstlineno + 1}: ValueError: 42.",
            " Probably an object has a faulty __repr__.)",
        ]

    def test_one_repr_empty(self) -> None:
        """The faulty empty string repr did trigger an 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) -> None:
        expl = callequal("foo", "bar")
        assert expl is not None
        assert "raised in repr()" not in " ".join(expl)

    def test_unicode(self) -> None:
        assert callequal("£€", "£") == [
            "'£€' == '£'",
            "",
            "- £",
            "+ £€",
        ]

    def test_nonascii_text(self) -> None:
        """
        :issue: 877
        non ascii python2 str caused a UnicodeDecodeError
        """

        class A(str):
            def __repr__(self):
                return "\xff"

        expl = callequal(A(), "1")
        assert expl == ["ÿ == '1'", "", "- 1"]

    def test_format_nonascii_explanation(self) -> None:
        assert util.format_explanation("λ")

    def test_mojibake(self) -> None:
        # issue 429
        left = b"e"
        right = b"\xc3\xa9"
        expl = callequal(left, right)
        assert expl is not None
        for line in expl:
            assert isinstance(line, str)
        msg = "\n".join(expl)
        assert msg

    def test_nfc_nfd_same_string(self) -> None:
        # issue 3426
        left = "hyv\xe4"
        right = "hyva\u0308"
        expl = callequal(left, right)
        assert expl == [
            r"'hyv\xe4' == 'hyva\u0308'",
            "",
            f"- {right!s}",
            f"+ {left!s}",
        ]

        expl = callequal(left, right, verbose=2)
        assert expl == [
            r"'hyv\xe4' == 'hyva\u0308'",
            "",
            f"- {right!s}",
            f"+ {left!s}",
        ]


class TestAssert_reprcompare_dataclass:
    def test_dataclasses(self, pytester: Pytester) -> None:
        p = pytester.copy_example("dataclasses/test_compare_dataclasses.py")
        result = pytester.runpytest(p)
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.fnmatch_lines(
            [
                "E         Omitting 1 identical items, use -vv to show",
                "E         Differing attributes:",
                "E         ['field_b']",
                "E         ",
                "E         Drill down into differing attribute field_b:",
                "E           field_b: 'b' != 'c'",
                "E           - c",
                "E           + b",
            ],
            consecutive=True,
        )

    def test_recursive_dataclasses(self, pytester: Pytester) -> None:
        p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
        result = pytester.runpytest(p)
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.fnmatch_lines(
            [
                "E         Omitting 1 identical items, use -vv to show",
                "E         Differing attributes:",
                "E         ['g', 'h', 'j']",
                "E         ",
                "E         Drill down into differing attribute g:",
                "E           g: S(a=10, b='ten') != S(a=20, b='xxx')...",
                "E         ",
                "E         ...Full output truncated (51 lines hidden), use '-vv' to show",
            ],
            consecutive=True,
        )

    def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None:
        p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.fnmatch_lines(
            [
                "E         Matching attributes:",
                "E         ['i']",
                "E         Differing attributes:",
                "E         ['g', 'h', 'j']",
                "E         ",
                "E         Drill down into differing attribute g:",
                "E           g: S(a=10, b='ten') != S(a=20, b='xxx')",
                "E           ",
                "E           Differing attributes:",
                "E           ['a', 'b']",
                "E           ",
                "E           Drill down into differing attribute a:",
                "E             a: 10 != 20",
                "E           ",
                "E           Drill down into differing attribute b:",
                "E             b: 'ten' != 'xxx'",
                "E             - xxx",
                "E             + ten",
                "E         ",
                "E         Drill down into differing attribute h:",
            ],
            consecutive=True,
        )

    def test_dataclasses_verbose(self, pytester: Pytester) -> None:
        p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py")
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.fnmatch_lines(
            [
                "*Matching attributes:*",
                "*['field_a']*",
                "*Differing attributes:*",
                "*field_b: 'b' != 'c'*",
            ]
        )

    def test_dataclasses_with_attribute_comparison_off(
        self, pytester: Pytester
    ) -> None:
        p = pytester.copy_example(
            "dataclasses/test_compare_dataclasses_field_comparison_off.py"
        )
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=0, passed=1)

    def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
        p = pytester.copy_example(
            "dataclasses/test_compare_two_different_dataclasses.py"
        )
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=0, passed=1)

    def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None:
        p = pytester.copy_example(
            "dataclasses/test_compare_dataclasses_with_custom_eq.py"
        )
        # issue 9362
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.no_re_match_line(".*Differing attributes.*")

    def test_data_classes_with_initvar(self, pytester: Pytester) -> None:
        p = pytester.copy_example("dataclasses/test_compare_initvar.py")
        # issue 9820
        result = pytester.runpytest(p, "-vv")
        result.assert_outcomes(failed=1, passed=0)
        result.stdout.no_re_match_line(".*AttributeError.*")


class TestAssert_reprcompare_attrsclass:
    def test_attrs(self) -> None:
        @attr.s
        class SimpleDataObject:
            field_a = attr.ib()
            field_b = attr.ib()

        left = SimpleDataObject(1, "b")
        right = SimpleDataObject(1, "c")

        lines = callequal(left, right)
        assert lines is not None
        assert lines[2].startswith("Omitting 1 identical item")
        assert "Matching attributes" not in lines
        for line in lines[2:]:
            assert "field_a" not in line

    def test_attrs_recursive(self) -> None:
        @attr.s
        class OtherDataObject:
            field_c = attr.ib()
            field_d = attr.ib()

        @attr.s
        class SimpleDataObject:
            field_a = attr.ib()
            field_b = attr.ib()

        left = SimpleDataObject(OtherDataObject(1, "a"), "b")
        right = SimpleDataObject(OtherDataObject(1, "b"), "b")

        lines = callequal(left, right)
        assert lines is not None
        assert "Matching attributes" not in lines
        for line in lines[1:]:
            assert "field_b:" not in line
            assert "field_c:" not in line

    def test_attrs_recursive_verbose(self) -> None:
        @attr.s
        class OtherDataObject:
            field_c = attr.ib()
            field_d = attr.ib()

        @attr.s
        class SimpleDataObject:
            field_a = attr.ib()
            field_b = attr.ib()

        left = SimpleDataObject(OtherDataObject(1, "a"), "b")
        right = SimpleDataObject(OtherDataObject(1, "b"), "b")

        lines = callequal(left, right)
        assert lines is not None
        # indentation in output because of nested object structure
        assert "    field_d: 'a' != 'b'" in lines

    def test_attrs_verbose(self) -> None:
        @attr.s
        class SimpleDataObject:
            field_a = attr.ib()
            field_b = attr.ib()

        left = SimpleDataObject(1, "b")
        right = SimpleDataObject(1, "c")

        lines = callequal(left, right, verbose=2)
        assert lines is not None
        assert lines[2].startswith("Matching attributes:")
        assert "Omitting" not in lines[2]
        assert lines[3] == "['field_a']"

    def test_attrs_with_attribute_comparison_off(self) -> None:
        @attr.s
        class SimpleDataObject:
            field_a = attr.ib()
            field_b = attr.ib(eq=False)

        left = SimpleDataObject(1, "b")
        right = SimpleDataObject(1, "b")

        lines = callequal(left, right, verbose=2)
        assert lines is not None
        assert lines[2].startswith("Matching attributes:")
        assert "Omitting" not in lines[1]
        assert lines[3] == "['field_a']"
        for line in lines[3:]:
            assert "field_b" not in line

    def test_comparing_two_different_attrs_classes(self) -> None:
        @attr.s
        class SimpleDataObjectOne:
            field_a = attr.ib()
            field_b = attr.ib()

        @attr.s
        class SimpleDataObjectTwo:
            field_a = attr.ib()
            field_b = attr.ib()

        left = SimpleDataObjectOne(1, "b")
        right = SimpleDataObjectTwo(1, "c")

        lines = callequal(left, right)
        assert lines is None

    def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
        @attr.s(
            auto_detect=True
        )  # attr.s doesn't ignore a custom eq if auto_detect=True
        class SimpleDataObject:
            field_a = attr.ib()

            def __eq__(self, other):  # pragma: no cover
                return super().__eq__(other)

        left = SimpleDataObject(1)
        right = SimpleDataObject(2)
        # issue 9362
        lines = callequal(left, right, verbose=2)
        assert lines is None

    def test_attrs_with_custom_eq(self) -> None:
        @attr.define(slots=False)
        class SimpleDataObject:
            field_a = attr.ib()

            def __eq__(self, other):  # pragma: no cover
                return super().__eq__(other)

        left = SimpleDataObject(1)
        right = SimpleDataObject(2)
        # issue 9362
        lines = callequal(left, right, verbose=2)
        assert lines is None


class TestAssert_reprcompare_namedtuple:
    def test_namedtuple(self) -> None:
        class NT(NamedTuple):
            a: Any
            b: Any

        left = NT(1, "b")
        right = NT(1, "c")

        lines = callequal(left, right)
        assert lines == [
            "NT(a=1, b='b') == NT(a=1, b='c')",
            "",
            "Omitting 1 identical items, use -vv to show",
            "Differing attributes:",
            "['b']",
            "",
            "Drill down into differing attribute b:",
            "  b: 'b' != 'c'",
            "  - c",
            "  + b",
            "Use -v to get more diff",
        ]

    def test_comparing_two_different_namedtuple(self) -> None:
        class NT1(NamedTuple):
            a: Any
            b: Any

        class NT2(NamedTuple):
            a: Any
            b: Any

        left = NT1(1, "b")
        right = NT2(2, "b")

        lines = callequal(left, right)
        # Because the types are different, uses the generic sequence matcher.
        assert lines == [
            "NT1(a=1, b='b') == NT2(a=2, b='b')",
            "",
            "At index 0 diff: 1 != 2",
            "Use -v to get more diff",
        ]


class TestFormatExplanation:
    def test_special_chars_full(self, pytester: Pytester) -> None:
        # Issue 453, for the bug this would raise IndexError
        pytester.makepyfile(
            """
            def test_foo():
                assert '\\n}' == ''
        """
        )
        result = pytester.runpytest()
        assert result.ret == 1
        result.stdout.fnmatch_lines(["*AssertionError*"])

    def test_fmt_simple(self) -> None:
        expl = "assert foo"
        assert util.format_explanation(expl) == "assert foo"

    def test_fmt_where(self) -> None:
        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) -> None:
        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) -> None:
        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) -> None:
        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) -> None:
        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) -> None:
        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) -> None:
        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


class TestTruncateExplanation:
    # The number of lines in the truncation explanation message. Used
    # to calculate that results have the expected length.
    LINES_IN_TRUNCATION_MSG = 2

    def test_doesnt_truncate_when_input_is_empty_list(self) -> None:
        expl: list[str] = []
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
        assert result == expl

    def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None:
        expl = ["a" * 100 for x in range(5)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
        assert result == expl

    def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
        expl = ["" for x in range(50)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
        assert len(result) != len(expl)
        assert result != expl
        assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
        assert "Full output truncated" in result[-1]
        assert "42 lines hidden" in result[-1]
        last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
        assert last_line_before_trunc_msg.endswith("...")

    def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
        total_lines = 100
        expl = ["a" for x in range(total_lines)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
        assert result != expl
        assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
        assert "Full output truncated" in result[-1]
        assert f"{total_lines - 8} lines hidden" in result[-1]
        last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
        assert last_line_before_trunc_msg.endswith("...")

    def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None:
        """The number of line in the result is 9, the same number as if we truncated."""
        expl = ["a" for x in range(9)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
        assert result == expl
        assert "truncated" not in result[-1]

    def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars(
        self,
    ) -> None:
        line = "a" * 10
        expl = [line, line]
        result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
        assert result == [line, line]

    def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
        self,
    ) -> None:
        line = "a" * 10
        expl = [line, line]
        result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
        assert result == [line, line]

    def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
        expl = [chr(97 + x) * 80 for x in range(16)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
        assert result != expl
        assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG
        assert "Full output truncated" in result[-1]
        assert "8 lines hidden" in result[-1]
        last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
        assert last_line_before_trunc_msg.endswith("...")

    def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None:
        expl = ["a" * 250 for x in range(10)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999)
        assert result != expl
        assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG
        assert "Full output truncated" in result[-1]
        assert "7 lines hidden" in result[-1]
        last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
        assert last_line_before_trunc_msg.endswith("...")

    def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None:
        expl = ["a" * 250 for x in range(1000)]
        result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
        assert result != expl
        assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG
        assert "Full output truncated" in result[-1]
        assert "1000 lines hidden" in result[-1]
        last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
        assert last_line_before_trunc_msg.endswith("...")

    def test_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None:
        """Test against full runpytest() output."""
        line_count = 7
        line_len = 100
        expected_truncated_lines = 2
        pytester.makepyfile(
            rf"""
            def test_many_lines():
                a = list([str(i)[0] * {line_len} for i in range({line_count})])
                b = a[::2]
                a = '\n'.join(map(str, a))
                b = '\n'.join(map(str, b))
                assert a == b
        """
        )
        monkeypatch.delenv("CI", raising=False)

        result = pytester.runpytest()
        # without -vv, truncate the message showing a few diff lines only
        result.stdout.fnmatch_lines(
            [
                "*+ 1*",
                "*+ 3*",
                f"*truncated ({expected_truncated_lines} lines hidden)*use*-vv*",
            ]
        )

        result = pytester.runpytest("-vv")
        result.stdout.fnmatch_lines(["* 6*"])

        # Setting CI to empty string is same as having it undefined
        monkeypatch.setenv("CI", "")
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(
            [
                "*+ 1*",
                "*+ 3*",
                f"*truncated ({expected_truncated_lines} lines hidden)*use*-vv*",
            ]
        )

        monkeypatch.setenv("CI", "1")
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(["* 6*"])

    @pytest.mark.parametrize(
        ["truncation_lines", "truncation_chars", "expected_lines_hidden"],
        (
            (3, None, 3),
            (4, None, 0),
            (0, None, 0),
            (None, 8, 6),
            (None, 9, 0),
            (None, 0, 0),
            (0, 0, 0),
            (0, 1000, 0),
            (1000, 0, 0),
        ),
    )
    def test_truncation_with_ini(
        self,
        monkeypatch,
        pytester: Pytester,
        truncation_lines: int | None,
        truncation_chars: int | None,
        expected_lines_hidden: int,
    ) -> None:
        pytester.makepyfile(
            """\
            string_a = "123456789\\n23456789\\n3"
            string_b = "123456789\\n23456789\\n4"

            def test():
                assert string_a == string_b
            """
        )

        # This test produces 6 lines of diff output or 79 characters
        # So the effect should be when threshold is < 4 lines (considering 2 additional lines for explanation)
        # Or < 9 characters (considering 70 additional characters for explanation)

        monkeypatch.delenv("CI", raising=False)

        ini = "[pytest]\n"
        if truncation_lines is not None:
            ini += f"truncation_limit_lines = {truncation_lines}\n"
        if truncation_chars is not None:
            ini += f"truncation_limit_chars = {truncation_chars}\n"
        pytester.makeini(ini)

        result = pytester.runpytest()

        if expected_lines_hidden != 0:
            result.stdout.fnmatch_lines(
                [f"*truncated ({expected_lines_hidden} lines hidden)*"]
            )
        else:
            result.stdout.no_fnmatch_line("*truncated*")
            result.stdout.fnmatch_lines(
                [
                    "*- 4*",
                    "*+ 3*",
                ]
            )


def test_python25_compile_issue257(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_rewritten():
            assert 1 == 2
        # some comment
    """
    )
    result = pytester.runpytest()
    assert result.ret == 1
    result.stdout.fnmatch_lines(
        """
            *E*assert 1 == 2*
            *1 failed*
    """
    )


def test_rewritten(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_rewritten():
            assert "@py_builtins" in globals()
    """
    )
    assert pytester.runpytest().ret == 0


def test_reprcompare_notin() -> None:
    assert callop("not in", "foo", "aaafoobbb") == [
        "'foo' not in 'aaafoobbb'",
        "",
        "'foo' is contained here:",
        "  aaafoobbb",
        "?    +++",
    ]


def test_reprcompare_whitespaces() -> None:
    assert callequal("\r\n", "\n") == [
        r"'\r\n' == '\n'",
        "",
        r"Strings contain only whitespace, escaping them using repr()",
        r"- '\n'",
        r"+ '\r\n'",
        r"?  ++",
    ]


class TestSetAssertions:
    @pytest.mark.parametrize("op", [">=", ">", "<=", "<", "=="])
    def test_set_extra_item(self, op, pytester: Pytester) -> None:
        pytester.makepyfile(
            f"""
            def test_hello():
                x = set("hello x")
                y = set("hello y")
                assert x {op} y
        """
        )

        result = pytester.runpytest()
        result.stdout.fnmatch_lines(
            [
                "*def test_hello():*",
                f"*assert x {op} y*",
            ]
        )
        if op in [">=", ">", "=="]:
            result.stdout.fnmatch_lines(
                [
                    "*E*Extra items in the right set:*",
                    "*E*'y'",
                ]
            )
        if op in ["<=", "<", "=="]:
            result.stdout.fnmatch_lines(
                [
                    "*E*Extra items in the left set:*",
                    "*E*'x'",
                ]
            )

    @pytest.mark.parametrize("op", [">", "<", "!="])
    def test_set_proper_superset_equal(self, pytester: Pytester, op) -> None:
        pytester.makepyfile(
            f"""
            def test_hello():
                x = set([1, 2, 3])
                y = x.copy()
                assert x {op} y
        """
        )

        result = pytester.runpytest()
        result.stdout.fnmatch_lines(
            [
                "*def test_hello():*",
                f"*assert x {op} y*",
                "*E*Both sets are equal*",
            ]
        )

    def test_pytest_assertrepr_compare_integration(self, pytester: Pytester) -> None:
        pytester.makepyfile(
            """
            def test_hello():
                x = set(range(100))
                y = x.copy()
                y.remove(50)
                assert x == y
        """
        )
        result = pytester.runpytest()
        result.stdout.fnmatch_lines(
            [
                "*def test_hello():*",
                "*assert x == y*",
                "*E*Extra items*left*",
                "*E*50*",
                "*= 1 failed in*",
            ]
        )


def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
    pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
    a = pytester.mkdir("a")
    a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8")
    a.joinpath("conftest.py").write_text(
        'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8"
    )
    b = pytester.mkdir("b")
    b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8")
    b.joinpath("conftest.py").write_text(
        'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8"
    )

    result = pytester.runpytest()
    result.stdout.fnmatch_lines(
        [
            "*def test_a():*",
            "*E*assert summary a*",
            "*def test_b():*",
            "*E*assert summary b*",
            "*def test_base():*",
            "*E*assert 1 == 2*",
        ]
    )


def test_assertion_options(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_hello():
            x = 3
            assert x == 4
    """
    )
    result = pytester.runpytest()
    assert "3 == 4" in result.stdout.str()
    result = pytester.runpytest_subprocess("--assert=plain")
    result.stdout.no_fnmatch_line("*3 == 4*")


def test_triple_quoted_string_issue113(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_hello():
            assert "" == '''
    '''"""
    )
    result = pytester.runpytest("--fulltrace")
    result.stdout.fnmatch_lines(["*1 failed*"])
    result.stdout.no_fnmatch_line("*SyntaxError*")


def test_traceback_failure(pytester: Pytester) -> None:
    p1 = pytester.makepyfile(
        """
        def g():
            return 2
        def f(x):
            assert x == g()
        def test_onefails():
            f(3)
    """
    )
    result = pytester.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 = pytester.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",
        ]
    )


def test_exception_handling_no_traceback(pytester: Pytester) -> None:
    """Handle chain exceptions in tasks submitted by the multiprocess module (#1984)."""
    p1 = pytester.makepyfile(
        """
        from multiprocessing import Pool

        def process_task(n):
            assert n == 10

        def multitask_job():
            tasks = [1]
            with Pool(processes=1) as pool:
                pool.map(process_task, tasks)

        def test_multitask_job():
            multitask_job()
    """
    )
    pytester.syspathinsert()
    result = pytester.runpytest(p1, "--tb=long")
    result.stdout.fnmatch_lines(
        [
            "====* FAILURES *====",
            "*multiprocessing.pool.RemoteTraceback:*",
            "Traceback (most recent call last):",
            "*assert n == 10",
            "The above exception was the direct cause of the following exception:",
            "> * multitask_job()",
        ]
    )


@pytest.mark.skipif("'__pypy__' in sys.builtin_module_names")
@pytest.mark.parametrize(
    "cmdline_args, warning_output",
    [
        (
            ["-OO", "-m", "pytest", "-h"],
            ["warning :*PytestConfigWarning:*assert statements are not executed*"],
        ),
        (
            ["-OO", "-m", "pytest"],
            [
                "=*= warnings summary =*=",
                "*PytestConfigWarning:*assert statements are not executed*",
            ],
        ),
        (
            ["-OO", "-m", "pytest", "--assert=plain"],
            [
                "=*= warnings summary =*=",
                "*PytestConfigWarning: ASSERTIONS ARE NOT EXECUTED and FAILING TESTS WILL PASS.  "
                "Are you using python -O?",
            ],
        ),
    ],
)
def test_warn_missing(pytester: Pytester, cmdline_args, warning_output) -> None:
    pytester.makepyfile("")

    result = pytester.run(sys.executable, *cmdline_args)
    result.stdout.fnmatch_lines(warning_output)


def test_recursion_source_decode(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_something():
            pass
    """
    )
    pytester.makeini(
        """
        [pytest]
        python_files = *.py
    """
    )
    result = pytester.runpytest("--collect-only")
    result.stdout.fnmatch_lines(
        [
            "  <Module*>",
        ]
    )


def test_AssertionError_message(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_hello():
            x,y = 1,2
            assert 0, (x,y)
    """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines(
        """
        *def test_hello*
        *assert 0, (x,y)*
        *AssertionError: (1, 2)*
    """
    )


def test_diff_newline_at_end(pytester: Pytester) -> None:
    pytester.makepyfile(
        r"""
        def test_diff():
            assert 'asdf' == 'asdf\n'
    """
    )

    result = pytester.runpytest()
    result.stdout.fnmatch_lines(
        r"""
        *assert 'asdf' == 'asdf\n'
        *  - asdf
        *  ?     -
        *  + asdf
    """
    )


@pytest.mark.filterwarnings("default")
def test_assert_tuple_warning(pytester: Pytester) -> None:
    msg = "assertion is always true"
    pytester.makepyfile(
        """
        def test_tuple():
            assert(False, 'you shall not pass')
    """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines([f"*test_assert_tuple_warning.py:2:*{msg}*"])

    # tuples with size != 2 should not trigger the warning
    pytester.makepyfile(
        """
        def test_tuple():
            assert ()
    """
    )
    result = pytester.runpytest()
    assert msg not in result.stdout.str()


def test_assert_indirect_tuple_no_warning(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def test_tuple():
            tpl = ('foo', 'bar')
            assert tpl
    """
    )
    result = pytester.runpytest()
    output = "\n".join(result.stdout.lines)
    assert "WR1" not in output


def test_assert_with_unicode(pytester: Pytester) -> None:
    pytester.makepyfile(
        """\
        def test_unicode():
            assert '유니코드' == 'Unicode'
        """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines(["*AssertionError*"])


def test_raise_unprintable_assertion_error(pytester: Pytester) -> None:
    pytester.makepyfile(
        r"""
        def test_raise_assertion_error():
            raise AssertionError('\xff')
    """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines(
        [r">       raise AssertionError('\xff')", "E       AssertionError: *"]
    )


def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        class RaisingRepr(object):
            def __repr__(self):
                raise Exception()
        def test_raising_repr():
            raise AssertionError(RaisingRepr())
    """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines(["E       AssertionError: <exception str() failed>"])


def test_issue_1944(pytester: Pytester) -> None:
    pytester.makepyfile(
        """
        def f():
            return

        assert f() == 10
    """
    )
    result = pytester.runpytest()
    result.stdout.fnmatch_lines(["*1 error*"])
    assert (
        "AttributeError: 'Module' object has no attribute '_obj'"
        not in result.stdout.str()
    )


def test_exit_from_assertrepr_compare(monkeypatch) -> None:
    def raise_exit(obj):
        outcomes.exit("Quitting debugger")

    monkeypatch.setattr(util, "istext", raise_exit)

    with pytest.raises(outcomes.Exit, match="Quitting debugger"):
        callequal(1, 1)


def test_assertion_location_with_coverage(pytester: Pytester) -> None:
    """This used to report the wrong location when run with coverage (#5754)."""
    p = pytester.makepyfile(
        """
        def test():
            assert False, 1
            assert False, 2
        """
    )
    result = pytester.runpytest(str(p))
    result.stdout.fnmatch_lines(
        [
            ">       assert False, 1",
            "E       AssertionError: 1",
            "E       assert False",
            "*= 1 failed in*",
        ]
    )


def test_reprcompare_verbose_long() -> None:
    a = {f"v{i}": i for i in range(11)}
    b = a.copy()
    b["v2"] += 10
    lines = callop("==", a, b, verbose=2)
    assert lines is not None
    assert lines[0] == (
        "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4, 'v5': 5, "
        "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}"
        " == "
        "{'v0': 0, 'v1': 1, 'v2': 12, 'v3': 3, 'v4': 4, 'v5': 5, "
        "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}"
    )


@pytest.mark.parametrize("enable_colors", [True, False])
@pytest.mark.parametrize(
    ("test_code", "expected_lines"),
    (
        (
            """
            def test():
                assert [0, 1] == [0, 2]
            """,
            [
                "{bold}{red}E         At index 1 diff: {reset}{number}1{hl-reset}{endline} != {reset}{number}2*",
                "{bold}{red}E         {light-red}-     2,{hl-reset}{endline}{reset}",
                "{bold}{red}E         {light-green}+     1,{hl-reset}{endline}{reset}",
            ],
        ),
        (
            """
            def test():
                assert {f"number-is-{i}": i for i in range(1, 6)} == {
                    f"number-is-{i}": i for i in range(5)
                }
            """,
            [
                "{bold}{red}E         Common items:{reset}",
                "{bold}{red}E         {reset}{{{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*",
                "{bold}{red}E         Left contains 1 more item:{reset}",
                "{bold}{red}E         {reset}{{{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*",
                "{bold}{red}E         Right contains 1 more item:{reset}",
                "{bold}{red}E         {reset}{{{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*",
                "{bold}{red}E         {reset}{light-gray} {hl-reset} {{{endline}{reset}",
                "{bold}{red}E         {light-gray} {hl-reset}     'number-is-1': 1,{endline}{reset}",
                "{bold}{red}E         {light-green}+     'number-is-5': 5,{hl-reset}{endline}{reset}",
            ],
        ),
        (
            """
            def test():
                assert "abcd" == "abce"
            """,
            [
                "{bold}{red}E         {reset}{light-red}- abce{hl-reset}{endline}{reset}",
                "{bold}{red}E         {light-green}+ abcd{hl-reset}{endline}{reset}",
            ],
        ),
    ),
)
def test_comparisons_handle_colors(
    pytester: Pytester, color_mapping, enable_colors, test_code, expected_lines
) -> None:
    p = pytester.makepyfile(test_code)
    result = pytester.runpytest(
        f"--color={'yes' if enable_colors else 'no'}", "-vv", str(p)
    )
    formatter = (
        color_mapping.format_for_fnmatch
        if enable_colors
        else color_mapping.strip_colors
    )

    result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)


def test_fine_grained_assertion_verbosity(pytester: Pytester):
    long_text = "Lorem ipsum dolor sit amet " * 10
    p = pytester.makepyfile(
        f"""
        def test_ok():
            pass


        def test_words_fail():
            fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
            fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
            assert fruits1 == fruits2


        def test_numbers_fail():
            number_to_text1 = {{str(x): x for x in range(5)}}
            number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}}
            assert number_to_text1 == number_to_text2


        def test_long_text_fail():
            long_text = "{long_text}"
            assert "hello world" in long_text
        """
    )
    pytester.makeini(
        """
        [pytest]
        verbosity_assertions = 2
        """
    )
    result = pytester.runpytest(p)

    result.stdout.fnmatch_lines(
        [
            f"{p.name} .FFF                            [100%]",
            "E         At index 2 diff: 'grapes' != 'orange'",
            "E         Full diff:",
            "E           [",
            "E               'banana',",
            "E               'apple',",
            "E         -     'orange',",
            "E         ?      ^  ^^",
            "E         +     'grapes',",
            "E         ?      ^  ^ +",
            "E               'melon',",
            "E               'kiwi',",
            "E           ]",
            "E         Full diff:",
            "E           {",
            "E               '0': 0,",
            "E         -     '10': 10,",
            "E         ?       -    -",
            "E         +     '1': 1,",
            "E         -     '20': 20,",
            "E         ?       -    -",
            "E         +     '2': 2,",
            "E         -     '30': 30,",
            "E         ?       -    -",
            "E         +     '3': 3,",
            "E         -     '40': 40,",
            "E         ?       -    -",
            "E         +     '4': 4,",
            "E           }",
            f"E       AssertionError: assert 'hello world' in '{long_text}'",
        ]
    )


def test_full_output_vvv(pytester: Pytester) -> None:
    pytester.makepyfile(
        r"""
        def crash_helper(m):
            assert 1 == 2
        def test_vvv():
            crash_helper(500 * "a")
    """
    )
    result = pytester.runpytest("")
    # without -vvv, the passed args are truncated
    expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*"
    result.stdout.fnmatch_lines(
        [
            expected_non_vvv_arg_line,
            "test_full_output_vvv.py:2: AssertionError",
        ],
    )
    # double check that the untruncated part is not in the output
    expected_vvv_arg_line = "m = '{}'".format(500 * "a")
    result.stdout.no_fnmatch_line(expected_vvv_arg_line)

    # but with "-vvv" the args are not truncated
    result = pytester.runpytest("-vvv")
    result.stdout.fnmatch_lines(
        [
            expected_vvv_arg_line,
            "test_full_output_vvv.py:2: AssertionError",
        ]
    )
    result.stdout.no_fnmatch_line(expected_non_vvv_arg_line)
