# -*- coding: utf-8 -*-
from __future__ import print_function

import bdb
import inspect
import io
import os
import os.path
import re
import subprocess
import sys
import textwrap
import traceback
from io import BytesIO

import py
import pytest

import pdbpp
from pdbpp import DefaultConfig, Pdb, StringIO

try:
    from shlex import quote
except ImportError:
    from pipes import quote

try:
    from itertools import zip_longest
except ImportError:
    from itertools import izip_longest as zip_longest


# Windows support
# The basic idea is that paths on Windows are dumb because of backslashes.
# Typically this would be resolved by using `pathlib`, but we need to maintain
# support for pre-Py36 versions.
# A lot of tests are regex checks and the back-slashed Windows paths end
# up looking like they have escape characters in them (specifically the `\p`
# in `...\pdbpp`). So we need to make sure to escape those strings.
# In addtion, Windows is a case-insensitive file system. Most introspection
# tools return the `normcase` version (eg: all lowercase), so we adjust the
# canonical filename accordingly.
RE_THIS_FILE = re.escape(__file__)
THIS_FILE_CANONICAL = __file__
if sys.platform == 'win32':
    THIS_FILE_CANONICAL = __file__.lower()
RE_THIS_FILE_CANONICAL = re.escape(THIS_FILE_CANONICAL)
RE_THIS_FILE_CANONICAL_QUOTED = re.escape(quote(THIS_FILE_CANONICAL))
RE_THIS_FILE_QUOTED = re.escape(quote(__file__))


class FakeStdin:
    def __init__(self, lines):
        self.lines = iter(lines)

    def readline(self):
        try:
            line = next(self.lines) + '\n'
            sys.stdout.write(line)
            return line
        except StopIteration:
            return ''


class ConfigTest(DefaultConfig):
    highlight = False
    use_pygments = False
    prompt = '# '  # because + has a special meaning in the regexp
    editor = 'emacs'
    stdin_paste = 'epaste'
    disable_pytest_capturing = False
    current_line_color = 44


class ConfigWithHighlight(ConfigTest):
    highlight = True


class ConfigWithPygments(ConfigTest):
    use_pygments = True


class ConfigWithPygmentsNone(ConfigTest):
    use_pygments = None


class ConfigWithPygmentsAndHighlight(ConfigWithPygments, ConfigWithHighlight):
    pass


class PdbTest(pdbpp.Pdb):
    use_rawinput = 1

    def __init__(self, *args, **kwds):
        readrc = kwds.pop("readrc", False)
        nosigint = kwds.pop("nosigint", True)
        kwds.setdefault('Config', ConfigTest)
        if sys.version_info >= (3, 6):
            super(PdbTest, self).__init__(*args, readrc=readrc, **kwds)
        else:
            super(PdbTest, self).__init__(*args, **kwds)
        # Do not install sigint_handler in do_continue by default.
        self.nosigint = nosigint

    def _open_editor(self, editcmd):
        print("RUN %s" % editcmd)

    def _open_stdin_paste(self, cmd, lineno, filename, text):
        print("RUN %s +%d" % (cmd, lineno))
        print(repr(text))

    def do_shell(self, arg):
        """Track when do_shell gets called (via "!").

        This is not implemented by default, but we should not trigger it
        via parseline unnecessarily, which would cause unexpected results
        if somebody uses it.
        """
        print("do_shell_called: %r" % arg)
        return self.default(arg)


def set_trace_via_module(frame=None, cleanup=True, Pdb=PdbTest, **kwds):
    """set_trace helper that goes through pdb.set_trace.

    It injects Pdb into the globals of pdb.set_trace, to use the given frame.
    """
    if frame is None:
        frame = sys._getframe().f_back

    if cleanup:
        pdbpp.cleanup()

    class PdbForFrame(Pdb):
        def set_trace(self, _frame, *args, **kwargs):
            super(PdbForFrame, self).set_trace(frame, *args, **kwargs)

    newglobals = pdbpp.set_trace.__globals__.copy()
    newglobals['Pdb'] = PdbForFrame
    new_set_trace = pdbpp.rebind_globals(pdbpp.set_trace, newglobals)
    new_set_trace(**kwds)


def set_trace(frame=None, cleanup=True, Pdb=PdbTest, **kwds):
    """set_trace helper for tests, going through Pdb.set_trace directly."""
    if frame is None:
        frame = sys._getframe().f_back

    if cleanup:
        pdbpp.cleanup()

    Pdb(**kwds).set_trace(frame)


def xpm():
    pdbpp.xpm(PdbTest)


def runpdb(func, input, terminal_size=None):
    oldstdin = sys.stdin
    oldstdout = sys.stdout
    oldstderr = sys.stderr
    # Use __dict__ to avoid class descriptor (staticmethod).
    old_get_terminal_size = pdbpp.Pdb.__dict__["get_terminal_size"]

    if sys.version_info < (3, ):
        text_type = unicode  # noqa: F821
    else:
        text_type = str

    class MyBytesIO(BytesIO):
        """write accepts unicode or bytes"""

        encoding = 'ascii'

        def __init__(self, encoding='utf-8'):
            self.encoding = encoding

        def write(self, msg):
            if isinstance(msg, text_type):
                msg = msg.encode(self.encoding)
            super(MyBytesIO, self).write(msg)

        def get_unicode_value(self):
            return self.getvalue().decode(self.encoding).replace(
                pdbpp.CLEARSCREEN, "<CLEARSCREEN>\n"
            ).replace(chr(27), "^[")

    # Use a predictable terminal size.
    if terminal_size is None:
        terminal_size = (80, 24)
    pdbpp.Pdb.get_terminal_size = staticmethod(lambda: terminal_size)
    try:
        sys.stdin = FakeStdin(input)
        sys.stdout = stdout = MyBytesIO()
        sys.stderr = stderr = MyBytesIO()
        func()
    except InnerTestException:
        pass
    except bdb.BdbQuit:
        print("!! Received unexpected bdb.BdbQuit !!")
    except Exception:
        # Make it available for pytests output capturing.
        print(stdout.get_unicode_value(), file=oldstdout)
        raise
    finally:
        sys.stdin = oldstdin
        sys.stdout = oldstdout
        sys.stderr = oldstderr
        pdbpp.Pdb.get_terminal_size = old_get_terminal_size

    stderr = stderr.get_unicode_value()
    if stderr:
        # Make it available for pytests output capturing.
        print(stdout.get_unicode_value())
        raise AssertionError("Unexpected output on stderr: %s" % stderr)

    return stdout.get_unicode_value().splitlines()


def extract_commands(lines):
    cmds = []
    prompts = {'# ', '(#) ', '((#)) ', '(((#))) ', '(Pdb) ', '(Pdb++) '}
    for line in lines:
        for prompt in prompts:
            if line.startswith(prompt):
                cmds.append(line[len(prompt):])
                continue
    return cmds


shortcuts = [
    ('[', '\\['),
    (']', '\\]'),
    ('(', '\\('),
    (')', '\\)'),
    ('^', '\\^'),
    (r'\(Pdb++\) ', r'\(Pdb\+\+\) '),
    ('<COLORCURLINE>', r'\^\[\[44m\^\[\[36;01;44m *[0-9]+\^\[\[00;44m'),
    ('<COLORNUM>', r'\^\[\[36;01m *[0-9]+\^\[\[00m'),
    ('<COLORFNAME>', r'\^\[\[33;01m'),
    ('<COLORLNUM>', r'\^\[\[36;01m'),
    ('<COLORRESET>', r'\^\[\[00m'),
    ('<PYGMENTSRESET>', r'\^\[\[39[^m]*m'),
    ('NUM', ' *[0-9]+'),
    # Optional message with Python 2.7 (e.g. "--Return--"), not using Pdb.message.
    ('<PY27_MSG>', '\n.*' if sys.version_info < (3,) else ''),
]


def cook_regexp(s):
    for key, value in shortcuts:
        s = s.replace(key, value)
    return s


def run_func(func, expected, terminal_size=None):
    """Runs given function and returns its output along with expected patterns.

    It does not make any assertions. To compare func's output with expected
    lines, use `check` function.
    """
    expected = textwrap.dedent(expected).strip().splitlines()
    # Remove comments.
    expected = [re.split(r'\s+###', line)[0] for line in expected]
    commands = extract_commands(expected)
    expected = list(map(cook_regexp, expected))

    # Explode newlines from pattern replacements (PY27_MSG).
    flattened = []
    for line in expected:
        if line == "":
            flattened.append("")
        else:
            flattened.extend(line.splitlines())
    expected = flattened

    return expected, runpdb(func, commands, terminal_size)


def count_frames():
    f = sys._getframe()
    i = 0
    while f is not None:
        i += 1
        f = f.f_back
    return i


class InnerTestException(Exception):
    """Ignored by check()."""
    pass


trans_trn_dict = {"\n": r"\n", "\r": r"\r", "\t": r"\t"}
if sys.version_info >= (3,):
    trans_trn_table = str.maketrans(trans_trn_dict)

    def trans_trn(string):
        return string.translate(trans_trn_table)


else:

    def trans_trn(string):
        for k, v in trans_trn_dict.items():
            string = string.replace(k, v)
        return string


def check(func, expected, terminal_size=None):
    expected, lines = run_func(func, expected, terminal_size)
    if expected:
        maxlen = max(map(len, expected))
        if sys.version_info < (3,):
            # Ensure same type for comparison.
            expected = [
                x.decode("utf8") if isinstance(x, bytes) else x for x in expected
            ]
    else:
        maxlen = 0
    all_ok = True
    print()
    for pattern, string in zip_longest(expected, lines):
        if pattern is not None and string is not None:
            try:
                ok = re.match(pattern, string)
            except re.error as exc:
                raise ValueError("re.match failed for {!r}: {!r}".format(pattern, exc))
        else:
            ok = False
            if pattern is None:
                pattern = '<None>'
            if string is None:
                string = '<None>'
        # Use "$" to mark end of line with trailing space
        if re.search(r'\s+$', string):
            string += '$'
        if re.search(r'\s+$', pattern):
            pattern += '$'
        pattern = trans_trn(pattern)
        string = trans_trn(string)
        print(pattern.ljust(maxlen+1), '| ', string, end='')
        if ok:
            print()
        else:
            print(pdbpp.Color.set(pdbpp.Color.red, '    <<<<<'))
            all_ok = False
    assert all_ok


def test_prompt_setter():
    p = pdbpp.Pdb()
    assert p.prompt == "(Pdb++) "

    p.prompt = "(Pdb)"
    assert p.prompt == "(Pdb++)"
    p.prompt = "ipdb> "
    assert p.prompt == "ipdb++> "
    p.prompt = "custom"
    assert p.prompt == "custom++"
    p.prompt = "custom "
    assert p.prompt == "custom++ "
    p.prompt = "custom :"
    assert p.prompt == "custom++ :"
    p.prompt = "custom  "
    assert p.prompt == "custom++  "
    p.prompt = ""
    assert p.prompt == ""
    # Not changed (also used in tests).
    p.prompt = "# "
    assert p.prompt == "# "
    # Can be forced.
    p._prompt = "custom"
    assert p.prompt == "custom"


def test_config_pygments(monkeypatch):
    import pygments.formatters

    assert not hasattr(DefaultConfig, "use_terminal256formatter")

    p = Pdb(Config=DefaultConfig)

    monkeypatch.delenv("TERM", raising=False)
    assert isinstance(
        p._get_pygments_formatter(), pygments.formatters.TerminalFormatter
    )

    monkeypatch.setenv("TERM", "xterm-256color")
    assert isinstance(
        p._get_pygments_formatter(), pygments.formatters.Terminal256Formatter
    )

    monkeypatch.setenv("TERM", "xterm-kitty")
    assert isinstance(
        p._get_pygments_formatter(), pygments.formatters.TerminalTrueColorFormatter
    )

    class Config(DefaultConfig):
        formatter = object()

    assert Pdb(Config=Config)._get_pygments_formatter() is Config.formatter

    class Config(DefaultConfig):
        pygments_formatter_class = "pygments.formatters.TerminalTrueColorFormatter"

    assert isinstance(
        Pdb(Config=Config)._get_pygments_formatter(),
        pygments.formatters.TerminalTrueColorFormatter
    )

    source = Pdb(Config=Config).format_source("print(42)")
    assert source.startswith("\x1b[38;2;0;128;")
    assert "print\x1b[39" in source
    assert source.endswith("m42\x1b[39m)\n")


@pytest.mark.parametrize("use_pygments", (None, True, False))
def test_config_missing_pygments(use_pygments, monkeypatch_importerror):
    class Config(DefaultConfig):
        pass

    Config.use_pygments = use_pygments

    class PdbForMessage(Pdb):
        messages = []

        def message(self, msg):
            self.messages.append(msg)

    pdb_ = PdbForMessage(Config=Config)

    with monkeypatch_importerror(('pygments', 'pygments.formatters')):
        with pytest.raises(ImportError):
            pdb_._get_pygments_formatter()
        assert pdb_._get_source_highlight_function() is False
        assert pdb_.format_source("print(42)") == "print(42)"

    if use_pygments is True:
        assert pdb_.messages == ['Could not import pygments, disabling.']
    else:
        assert pdb_.messages == []

    # Cover branch for cached _highlight property.
    assert pdb_.format_source("print(42)") == "print(42)"


def test_config_pygments_deprecated_use_terminal256formatter(monkeypatch):
    import pygments.formatters

    monkeypatch.setenv("TERM", "xterm-256color")

    class Config(DefaultConfig):
        use_terminal256formatter = False
    assert isinstance(
        Pdb(Config=Config)._get_pygments_formatter(),
        pygments.formatters.TerminalFormatter
    )

    class Config(DefaultConfig):
        use_terminal256formatter = True
    assert isinstance(
        Pdb(Config=Config)._get_pygments_formatter(),
        pygments.formatters.Terminal256Formatter
    )


def test_runpdb():
    def fn():
        set_trace()
        a = 1
        b = 2
        c = 3
        return a+b+c

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# n
[NUM] > .*fn()
-> b = 2
   5 frames hidden .*
# n
[NUM] > .*fn()
-> c = 3
   5 frames hidden .*
# c
""")


def test_set_trace_remembers_previous_state():
    def fn():
        a = 1
        set_trace()
        a = 2
        set_trace(cleanup=False)
        a = 3
        set_trace(cleanup=False)
        a = 4
        return a

    check(fn, """
[NUM] > .*fn()
-> a = 2
   5 frames hidden .*
# display a
# c
[NUM] > .*fn()
-> a = 3
   5 frames hidden .*
a: 1 --> 2
# c
[NUM] > .*fn()
-> a = 4
   5 frames hidden .*
a: 2 --> 3
# c
""")


def test_set_trace_remembers_previous_state_via_module():
    def fn():
        a = 1
        set_trace_via_module()
        a = 2
        set_trace_via_module(cleanup=False)
        a = 3
        set_trace_via_module(cleanup=False)
        a = 4
        return a

    check(fn, """
[NUM] > .*fn()
-> a = 2
   5 frames hidden .*
# display a
# c
[NUM] > .*fn()
-> a = 3
   5 frames hidden .*
a: 1 --> 2
# c
[NUM] > .*fn()
-> a = 4
   5 frames hidden .*
a: 2 --> 3
# c
""")


class TestPdbMeta:
    def test_called_for_set_trace_false(self):
        assert pdbpp.PdbMeta.called_for_set_trace(sys._getframe()) is False

    def test_called_for_set_trace_staticmethod(self):

        class Foo:
            @staticmethod
            def set_trace():
                frame = sys._getframe()
                assert pdbpp.PdbMeta.called_for_set_trace(frame) is frame
                return True

        assert Foo.set_trace() is True

    def test_called_for_set_trace_method(self):

        class Foo:
            def set_trace(self):
                frame = sys._getframe()
                assert pdbpp.PdbMeta.called_for_set_trace(frame) is frame
                return True

        assert Foo().set_trace() is True

    def test_called_for_set_trace_via_func(self):
        def set_trace():
            frame = sys._getframe()
            assert pdbpp.PdbMeta.called_for_set_trace(frame) is frame
            return True

        assert set_trace() is True

    def test_called_for_set_trace_via_other_func(self):

        def somefunc():
            def meta():
                frame = sys._getframe()
                assert pdbpp.PdbMeta.called_for_set_trace(frame) is False
            meta()
            return True

        assert somefunc() is True


def test_forget_with_new_pdb():
    """Regression test for having used local.GLOBAL_PDB in forget.

    This caused "AttributeError: 'NewPdb' object has no attribute 'lineno'",
    e.g. when pdbpp was used before pytest's debugging plugin was setup, which
    then later uses a custom Pdb wrapper.
    """
    def fn():
        set_trace()

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                return super(NewPdb, self).set_trace(*args)

        new_pdb = NewPdb()
        new_pdb.set_trace()

    check(fn, """
[NUM] > .*fn()
-> class NewPdb(PdbTest, pdbpp.Pdb):
   5 frames hidden .*
# c
new_set_trace
--Return--
[NUM] .*set_trace()->None
-> return super(NewPdb, self).set_trace(\\*args)
   5 frames hidden .*
# l
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
NUM .*
# c
""")


def test_global_pdb_with_classmethod():
    def fn():
        set_trace()
        assert isinstance(pdbpp.local.GLOBAL_PDB, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                assert pdbpp.local.GLOBAL_PDB is self
                ret = super(NewPdb, self).set_trace(*args)
                assert pdbpp.local.GLOBAL_PDB is self
                return ret

        new_pdb = NewPdb()
        new_pdb.set_trace()

    check(fn, """
[NUM] > .*fn()
-> assert isinstance(pdbpp.local.GLOBAL_PDB, PdbTest)
   5 frames hidden .*
# c
new_set_trace
[NUM] .*set_trace()
-> assert pdbpp.local.GLOBAL_PDB is self
   5 frames hidden .*
# c
""")


def test_global_pdb_via_new_class_in_init_method():
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(pdbpp.local.GLOBAL_PDB, PdbTest)

        class PdbLikePytest(object):
            @classmethod
            def init_pdb(cls):

                class NewPdb(PdbTest, pdbpp.Pdb):
                    def set_trace(self, frame):
                        print("new_set_trace")
                        super(NewPdb, self).set_trace(frame)

                return NewPdb()

            @classmethod
            def set_trace(cls, *args, **kwargs):
                frame = sys._getframe().f_back
                pdb_ = cls.init_pdb(*args, **kwargs)
                return pdb_.set_trace(frame)

        PdbLikePytest.set_trace()
        second = pdbpp.local.GLOBAL_PDB
        assert first != second

        PdbLikePytest.set_trace()
        third = pdbpp.local.GLOBAL_PDB
        assert third == second

    check(fn, """
[NUM] > .*fn()
-> first = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] > .*fn()
-> second = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] > .*fn()
-> third = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
""")


def test_global_pdb_via_existing_class_in_init_method():
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(pdbpp.local.GLOBAL_PDB, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, frame):
                print("new_set_trace")
                super(NewPdb, self).set_trace(frame)

        class PdbViaClassmethod(object):
            @classmethod
            def init_pdb(cls):
                return NewPdb()

            @classmethod
            def set_trace(cls, *args, **kwargs):
                frame = sys._getframe().f_back
                pdb_ = cls.init_pdb(*args, **kwargs)
                return pdb_.set_trace(frame)

        PdbViaClassmethod.set_trace()
        second = pdbpp.local.GLOBAL_PDB
        assert first != second

        PdbViaClassmethod.set_trace()
        third = pdbpp.local.GLOBAL_PDB
        assert third == second

    check(fn, """
[NUM] > .*fn()
-> first = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] > .*fn()
-> second = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] > .*fn()
-> third = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
""")


def test_global_pdb_can_be_skipped():
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(first, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                assert pdbpp.local.GLOBAL_PDB is not self
                ret = super(NewPdb, self).set_trace(*args)
                assert pdbpp.local.GLOBAL_PDB is not self
                return ret

        new_pdb = NewPdb(use_global_pdb=False)
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

    check(fn, """
[NUM] > .*fn()
-> first = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] .*set_trace()
-> assert pdbpp.local.GLOBAL_PDB is not self
   5 frames hidden .*
# readline_ = pdbpp.local.GLOBAL_PDB.fancycompleter.config.readline
# assert readline_.get_completer() != pdbpp.local.GLOBAL_PDB.complete
# c
[NUM] > .*fn()
-> assert pdbpp.local.GLOBAL_PDB is not new_pdb
   5 frames hidden .*
# c
""")


def test_global_pdb_can_be_skipped_unit(monkeypatch_pdb_methods):
    """Same as test_global_pdb_can_be_skipped, but with mocked Pdb methods."""
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(first, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                assert pdbpp.local.GLOBAL_PDB is not self
                ret = super(NewPdb, self).set_trace(*args)
                assert pdbpp.local.GLOBAL_PDB is not self
                return ret

        new_pdb = NewPdb(use_global_pdb=False)
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

    check(fn, """
=== set_trace
new_set_trace
=== set_trace
=== set_trace
""")


def test_global_pdb_can_be_skipped_but_set():
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(first, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                assert pdbpp.local.GLOBAL_PDB is self
                ret = super(NewPdb, self).set_trace(*args)
                assert pdbpp.local.GLOBAL_PDB is self
                return ret

        new_pdb = NewPdb(use_global_pdb=False, set_global_pdb=True)
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is new_pdb

        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is new_pdb

    check(fn, """
[NUM] > .*fn()
-> first = pdbpp.local.GLOBAL_PDB
   5 frames hidden .*
# c
new_set_trace
[NUM] .*set_trace()
-> assert pdbpp.local.GLOBAL_PDB is self
   5 frames hidden .*
# readline_ = pdbpp.local.GLOBAL_PDB.fancycompleter.config.readline
# assert readline_.get_completer() == pdbpp.local.GLOBAL_PDB.complete
# c
new_set_trace
[NUM] > .*fn()
-> assert pdbpp.local.GLOBAL_PDB is new_pdb
   5 frames hidden .*
# c
""")


def test_global_pdb_can_be_skipped_but_set_unit(monkeypatch_pdb_methods):
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB
        assert isinstance(first, PdbTest)

        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                assert pdbpp.local.GLOBAL_PDB is self
                ret = super(NewPdb, self).set_trace(*args)
                assert pdbpp.local.GLOBAL_PDB is self
                return ret

        new_pdb = NewPdb(use_global_pdb=False, set_global_pdb=True)
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is new_pdb

        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is new_pdb

    check(fn, """
=== set_trace
new_set_trace
=== set_trace
new_set_trace
=== set_trace
""")


def test_global_pdb_only_reused_for_same_class(monkeypatch_pdb_methods):
    def fn():
        class NewPdb(PdbTest, pdbpp.Pdb):
            def set_trace(self, *args):
                print("new_set_trace")
                ret = super(NewPdb, self).set_trace(*args)
                return ret

        new_pdb = NewPdb()
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is new_pdb

        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

        # What "debug" does, for coverage.
        new_pdb = NewPdb()
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is new_pdb
        pdbpp.local.GLOBAL_PDB._use_global_pdb_for_class = PdbTest
        set_trace(cleanup=False)
        assert pdbpp.local.GLOBAL_PDB is new_pdb

        # Explicit kwarg for coverage.
        new_pdb = NewPdb(set_global_pdb=False)
        new_pdb.set_trace()
        assert pdbpp.local.GLOBAL_PDB is not new_pdb

    check(fn, """
new_set_trace
=== set_trace
=== set_trace
new_set_trace
=== set_trace
new_set_trace
=== set_trace
new_set_trace
=== set_trace
""")


def test_global_pdb_not_reused_with_different_home(
    monkeypatch_pdb_methods, monkeypatch
):
    def fn():
        set_trace()
        first = pdbpp.local.GLOBAL_PDB

        set_trace(cleanup=False)
        assert first is pdbpp.local.GLOBAL_PDB

        monkeypatch.setenv("HOME", "something else")
        set_trace(cleanup=False)
        assert first is not pdbpp.local.GLOBAL_PDB

    check(fn, """
=== set_trace
=== set_trace
=== set_trace
""")


def test_single_question_mark():
    def fn():
        def nodoc():
            pass

        def f2(x, y):
            """Return product of x and y"""
            return x * y
        set_trace()
        a = 1
        b = 2
        c = 3
        return a+b+c

    check(fn, r"""
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# f2
<function .*f2 at .*>
# f2?
.*Type:.*function
.*String Form:.*<function .*f2 at .*>
^[[31;01mFile:^[[00m           {filename}:{lnum_f2}
.*Definition:.*f2(x, y)
.*Docstring:.*Return product of x and y
# nodoc?
.*Type:.*function
.*String Form:.*<function .*nodoc at .*>
^[[31;01mFile:^[[00m           {filename}:{lnum_nodoc}
^[[31;01mDefinition:^[[00m     nodoc()
# doesnotexist?
\*\*\* NameError.*
# c
    """.format(
        filename=RE_THIS_FILE_CANONICAL,
        lnum_nodoc=fn.__code__.co_firstlineno + 1,
        lnum_f2=fn.__code__.co_firstlineno + 4,
    ))


def test_double_question_mark():
    """Test do_inspect_with_source."""
    def fn():
        def f2(x, y):
            """Return product of x and y"""
            return x * y

        set_trace()
        a = 1
        b = 2
        c = 3
        return a+b+c

    check(fn, r"""
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# f2
<function .*f2 at .*>
# f2??
.*Type:.*function
.*String Form:.*<function .*f2 at .*>
^[[31;01mFile:^[[00m           {filename}
.*Definition:.*f2(x, y)
.*Docstring:.*Return product of x and y
.*Source:.*
.* def f2(x, y):
.*     \"\"\"Return product of x and y\"\"\"
.*     return x \* y
# doesnotexist??
\*\*\* NameError.*
# c
    """.format(
        filename=RE_THIS_FILE_CANONICAL,
    ))


def test_question_mark_unit(capsys, LineMatcher):
    _pdb = PdbTest()
    _pdb.reset()

    foo = {12: 34}  # noqa: F841
    _pdb.setup(sys._getframe(), None)

    _pdb.do_inspect("foo")

    out, err = capsys.readouterr()
    LineMatcher(out.splitlines()).fnmatch_lines([
        "\x1b[31;01mString Form:\x1b[00m    {12: 34}"
    ])

    _pdb.do_inspect("doesnotexist")
    out, err = capsys.readouterr()
    LineMatcher(out.splitlines()).re_match_lines([
        r"^\*\*\* NameError:",
    ])

    # Source for function, indented docstring.
    def foo():
        """doc_for_foo

        3rd line."""
        raise NotImplementedError()
    _pdb.setup(sys._getframe(), None)
    _pdb.do_inspect_with_source("foo")
    out, err = capsys.readouterr()
    LineMatcher(out.splitlines()).re_match_lines([
        r"\x1b\[31;01mDocstring:\x1b\[00m      doc_for_foo",
        r"",
        r"                3rd line\.",
        r"\x1b\[31;01mSource:\x1b\[00m        ",
        r" ?\d+         def foo\(\):",
        r" ?\d+             raise NotImplementedError\(\)",
    ])

    # Missing source
    _pdb.do_inspect_with_source("str.strip")
    out, err = capsys.readouterr()
    LineMatcher(out.splitlines()).fnmatch_lines([
        "\x1b[31;01mSource:\x1b[00m         -",
    ])


def test_single_question_mark_with_existing_command(monkeypatch):
    def mocked_inspect(self, arg):
        print("mocked_inspect: %r" % arg)
    monkeypatch.setattr(PdbTest, "do_inspect", mocked_inspect)

    def fn():
        mp = monkeypatch  # noqa: F841

        class MyClass:
            pass
        a = MyClass()  # noqa: F841
        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# a?
mocked_inspect: 'a'
# a.__class__?
mocked_inspect: 'a.__class__'
# !!a?
# !a?
do_shell_called: a?
\\*\\*\\* SyntaxError:
# mp.delattr(pdbpp.local.GLOBAL_PDB.__class__, "do_shell")
# !a?
\\*\\*\\* SyntaxError:
# help a
a(rgs)
.*
# c
""")


def test_up_local_vars():
    def nested():
        set_trace()
        return

    def fn():
        xx = 42  # noqa: F841
        nested()

    check(fn, """
[NUM] > .*nested()
-> return
   5 frames hidden .*
# up
[NUM] > .*fn()
-> nested()
# xx
42
# c
""")


def test_frame():
    def a():
        b()

    def b():
        c()

    def c():
        set_trace()
        return

    check(a, """
[NUM] > .*c()
-> return
   5 frames hidden .*
# f {frame_num_a}
[NUM] > .*a()
-> b()
# f
[{frame_num_a}] > .*a()
-> b()
# f 0
[ 0] > .*()
-> .*
# f -1
[{stack_len}] > .*c()
-> return
# c
    """.format(
        frame_num_a=count_frames() + 2 - 5,
        stack_len=len(traceback.extract_stack())
    ))


@pytest.mark.skipif(sys.version_info < (3, 6),
                    reason="only with f-strings")
def test_fstrings(monkeypatch):
    def mocked_inspect(self, arg):
        print("mocked_inspect: %r" % arg)
    monkeypatch.setattr(PdbTest, "do_inspect", mocked_inspect)

    def f():
        set_trace()

    check(f, """
--Return--
[NUM] > .*
-> set_trace()
   5 frames hidden .*
# f"fstring"
'fstring'
# f"foo"?
mocked_inspect: 'f"foo"'
# c
""")


def test_prefixed_strings(monkeypatch):
    def mocked_inspect(self, arg):
        print("mocked_inspect: %r" % arg)

    monkeypatch.setattr(PdbTest, "do_inspect", mocked_inspect)

    def f():
        set_trace()

    check(
        f,
        """
--Return--
[NUM] > .*
-> set_trace()
   5 frames hidden .*
# b"string"
{bytestring!r}
# u"string"
{unicodestring!r}
# r"string"
'string'
# b"foo"?
mocked_inspect: 'b"foo"'
# r"foo"?
mocked_inspect: 'r"foo"'
# u"foo"?
mocked_inspect: 'u"foo"'
# c
""".format(bytestring=b"string", unicodestring=u"string"))


def test_up_down_arg():
    def a():
        b()

    def b():
        c()

    def c():
        set_trace()
        return

    check(a, """
[NUM] > .*c()
-> return
   5 frames hidden .*
# up 3
[NUM] > .*runpdb()
-> func()
# down 1
[NUM] > .*a()
-> b()
# c
""")


def test_up_down_sticky():
    def a():
        b()

    def b():
        set_trace()
        return

    check(a, """
[NUM] > .*b()
-> return
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > .*b(), 5 frames hidden

NUM         def b():
NUM             set_trace()
NUM  ->         return
# up
<CLEARSCREEN>
[NUM] > .*a(), 5 frames hidden

NUM         def a()
NUM  ->         b()
# c
""")


def test_top_bottom():
    def a():
        b()

    def b():
        c()

    def c():
        set_trace()
        return

    check(
        a, """
[NUM] > .*c()
-> return
   5 frames hidden .*
# top
[ 0] > .*()
-> .*
# bottom
[{stack_len}] > .*c()
-> return
# c
""".format(stack_len=len(traceback.extract_stack())))


def test_top_bottom_frame_post_mortem():
    def fn():
        def throws():
            0 / 0

        def f():
            throws()

        try:
            f()
        except:
            pdbpp.post_mortem(Pdb=PdbTest)
    check(fn, r"""
[2] > .*throws()
-> 0 / 0
# top
[0] > .*fn()
-> f()
# top
\*\*\* Oldest frame
# bottom
[2] > .*throws()
-> 0 / 0
# bottom
\*\*\* Newest frame
# frame -1  ### Same as bottom, no error.
[2] > .*throws()
-> 0 / 0
# frame -2
[1] > .*f()
-> throws()
# frame -3
\*\*\* Out of range
# c
""")


def test_parseline():
    def fn():
        c = 42
        set_trace()
        return c

    check(fn, """
[NUM] > .*fn()
-> return c
   5 frames hidden .*
# c
42
# !c
do_shell_called: 'c'
42
# r = 5
# r
5
# r = 6
# r
6
# !!c
""")


def test_parseline_with_rc_commands(tmpdir):
    """Test that parseline handles execution of rc lines during setup."""
    with tmpdir.as_cwd():
        with open(".pdbrc", "w") as f:
            f.writelines([
                "p 'readrc'\n",
                "alias myalias print(%1)\n",
            ])

        def fn():
            alias = "trigger"  # noqa: F841
            set_trace(readrc=True)

        check(fn, """
--Return--
'readrc'
[NUM] > .*fn()->None
-> set_trace(readrc=True)
   5 frames hidden .*
# alias myalias
myalias = print(%1)
# !!alias myalias
myalias = print(%1)
# myalias 42
42
# c
""")


def test_parseline_with_existing_command():
    def fn():
        c = 42
        debug = True  # noqa: F841

        class r:
            text = "r.text"

        set_trace()
        return c

    check(fn, """
[NUM] > .*fn()
-> return c
   5 frames hidden .*
# print(pdbpp.local.GLOBAL_PDB.parseline("foo = "))
('foo', '=', 'foo =')
# print(pdbpp.local.GLOBAL_PDB.parseline("c = "))
(None, None, 'c = ')
# print(pdbpp.local.GLOBAL_PDB.parseline("a = "))
(None, None, 'a = ')
# print(pdbpp.local.GLOBAL_PDB.parseline("list()"))
(None, None, 'list()')
# print(pdbpp.local.GLOBAL_PDB.parseline("next(my_iter)"))
(None, None, 'next(my_iter)')
# c
42
# debug print(1)
ENTERING RECURSIVE DEBUGGER
[1] > <string>(1)<module>()
(#) cont
1
LEAVING RECURSIVE DEBUGGER
# debug
True
# r.text
'r.text'
# cont
""")


def test_args_name():
    def fn():
        args = 42
        set_trace()
        return args

    check(fn, """
[NUM] > .*fn()
-> return args
   5 frames hidden .*
# args
42
# c
""")


def lineno():
    """Returns the current line number in our program."""
    return inspect.currentframe().f_back.f_lineno


def test_help():
    instance = PdbTest()
    instance.stdout = StringIO()

    help_params = [
        ("", r"Documented commands \(type help <topic>\):"),
        ("EOF", "Handles the receipt of EOF as a command."),
        ("a", "Print the argument"),
        ("alias", "an alias"),
        ("args", "Print the argument"),
        ("b", "set a break"),
        ("break", "set a break"),
        ("bt", "Print a stack trace"),
        ("c", "Continue execution, only stop when a breakpoint"),
        ("cl", "clear all breaks"),
        ("clear", "clear all breaks"),
        ("commands", "Specify a list of commands for breakpoint"),
        ("condition", "must evaluate to true"),
        ("cont", "Continue execution, only stop when a breakpoint"),
        ("continue", "Continue execution, only stop when a breakpoint"),
        ("d", "Move the current frame .* down"),
        ("debug", "Enter a recursive debugger"),
        ("disable", "Disables the breakpoints"),
        ("display", "Add expression to the display list"),
        ("down", "Move the current frame .* down"),
        ("ed", "Open an editor"),
        ("edit", "Open an editor"),
        ("enable", "Enables the breakpoints"),
        ("exit", "Quit from the debugger."),
        ("h", "h(elp)"),
        ("help", "h(elp)"),
        ("hf_hide", "hide hidden frames"),
        ("hf_unhide", "unhide hidden frames"),
        ("ignore", "ignore count for the given breakpoint"),
        ("interact", "Start an interative interpreter"),
        ("j", "Set the next line that will be executed."),
        ("jump", "Set the next line that will be executed."),
        ("l", "List source code for the current file."),
        ("list", "List source code for the current file."),
        ("ll", "List source code for the current function."),
        ("longlist", "List source code for the current function."),
        ("n", "Continue execution until the next line"),
        ("next", "Continue execution until the next line"),
        ("p", "Print the value of the expression"),
        ("pp", "Pretty-print the value of the expression."),
        ("q", "Quit from the debugger."),
        ("quit", "Quit from the debugger."),
        ("r", "Continue execution until the current function returns."),
        ("restart", "Restart the debugged python program."),
        ("return", "Continue execution until the current function returns."),
        ("run", "Restart the debugged python program"),
        ("s", "Execute the current line, stop at the first possible occasion"),
        ("step", "Execute the current line, stop at the first possible occasion"),
        ("sticky", "Toggle sticky mode"),
        ("tbreak", "arguments as break"),
        ("track", "track expression"),
        ("u", "Move the current frame .* up"),
        ("unalias", "specified alias."),
        ("undisplay", "Remove expression from the display list"),
        ("unt", "until the line"),
        ("until", "until the line"),
        ("up", "Move the current frame .* up"),
        ("w", "Print a stack trace"),
        ("whatis", "Prints? the type of the argument."),
        ("where", "Print a stack trace"),
        ("hidden_frames", 'Some frames might be marked as "hidden"'),
        ("exec", r"Execute the \(one-line\) statement"),

        ("hf_list", r"\*\*\* No help"),
        ("paste", r"\*\*\* No help"),
        ("put", r"\*\*\* No help"),
        ("retval", r"\*\*\* No help|return value"),
        ("rv", r"\*\*\* No help|return value"),
        ("source", r"\*\*\* No help"),
        ("unknown_command", r"\*\*\* No help"),
        ("help", "print the list of available commands."),
    ]

    # Redirect sys.stdout because Python 2 pdb.py has `print >>self.stdout` for
    # some functions and plain ol' `print` for others.
    oldstdout = sys.stdout
    sys.stdout = instance.stdout
    errors = []
    try:
        for command, expected_regex in help_params:
            instance.do_help(command)
            output = instance.stdout.getvalue()
            if not re.search(expected_regex, output):
                errors.append(command)
    finally:
        sys.stdout = oldstdout

    if errors:
        pytest.fail("unexpected help for: {}".format(", ".join(errors)))


def test_shortlist():
    def fn():
        a = 1
        set_trace(Config=ConfigTest)
        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# l {line_num}, 3
NUM +\t    def fn():
NUM +\t        a = 1
NUM +\t        set_trace(Config=ConfigTest)
NUM +->	        return a
# c
""".format(line_num=fn.__code__.co_firstlineno))


def test_shortlist_with_pygments_and_EOF():
    def fn():
        a = 1
        set_trace(Config=ConfigWithPygments)
        return a

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m a
   5 frames hidden .*
# l {line_num}, 3
[EOF]
# c
""".format(line_num=100000))


def test_shortlist_with_highlight_and_EOF():
    def fn():
        a = 1
        set_trace(Config=ConfigWithHighlight)
        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# l {line_num}, 3
[EOF]
# c
""".format(line_num=100000))


@pytest.mark.parametrize('config', [ConfigWithPygments, ConfigWithPygmentsNone])
def test_shortlist_with_pygments(config, monkeypatch):

    def fn():
        a = 1
        set_trace(Config=config)

        return a

    calls = []
    orig_get_func = pdbpp.Pdb._get_source_highlight_function

    def check_calls(self):
        orig_highlight = orig_get_func(self)
        calls.append(["get", self])

        def new_highlight(src):
            calls.append(["highlight", src])
            return orig_highlight(src)
        return new_highlight

    monkeypatch.setattr(pdbpp.Pdb, "_get_source_highlight_function", check_calls)

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m a
   5 frames hidden .*
# l {line_num}, 5
NUM +\t$
NUM +\t    ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
NUM +\t        a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
NUM +\t        set_trace(Config^[[38;5;241m=^[[39mconfig)
NUM +\t$
NUM +->\t        ^[[38;5;28;01mreturn^[[39;00m a
# c
""".format(line_num=fn.__code__.co_firstlineno - 1))
    assert len(calls) == 3, calls


def test_shortlist_with_partial_code():
    """Highlights the whole file, to handle partial docstring etc."""
    def fn():
        """
        1
        2
        3
        """
        a = 1
        set_trace(Config=ConfigWithPygments)
        return a

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m a
   5 frames hidden .*
# l
NUM \t^[[38;5;124.*m        2^[[39.*m
NUM \t^[[38;5;124.*m        3^[[39.*m
NUM \t^[[38;5;124.*m        \"\"\"^[[39.*m
NUM \t        a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
NUM \t        set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
NUM ->\t        ^[[38;5;28;01mreturn^[[39;00m a
NUM \t$
NUM \t    check(fn, ^[[38;5;124m\"\"\"^[[39m
NUM \t^[[38;5;124m.* > .*fn()^[[39m
NUM \t^[[38;5;124m-> ^[[38;5;28;01mreturn^[[39;00m a^[[39m
NUM \t^[[38;5;124m   5 frames hidden .*^[[39m
# c
""")


def test_truncated_source_with_pygments():

    def fn():
        """some docstring longer than maxlength for truncate_long_lines, which is 80"""
        a = 1
        set_trace(Config=ConfigWithPygments)

        return a

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m a
   5 frames hidden .*
# l {line_num}, 5
NUM +\t$
NUM +\t    ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
NUM +\t        ^[[38;5;124.*m\"\"\"some docstring longer than maxlength for truncate_long_lines, which is 80\"\"\"^[[39.*m
NUM +\t        a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
NUM +\t        set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
NUM +\t$
# sticky
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM +       ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
NUM +           ^[[38;5;124.*m\"\"\"some docstring longer than maxlength for truncate_long_lines^[[39.*m
NUM +           a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
NUM +           set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
NUM +$
NUM +->         ^[[38;5;28;01mreturn^[[39;00m a
# c
""".format(line_num=fn.__code__.co_firstlineno - 1))  # noqa: E501


def test_truncated_source_with_pygments_and_highlight():

    def fn():
        """some docstring longer than maxlength for truncate_long_lines, which is 80"""
        a = 1
        set_trace(Config=ConfigWithPygmentsAndHighlight)

        return a

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m a
   5 frames hidden .*
# l {line_num}, 5
<COLORNUM> +\t$
<COLORNUM> +\t    ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
<COLORNUM> +\t        ^[[38;5;124.*m\"\"\"some docstring longer than maxlength for truncate_long_lines, which is 80\"\"\"^[[39.*m
<COLORNUM> +\t        a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
<COLORNUM> +\t        set_trace(Config^[[38;5;241m=^[[39mConfigWithPygmentsAndHighlight)
<COLORNUM> +\t$
# sticky
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

<COLORNUM> +       ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
<COLORNUM> +           ^[[38;5;124.*m\"\"\"some docstring longer than maxlength for truncate_long_lines<PYGMENTSRESET>
<COLORNUM> +           a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
<COLORNUM> +           set_trace(Config^[[38;5;241m=^[[39mConfigWithPygmentsAndHighlight)
<COLORNUM> +$
<COLORCURLINE> +->         ^[[38;5;28;01;44mreturn<PYGMENTSRESET> a                                                       ^[[00m
# c
""".format(line_num=fn.__code__.co_firstlineno - 1))  # noqa: E501


def test_shortlist_with_highlight():

    def fn():
        a = 1
        set_trace(Config=ConfigWithHighlight)

        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# l {line_num}, 4
<COLORNUM> +\t    def fn():
<COLORNUM> +\t        a = 1
<COLORNUM> +\t        set_trace(Config=ConfigWithHighlight)
<COLORNUM> +\t$
<COLORNUM> +->\t        return a
# c
""".format(line_num=fn.__code__.co_firstlineno))


def test_shortlist_without_arg():
    """Ensure that forget was called for lineno."""
    def fn():
        a = 1
        set_trace(Config=ConfigTest)
        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# l
NUM \tdef test_shortlist_without_arg():
NUM \t    \"""Ensure that forget was called for lineno.\"""
.*
.*
.*
.*
.*
.*
.*
NUM \t-> return a
NUM \t   5 frames hidden .*
# c
""".format(line_num=fn.__code__.co_firstlineno))


def test_shortlist_heuristic():
    def fn():
        a = 1
        set_trace(Config=ConfigTest)
        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# list {line_num}, 3
NUM \t    def fn():
NUM \t        a = 1
NUM \t        set_trace(Config=ConfigTest)
NUM ->	        return a
# list(range(4))
[0, 1, 2, 3]
# c
""".format(line_num=fn.__code__.co_firstlineno))


def test_shortlist_with_second_set_trace_resets_lineno():
    def fn():
        def f1():
            set_trace(cleanup=False)

        set_trace()
        f1()

    check(fn, r"""
[NUM] > .*fn()
-> f1()
   5 frames hidden .*
# l {line_num}, 2
NUM \t    def fn():
NUM \t        def f1():
NUM \t            set_trace(cleanup=False)
# import pdb; pdbpp.local.GLOBAL_PDB.lineno
{set_lineno}
# c
--Return--
[NUM] > .*f1()->None
-> set_trace(cleanup=False)
   5 frames hidden .*
# import pdb; pdbpp.local.GLOBAL_PDB.lineno
# c
    """.format(
        line_num=fn.__code__.co_firstlineno,
        set_lineno=fn.__code__.co_firstlineno + 2,
    ))


def test_longlist():
    def fn():
        a = 1
        set_trace()
        return a

    check(fn, """
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# ll
NUM         def fn():
NUM             a = 1
NUM             set_trace()
NUM  ->         return a
# c
""")


def test_longlist_displays_whole_function():
    """`ll` displays the whole function (no cutoff)."""
    def fn():
        set_trace()
        a = 1
        a = 1
        a = 1
        a = 1
        a = 1
        a = 1
        a = 1
        a = 1
        a = 1
        return a

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden (try 'help hidden_frames')
# ll
NUM         def fn():
NUM             set_trace()
NUM  ->         a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             a = 1
NUM             return a
# c

""", terminal_size=(len(__file__) + 50, 10))


class TestListWithChangedSource:
    """Uses the cached (current) code."""

    @pytest.fixture(autouse=True)
    def setup_myfile(self, testdir):
        testdir.makepyfile(
            myfile="""
            from pdbpp import set_trace

            def rewrite_file():
                with open(__file__, "w") as f:
                    f.write("something completely different")

            def after_settrace():
                import linecache
                linecache.checkcache()

            def fn():
                set_trace()
                after_settrace()
                set_trace()
                a = 3
            """)
        testdir.monkeypatch.setenv("PDBPP_COLORS", "0")
        testdir.syspathinsert()

    def test_list_with_changed_source(self):
        from myfile import fn

        check(fn, r"""
    [NUM] > .*fn()
    -> after_settrace()
       5 frames hidden (try 'help hidden_frames')
    (Pdb++) l
    NUM  \t    import linecache$
    NUM  \t    linecache.checkcache()$
    NUM  \t$
    NUM  \tdef fn():
    NUM  \t    set_trace()
    NUM  ->\t    after_settrace()
    NUM  \t    set_trace()
    NUM  \t    a = 3
    [EOF]
    (Pdb++) rewrite_file()
    (Pdb++) c
    [NUM] > .*fn()
    -> a = 3
       5 frames hidden (try 'help hidden_frames')
    (Pdb++) l
    NUM  \t
    NUM  \tdef fn():
    NUM  \t    set_trace()
    NUM  \t    after_settrace()
    NUM  \t    set_trace()
    NUM  ->\t    a = 3
    [EOF]
    (Pdb++) c
    """)

    def test_longlist_with_changed_source(self):
        from myfile import fn

        check(fn, r"""
    [NUM] > .*fn()
    -> after_settrace()
       5 frames hidden (try 'help hidden_frames')
    (Pdb++) ll
    NUM     def fn():
    NUM         set_trace()
    NUM  ->     after_settrace()
    NUM         set_trace()
    NUM         a = 3
    (Pdb++) rewrite_file()
    (Pdb++) c
    [NUM] > .*fn()
    -> a = 3
       5 frames hidden (try 'help hidden_frames')
    (Pdb++) ll
    NUM     def fn():
    NUM         set_trace()
    NUM         after_settrace()
    NUM         set_trace()
    NUM  ->     a = 3
    (Pdb++) c
    """)


def test_longlist_with_highlight():
    def fn():
        a = 1
        set_trace(Config=ConfigWithHighlight)
        return a

    check(fn, r"""
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# ll
<COLORNUM>         def fn():
<COLORNUM>             a = 1
<COLORNUM>             set_trace(Config=ConfigWithHighlight)
<COLORCURLINE> +->         return a                                                       ^[[00m$
# c
""")  # noqa: E501


def test_display():
    def fn():
        a = 1
        set_trace()
        b = 1  # noqa: F841
        a = 2
        a = 3
        return a

    check(fn, """
[NUM] > .*fn()
-> b = 1
   5 frames hidden .*
# display a
# n
[NUM] > .*fn()
-> a = 2
   5 frames hidden .*
# n
[NUM] > .*fn()
-> a = 3
   5 frames hidden .*
a: 1 --> 2
# undisplay a
# n
[NUM] > .*fn()
-> return a
   5 frames hidden .*
# c
""")


def test_display_undefined():
    def fn():
        set_trace()
        b = 42
        return b

    check(fn, """
[NUM] > .*fn()
-> b = 42
   5 frames hidden .*
# display b
# n
[NUM] > .*fn()
-> return b
   5 frames hidden .*
b: <undefined> --> 42
# c
""")


def test_sticky():
    def fn():
        set_trace()
        a = 1
        b = 2  # noqa: F841
        c = 3  # noqa: F841
        return a

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace()
NUM  ->         a = 1
NUM             b = 2
NUM             c = 3
NUM             return a
# n
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace()
NUM             a = 1
NUM  ->         b = 2
NUM             c = 3
NUM             return a
# sticky
# n
[NUM] > .*fn()
-> c = 3
   5 frames hidden .*
# c
""")


def test_sticky_resets_cls():
    def fn():
        set_trace()
        a = 1
        print(a)
        set_trace(cleanup=False)
        return a

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace()
NUM  ->         a = 1
NUM             print(a)
NUM             set_trace(cleanup=False)
NUM             return a
# c
1
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace()
NUM             a = 1
NUM             print(a)
NUM             set_trace(cleanup=False)
NUM  ->         return a
# n
<CLEARSCREEN><PY27_MSG>
[NUM] > .*fn()->1, 5 frames hidden

NUM         def fn():
NUM             set_trace()
NUM             a = 1
NUM             print(a)
NUM             set_trace(cleanup=False)
NUM  ->         return a
 return 1
# c
""")


def test_sticky_with_same_frame():
    def fn():
        def inner(cleanup):
            set_trace(cleanup=cleanup)
            print(cleanup)

        for cleanup in (True, False):
            inner(cleanup)

    check(fn, """
[NUM] > .*inner()
-> print(cleanup)
   5 frames hidden (try 'help hidden_frames')
# sticky
<CLEARSCREEN>
[NUM] > .*inner(), 5 frames hidden

NUM             def inner(cleanup):
NUM                 set_trace(cleanup=cleanup)
NUM  ->             print(cleanup)
# n
<CLEARSCREEN>
True<PY27_MSG>
[NUM] > .*inner()->None, 5 frames hidden

NUM             def inner(cleanup):
NUM                 set_trace(cleanup=cleanup)
NUM  ->             print(cleanup)
 return None
# c
[NUM] > .*inner(), 5 frames hidden

NUM             def inner(cleanup):
NUM                 set_trace(cleanup=cleanup)
NUM  ->             print(cleanup)
# n
<CLEARSCREEN>
False<PY27_MSG>
[NUM] > .*inner()->None, 5 frames hidden

NUM             def inner(cleanup):
NUM                 set_trace(cleanup=cleanup)
NUM  ->             print(cleanup)
 return None
# c
""")


def test_sticky_range():
    def fn():
        set_trace()
        a = 1
        b = 2  # noqa: F841
        c = 3  # noqa: F841
        return a
    _, lineno = inspect.getsourcelines(fn)
    start = lineno + 1
    end = lineno + 3

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# sticky %d %d
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

%d \\s+         set_trace()
NUM  ->         a = 1
NUM             b = 2
# c
""" % (start, end, start))


def test_sticky_by_default():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def fn():
        set_trace(Config=MyConfig)
        a = 1
        b = 2  # noqa: F841
        c = 3  # noqa: F841
        return a

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace(Config=MyConfig)
NUM  ->         a = 1
NUM             b = 2
NUM             c = 3
NUM             return a
# n
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace(Config=MyConfig)
NUM             a = 1
NUM  ->         b = 2
NUM             c = 3
NUM             return a
# c
""")


def test_sticky_by_default_with_use_pygments_auto():
    class MyConfig(ConfigTest):
        sticky_by_default = True
        use_pygments = None

    def fn():
        set_trace(Config=MyConfig)
        a = 1
        return a

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
NUM             set_trace(Config^[[38;5;241m=^[[39mMyConfig)
NUM  ->         a ^[[38;5;241m=^[[39m ^[[38;5;241m1^[[39m
NUM             ^[[38;5;28;01mreturn^[[39;00m a
# c
""")


def test_sticky_dunder_exception():
    """Test __exception__ being displayed in sticky mode."""

    def fn():
        def raises():
            raise InnerTestException()

        set_trace()
        raises()

    check(fn, """
[NUM] > .*fn()
-> raises()
   5 frames hidden (try 'help hidden_frames')
# n
.*InnerTestException.*  ### via pdbpp.Pdb.user_exception (differs on py3/py27)
[NUM] > .*fn()
-> raises()
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > {filename}(NUM)fn(), 5 frames hidden

NUM         def fn():
NUM             def raises():
NUM                 raise InnerTestException()
NUM
NUM             set_trace(.*)
NUM  ->         raises()
InnerTestException:
# c
    """.format(
        filename=RE_THIS_FILE_CANONICAL,
    ))


def test_sticky_dunder_exception_with_highlight():
    """Test __exception__ being displayed in sticky mode."""

    def fn():
        def raises():
            raise InnerTestException()

        set_trace(Config=ConfigWithHighlight)
        raises()

    check(fn, """
[NUM] > .*fn()
-> raises()
   5 frames hidden (try 'help hidden_frames')
# n
.*InnerTestException.*  ### via pdbpp.Pdb.user_exception (differs on py3/py27)
[NUM] > .*fn()
-> raises()
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > <COLORFNAME>{filename}<COLORRESET>(<COLORNUM>)fn(), 5 frames hidden

<COLORNUM>         def fn():
<COLORNUM>             def raises():
<COLORNUM>                 raise InnerTestException()
<COLORNUM>
<COLORNUM>             set_trace(.*)
<COLORCURLINE>  ->         raises().*
<COLORLNUM>InnerTestException: <COLORRESET>
# c
    """.format(
        filename=RE_THIS_FILE_CANONICAL,
    ))


def test_format_exc_for_sticky():
    _pdb = PdbTest()
    f = _pdb._format_exc_for_sticky

    assert f((Exception, Exception())) == "Exception: "

    exc_from_str = Exception("exc_from_str")

    class UnprintableExc:
        def __str__(self):
            raise exc_from_str

    assert f((UnprintableExc, UnprintableExc())) == (
        "UnprintableExc: (unprintable exception: %r)" % exc_from_str
    )

    class UnprintableExc:
        def __str__(self):
            class RaisesInRepr(Exception):
                def __repr__(self):
                    raise Exception()
            raise RaisesInRepr()

    assert f((UnprintableExc, UnprintableExc())) == (
        "UnprintableExc: (unprintable exception)"
    )

    assert f((1, 3, 3)) == 'pdbpp: got unexpected __exception__: (1, 3, 3)'


def test_sticky_dunder_return():
    """Test __return__ being displayed in sticky mode."""

    def fn():
        def returns():
            return 40 + 2

        set_trace()
        returns()

    check(fn, """
[NUM] > .*fn()
-> returns()
   5 frames hidden (try 'help hidden_frames')
# s
--Call--
[NUM] > .*returns()
-> def returns()
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > .*(NUM)returns(), 5 frames hidden

NUM  ->         def returns():
NUM                 return 40 \\+ 2
# retval
\\*\\*\\* Not yet returned!
# r
<CLEARSCREEN><PY27_MSG>
[NUM] > {filename}(NUM)returns()->42, 5 frames hidden

NUM             def returns():
NUM  ->             return 40 \\+ 2
 return 42
# retval
42
# c
    """.format(
        filename=RE_THIS_FILE_CANONICAL,
    ))


def test_sticky_with_user_exception():
    def fn():
        def throws():
            raise InnerTestException()

        set_trace()
        throws()

    check(fn, """
[NUM] > .*fn()
-> throws()
   5 frames hidden (try 'help hidden_frames')
# s
--Call--
[NUM] > .*throws()
-> def throws():
   5 frames hidden .*
# sticky
<CLEARSCREEN>
[NUM] > .*throws(), 5 frames hidden

NUM  ->         def throws():
NUM                 raise InnerTestException()
# n
<CLEARSCREEN>
[NUM] > .*throws(), 5 frames hidden

NUM             def throws():
NUM  ->             raise InnerTestException()
# n
<CLEARSCREEN><PY27_MSG>
[NUM] > .*throws(), 5 frames hidden

NUM             def throws():
NUM  ->             raise InnerTestException()
InnerTestException:
# c
""")


def test_sticky_last_value():
    """sys.last_value is displayed in sticky mode."""
    def outer():
        try:
            raise ValueError("very long excmsg\n" * 10)
        except ValueError:
            sys.last_value, sys.last_traceback = sys.exc_info()[1:]

    def fn():
        outer()
        set_trace()

        __exception__ = "foo"  # noqa: F841
        set_trace(cleanup=False)

    expected = r"""
[NUM] > .*fn()
-> __exception__ = "foo"
   5 frames hidden (try 'help hidden_frames')
# sticky
<CLEARSCREEN>
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             outer()
NUM             set_trace()
NUM
NUM  ->         __exception__ = "foo"  # noqa: F841
NUM             set_trace(cleanup=False)
ValueError: very long excmsg\\nvery long excmsg\\nvery long e…
# c
<PY27_MSG>[NUM] > .*fn()->None, 5 frames hidden

NUM         def fn():
NUM             outer()
NUM             set_trace()
NUM
NUM             __exception__ = "foo"  # noqa: F841
NUM  ->         set_trace(cleanup=False)
pdbpp: got unexpected __exception__: 'foo'
# c
"""
    saved = sys.exc_info()[1:]
    try:
        check(fn, expected, terminal_size=(60, 20))
    finally:
        sys.last_value, sys.last_traceback = saved


def test_sticky_dunder_return_with_highlight():
    class Config(ConfigWithHighlight, ConfigWithPygments):
        pass

    def fn():
        def returns():
            return 40 + 2

        set_trace(Config=Config)
        returns()

    expected, lines = run_func(fn, '# s\n# sticky\n# r\n# retval\n# c')
    assert lines[-4:] == [
        '^[[36;01m return 42^[[00m',
        '# retval',
        '42',
        '# c',
    ]

    colored_cur_lines = [
        x for x in lines
        if x.startswith('^[[44m^[[36;01;44m') and '->' in x
    ]
    assert len(colored_cur_lines) == 2


def test_sticky_cutoff_with_tail():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def fn():
        set_trace(Config=MyConfig)
        print(1)
        # 1
        # 2
        # 3
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         def fn():
NUM             set_trace(Config=MyConfig)
NUM  ->         print(1)
NUM             # 1
NUM             # 2
...
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_head():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def fn():
        # 1
        # 2
        # 3
        # 4
        # 5
        set_trace(Config=MyConfig)
        print(1)
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

...
NUM             # 4
NUM             # 5
NUM             set_trace(Config=MyConfig)
NUM  ->         print(1)
NUM             return
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_head_and_tail():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def fn():
        # 1
        # 2
        # 3
        set_trace(Config=MyConfig)
        print(1)
        # 1
        # 2
        # 3
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

...
NUM             set_trace(Config=MyConfig)
NUM  ->         print(1)
NUM             # 1
NUM             # 2
...
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_long_head_and_tail():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def fn():
        # 1
        # 2
        # 3
        # 4
        # 5
        # 6
        # 7
        # 8
        # 9
        # 10
        set_trace(Config=MyConfig)
        print(1)
        # 1
        # 2
        # 3
        # 4
        # 5
        # 6
        # 7
        # 8
        # 9
        # 10
        # 11
        # 12
        # 13
        # 14
        # 15
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

...
NUM             # 8
NUM             # 9
NUM             # 10
NUM             set_trace(Config=MyConfig)
NUM  ->         print(1)
NUM             # 1
NUM             # 2
NUM             # 3
NUM             # 4
...
# c
1
""", terminal_size=(len(__file__) + 50, 15))


def test_sticky_cutoff_with_decorator():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def deco(f):
        return f

    @deco
    def fn():
        # 1
        # 2
        # 3
        # 4
        # 5
        set_trace(Config=MyConfig)
        print(1)
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         @deco
...
NUM             # 5
NUM             set_trace(Config=MyConfig)
NUM  ->         print(1)
NUM             return
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_many_decorators():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def deco(f):
        return f

    @deco
    @deco
    @deco
    @deco
    @deco
    @deco
    @deco
    @deco
    def fn():
        # 1
        # 2
        # 3
        # 4
        # 5
        set_trace(Config=MyConfig)
        print(1)
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         @deco
...
NUM         @deco
...
NUM  ->         print(1)
NUM             return
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_decorator_colored():
    class MyConfig(ConfigWithPygmentsAndHighlight):
        sticky_by_default = True

    def deco(f):
        return f

    @deco
    @deco
    def fn():
        # 1
        # 2
        # 3
        # 4
        # 5
        set_trace(Config=MyConfig)
        print(1)
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

<COLORNUM>         ^[[38;5;129m@deco^[[39m
<COLORNUM>         ^[[38;5;129m@deco^[[39m
...
<COLORNUM>             set_trace.*
<COLORCURLINE>  ->         ^[[38;5;28.*;44mprint.*
<COLORNUM>             ^[[38;5;28;01mreturn^[[39;00m
# c
1
""", terminal_size=(len(__file__) + 50, 10))


def test_sticky_cutoff_with_minimal_lines():
    class MyConfig(ConfigTest):
        sticky_by_default = True

    def deco(f):
        return f

    @deco
    def fn():
        set_trace(Config=MyConfig)
        print(1)
        # 1
        # 2
        # 3
        return

    check(fn, """
[NUM] > .*fn(), 5 frames hidden

NUM         @deco
...
NUM  ->         print(1)
NUM             # 1
NUM             # 2
...
# c
1
""", terminal_size=(len(__file__) + 50, 3))


def test_exception_lineno():
    def bar():
        assert False

    def fn():
        try:
            a = 1  # noqa: F841
            bar()
            b = 2  # noqa: F841
        except AssertionError:
            xpm()

    check(fn, """
Traceback (most recent call last):
  File "{filename}", line NUM, in fn
    bar()
  File "{filename}", line NUM, in bar
    assert False
AssertionError.*

[NUM] > .*bar()
-> assert False
# u
[NUM] > .*fn()
-> bar()
# ll
NUM         def fn():
NUM             try:
NUM                 a = 1
NUM  >>             bar()
NUM                 b = 2
NUM             except AssertionError:
NUM  ->             xpm()
# c
    """.format(
        filename=RE_THIS_FILE,
    ))


def test_postmortem_noargs():

    def fn():
        try:
            a = 1  # noqa: F841
            1/0
        except ZeroDivisionError:
            pdbpp.post_mortem(Pdb=PdbTest)

    check(fn, """
[NUM] > .*fn()
-> 1/0
# c
""")


def test_postmortem_needs_exceptioncontext():
    pytest.raises(ValueError, pdbpp.post_mortem)


def test_exception_through_generator():
    def gen():
        yield 5
        assert False

    def fn():
        try:
            for i in gen():
                pass
        except AssertionError:
            xpm()

    check(fn, """
Traceback (most recent call last):
  File "{filename}", line NUM, in fn
    for i in gen():
  File "{filename}", line NUM, in gen
    assert False
AssertionError.*

[NUM] > .*gen()
-> assert False
# u
[NUM] > .*fn()
-> for i in gen():
# c
    """.format(
        filename=RE_THIS_FILE,
    ))


def test_py_code_source():  # noqa: F821
    src = py.code.Source("""
    def fn():
        x = 42
        set_trace()
        return x
    """)

    exec(src.compile(), globals())
    check(fn,  # noqa: F821
          """
[NUM] > .*fn()
-> return x
   5 frames hidden .*
# ll
NUM     def fn():
NUM         x = 42
NUM         set_trace()
NUM  ->     return x
# c
""")


def test_source():
    def bar():
        return 42

    def fn():
        set_trace()
        return bar()

    check(fn, """
[NUM] > .*fn()
-> return bar()
   5 frames hidden .*
# source bar
NUM         def bar():
NUM             return 42
# c
""")


def test_source_with_pygments():
    def bar():

        return 42

    def fn():
        set_trace(Config=ConfigWithPygments)
        return bar()

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m bar()
   5 frames hidden .*
# source bar
NUM         ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mbar^[[39m():
NUM
NUM             ^[[38;5;28;01mreturn^[[39;00m ^[[38;5;241m42^[[39m
# c
""")


def test_source_with_highlight():
    def bar():

        return 42

    def fn():
        set_trace(Config=ConfigWithHighlight)
        return bar()

    check(fn, """
[NUM] > .*fn()
-> return bar()
   5 frames hidden .*
# source bar
<COLORNUM>         def bar():
<COLORNUM>
<COLORNUM>             return 42
# c
""")


def test_source_with_pygments_and_highlight():
    def bar():

        return 42

    def fn():
        set_trace(Config=ConfigWithPygmentsAndHighlight)
        return bar()

    check(fn, """
[NUM] > .*fn()
-> ^[[38;5;28;01mreturn^[[39;00m bar()
   5 frames hidden .*
# source bar
<COLORNUM>         ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mbar^[[39m():
<COLORNUM>
<COLORNUM>             ^[[38;5;28;01mreturn^[[39;00m ^[[38;5;241m42^[[39m
# c
""")


def test_bad_source():
    def fn():
        set_trace()
        return 42

    check(fn, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# source 42
\*\* Error: .*module, class, method, function, traceback, frame, or code object .*\*\*
# c
""")  # noqa: E501


def test_edit():
    def fn():
        set_trace()
        return 42

    def bar():
        fn()
        return 100

    _, lineno = inspect.getsourcelines(fn)
    return42_lineno = lineno + 2
    call_fn_lineno = lineno + 5

    check(fn, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# edit
RUN emacs \+%d %s
# c
""" % (return42_lineno, RE_THIS_FILE_QUOTED))

    check(bar, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# up
[NUM] > .*bar()
-> fn()
# edit
RUN emacs \+%d %s
# c
""" % (call_fn_lineno, RE_THIS_FILE_QUOTED))


def test_edit_obj():
    def fn():
        bar()
        set_trace()
        return 42

    def bar():
        pass
    _, bar_lineno = inspect.getsourcelines(bar)

    check(fn, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# edit bar
RUN emacs \+%d %s
# c
""" % (bar_lineno, RE_THIS_FILE_CANONICAL_QUOTED))


def test_edit_py_code_source():
    src = py.code.Source("""
    def bar():
        set_trace()
        return 42
    """)
    _, base_lineno = inspect.getsourcelines(test_edit_py_code_source)
    dic = {'set_trace': set_trace}
    exec(src.compile(), dic)  # 8th line from the beginning of the function
    bar = dic['bar']
    src_compile_lineno = base_lineno + 8

    check(bar, r"""
[NUM] > .*bar()
-> return 42
   5 frames hidden .*
# edit bar
RUN emacs \+%d %s
# c
""" % (src_compile_lineno, RE_THIS_FILE_CANONICAL_QUOTED))


def test_put():
    def fn():
        set_trace()
        return 42
    _, lineno = inspect.getsourcelines(fn)
    start_lineno = lineno + 1

    check(fn, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# x = 10
# y = 12
# put
RUN epaste \+%d
'        x = 10\\n        y = 12\\n'
# c
""" % start_lineno)


def test_paste():
    def g():
        print('hello world')

    def fn():
        set_trace()
        if 4 != 5:
            g()
        return 42
    _, lineno = inspect.getsourcelines(fn)
    start_lineno = lineno + 1

    check(fn, r"""
[NUM] > .*fn()
-> if 4 != 5:
   5 frames hidden .*
# g()
hello world
# paste g()
hello world
RUN epaste \+%d
'hello world\\n'
# c
hello world
""" % start_lineno)


def test_put_if():
    def fn():
        x = 0
        if x < 10:
            set_trace()
        return x
    _, lineno = inspect.getsourcelines(fn)
    start_lineno = lineno + 3

    check(fn, r"""
[NUM] > .*fn()
-> return x
   5 frames hidden .*
# x = 10
# y = 12
# put
RUN epaste \+%d
.*x = 10\\n            y = 12\\n.
# c
""" % start_lineno)


def test_side_effects_free():
    r = pdbpp.side_effects_free
    assert r.match('  x')
    assert r.match('x.y[12]')
    assert not r.match('x(10)')
    assert not r.match('  x = 10')
    assert not r.match('x = 10')


def test_put_side_effects_free():
    def fn():
        x = 10  # noqa: F841
        set_trace()
        return 42
    _, lineno = inspect.getsourcelines(fn)
    start_lineno = lineno + 2

    check(fn, r"""
[NUM] > .*fn()
-> return 42
   5 frames hidden .*
# x
10
# x.__add__
.*
# y = 12
# put
RUN epaste \+%d
'        y = 12\\n'
# c
""" % start_lineno)


def test_enable_disable_via_module():
    def fn():
        x = 1
        pdbpp.disable()
        set_trace_via_module()
        x = 2
        pdbpp.enable()
        set_trace_via_module()
        return x

    check(fn, """
[NUM] > .*fn()
-> return x
   5 frames hidden .*
# x
2
# c
""")


def test_enable_disable_from_prompt_via_class():
    def fn():
        pdb_ = PdbTest()

        pdb_.set_trace()
        x = 1
        pdb_.set_trace()
        x = 2
        pdbpp.enable()
        pdb_.set_trace()
        return x

    check(fn, """
[NUM] > .*fn()
-> x = 1
   5 frames hidden .*
# pdbpp.disable()
# c
[NUM] > .*fn()
-> return x
   5 frames hidden .*
# x
2
# c
""")


def test_hideframe():
    @pdbpp.hideframe
    def g():
        pass
    assert g.__code__.co_consts[-1] is pdbpp._HIDE_FRAME


def test_hide_hidden_frames():
    @pdbpp.hideframe
    def g():
        set_trace()
        return 'foo'

    def fn():
        g()
        return 1

    check(fn, """
[NUM] > .*fn()
-> g()
   6 frames hidden .*
# down
... Newest frame
# hf_unhide
# down
[NUM] > .*g()
-> return 'foo'
# up
[NUM] > .*fn()
-> g()
# hf_hide        ### hide the frame again
# down
... Newest frame
# c
""")


def test_hide_current_frame():
    @pdbpp.hideframe
    def g():
        set_trace()
        return 'foo'

    def fn():
        g()
        return 1

    check(fn, """
[NUM] > .*fn()
-> g()
   6 frames hidden .*
# hf_unhide
# down           ### now the frame is no longer hidden
[NUM] > .*g()
-> return 'foo'
# hf_hide        ### hide the current frame, go to the top of the stack
[NUM] > .*fn()
-> g()
# c
""")


def test_hide_frame_for_set_trace_on_class():
    def g():
        # Simulate set_trace, with frame=None.
        pdbpp.cleanup()
        _pdb = PdbTest()
        _pdb.set_trace()
        return 'foo'

    def fn():
        g()
        return 1

    check(fn, """
[NUM] > .*g()
-> return 'foo'
   5 frames hidden .*
# hf_unhide
# down
\\*\\*\\* Newest frame
# c
""")


def test_list_hidden_frames():
    @pdbpp.hideframe
    def g():
        set_trace()
        return 'foo'

    @pdbpp.hideframe
    def k():
        return g()

    def fn():
        k()
        return 1
    check(fn, r"""
[NUM] > .*fn()
-> k()
   7 frames hidden .*
# hf_list
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*k()
-> return g()
.*g()
-> return 'foo'
# c
""")


def test_hidden_pytest_frames():
    def s():
        __tracebackhide__ = True  # Ignored for set_trace in here.
        set_trace()
        return 'foo'

    def g(s=s):
        __tracebackhide__ = True
        return s()

    def k(g=g):
        return g()
    k = pdbpp.rebind_globals(k, {'__tracebackhide__': True})

    def fn():
        k()
        return 1

    check(fn, r"""
[NUM] > .*s()
-> return 'foo'
   7 frames hidden .*
# hf_list
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*k()
-> return g()
.*g()
-> return s()
# c
    """)


def test_hidden_unittest_frames():

    def s(set_trace=set_trace):
        set_trace()
        return 'foo'

    def g(s=s):
        return s()
    g = pdbpp.rebind_globals(g, {'__unittest': True})

    def fn():
        return g()

    check(fn, r"""
[NUM] > .*s()
-> return 'foo'
   6 frames hidden .*
# hf_list
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*_multicall()
-> res = hook_impl.function(\*args)
.*g()
-> return s()
# c
    """)


def test_dont_show_hidden_frames_count():
    class MyConfig(ConfigTest):
        show_hidden_frames_count = False

    @pdbpp.hideframe
    def g():
        set_trace(Config=MyConfig)
        return 'foo'

    def fn():
        g()
        return 1

    check(fn, """
[NUM] > .*fn()
-> g()
# c           ### note that the hidden frame count is not displayed
""")


def test_disable_hidden_frames():
    class MyConfig(ConfigTest):
        enable_hidden_frames = False

    @pdbpp.hideframe
    def g():
        set_trace(Config=MyConfig)
        return 'foo'

    def fn():
        g()
        return 1

    check(fn, """
[NUM] > .*g()
-> return 'foo'
# c           ### note that we were inside g()
""")


def test_break_on_setattr():
    # we don't use a class decorator to keep 2.5 compatibility
    class Foo(object):
        pass
    Foo = pdbpp.break_on_setattr('x', Pdb=PdbTest)(Foo)

    def fn():
        obj = Foo()
        obj.x = 0
        return obj.x

    check(fn, """
[NUM] > .*fn()
-> obj.x = 0
   5 frames hidden .*
# hasattr(obj, 'x')
False
# n
[NUM] > .*fn()
-> return obj.x
   5 frames hidden .*
# p obj.x
0
# c
""")


def test_break_on_setattr_without_hidden_frames():

    class PdbWithConfig(PdbTest):
        def __init__(self, *args, **kwargs):
            class Config(ConfigTest):
                enable_hidden_frames = False

            super(PdbWithConfig, self).__init__(*args, Config=Config, **kwargs)

    class Foo(object):
        pass
    Foo = pdbpp.break_on_setattr('x', Pdb=PdbWithConfig)(Foo)

    def fn():
        obj = Foo()
        obj.x = 0
        return obj.x

    check(fn, """
[NUM] > .*fn()
-> obj.x = 0
# hasattr(obj, 'x')
False
# n
[NUM] > .*fn()
-> return obj.x
# p obj.x
0
# c
""")


def test_break_on_setattr_condition():
    def mycond(obj, value):
        return value == 42

    class Foo(object):
        pass
    # we don't use a class decorator to keep 2.5 compatibility
    Foo = pdbpp.break_on_setattr('x', condition=mycond, Pdb=PdbTest)(Foo)

    def fn():
        obj = Foo()
        obj.x = 0
        obj.x = 42
        return obj.x

    check(fn, """
[NUM] > .*fn()
-> obj.x = 42
   5 frames hidden .*
# obj.x
0
# n
[NUM] > .*fn()
-> return obj.x
   5 frames hidden .*
# obj.x
42
# c
""")


def test_break_on_setattr_non_decorator():
    class Foo(object):
        pass

    def fn():
        a = Foo()
        b = Foo()

        def break_if_a(obj, value):
            return obj is a

        pdbpp.break_on_setattr("bar", condition=break_if_a, Pdb=PdbTest)(Foo)
        b.bar = 10
        a.bar = 42

    check(fn, """
[NUM] > .*fn()
-> a.bar = 42
   5 frames hidden .*
# c
""")


def test_break_on_setattr_overridden():
    # we don't use a class decorator to keep 2.5 compatibility
    class Foo(object):
        def __setattr__(self, attr, value):
            object.__setattr__(self, attr, value+1)
    Foo = pdbpp.break_on_setattr('x', Pdb=PdbTest)(Foo)

    def fn():
        obj = Foo()
        obj.y = 41
        obj.x = 0
        return obj.x

    check(fn, """
[NUM] > .*fn()
-> obj.x = 0
   5 frames hidden .*
# obj.y
42
# hasattr(obj, 'x')
False
# n
[NUM] > .*fn()
-> return obj.x
   5 frames hidden .*
# p obj.x
1
# c
""")


def test_track_with_no_args():
    pytest.importorskip('rpython.translator.tool.reftracker')

    def fn():
        set_trace()
        return 42

    check(fn, """
[NUM] > .*fn()
-> return 42
# track
... SyntaxError:
# c
""")


def test_utf8():
    def fn():
        # тест
        a = 1
        set_trace(Config=ConfigWithHighlight)
        return a

    # we cannot easily use "check" because the output is full of ANSI escape
    # sequences
    expected, lines = run_func(fn, '# ll\n# c')
    assert u'тест' in lines[5]


def test_debug_normal():
    def g():
        a = 1
        return a

    def fn():
        g()
        set_trace()
        return 1

    check(fn, """
[NUM] > .*fn()
-> return 1
   5 frames hidden .*
# debug g()
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(#) s
--Call--
[NUM] > .*g()
-> def g():
(#) ll
NUM  ->     def g():
NUM             a = 1
NUM             return a
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_debug_thrice():
    def fn():
        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# debug 1
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(#) debug 2
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
((#)) debug 34
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(((#))) p 42
42
(((#))) c
LEAVING RECURSIVE DEBUGGER
((#)) c
LEAVING RECURSIVE DEBUGGER
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_syntaxerror_in_command():
    expected_debug_err = "ENTERING RECURSIVE DEBUGGER\n\\*\\*\\* SyntaxError: .*"

    # Python 3.8.0a2+ handles the SyntaxError itself.
    # Ref/followup: https://github.com/python/cpython/pull/12103
    # https://github.com/python/cpython/commit/3e93643
    if sys.version_info >= (3, 7, 3):
        expected_debug_err += "\nLEAVING RECURSIVE DEBUGGER"

    def f():
        set_trace()

    check(f, """
--Return--
[NUM] > .*f()
-> set_trace
   5 frames hidden .*
# print(
\\*\\*\\* SyntaxError: .*
# debug print(
%s
# c
""" % expected_debug_err)


def test_debug_with_overridden_continue():
    class CustomPdb(PdbTest, object):
        """CustomPdb that overrides do_continue like with pytest's wrapper."""

        def do_continue(self, arg):
            global count_continue
            count_continue += 1
            print("do_continue_%d" % count_continue)
            return super(CustomPdb, self).do_continue(arg)

        do_c = do_cont = do_continue

    def g():
        a = 1
        return a

    def fn():
        global count_continue
        count_continue = 0

        g()

        set_trace(Pdb=CustomPdb)
        set_trace(Pdb=CustomPdb)

        assert count_continue == 3
        return 1

    check(fn, """
[NUM] > .*fn()
-> set_trace(Pdb=CustomPdb)
   5 frames hidden .*
# c
do_continue_1
[NUM] > .*fn()
-> assert count_continue == 3
   5 frames hidden .*
# debug g()
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(#) s
--Call--
[NUM] > .*g()
-> def g():
(#) ll
NUM  ->     def g():
NUM             a = 1
NUM             return a
(#) c
do_continue_2
LEAVING RECURSIVE DEBUGGER
# c
do_continue_3
""")


def test_before_interaction_hook():
    class MyConfig(ConfigTest):
        def before_interaction_hook(self, pdb):
            pdb.stdout.write('HOOK!\n')

    def fn():
        set_trace(Config=MyConfig)
        return 1

    check(fn, """
[NUM] > .*fn()
-> return 1
   5 frames hidden .*
HOOK!
# c
""")


def test_unicode_bug():
    def fn():
        set_trace()
        x = "this is plain ascii"  # noqa: F841
        y = "this contains a unicode: à"  # noqa: F841
        return

    check_output = """
[NUM] > .*fn()
-> x = "this is plain ascii"
   5 frames hidden .*
# n
[NUM] > .*fn()
-> y = "this contains a unicode: à"
   5 frames hidden .*
# c
"""

    if sys.version_info < (3, ):
        check_output = check_output.decode('utf-8')

    check(fn, check_output)


def test_continue_arg():
    def fn():
        set_trace()
        x = 1
        y = 2
        z = 3
        return x+y+z
    _, lineno = inspect.getsourcelines(fn)
    line_z = lineno+4

    check(fn, """
[NUM] > .*fn()
-> x = 1
   5 frames hidden .*
# c {break_lnum}
Breakpoint NUM at {filename}:{break_lnum}
Deleted breakpoint NUM
[NUM] > .*fn()
-> z = 3
   5 frames hidden .*
# c
    """.format(
        break_lnum=line_z,
        filename=RE_THIS_FILE_CANONICAL,
    ))


# On Windows, it seems like this file is handled as cp1252-encoded instead
# of utf8 (even though the "# -*- coding: utf-8 -*-" line exists) and the
# core pdb code does not support that. Or something to that effect, I don't
# actually know.
# UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position
# 6998: character maps to <undefined>.
# So we XFail this test on Windows.
@pytest.mark.xfail(
    sys.platform == "win32",
    raises=UnicodeDecodeError,
    strict=True,
    reason=(
        "Windows encoding issue. See comments and"
        " https://github.com/pdbpp/pdbpp/issues/341"
    ),
)
@pytest.mark.skipif(not hasattr(pdbpp.pdb.Pdb, "error"),
                    reason="no error method")
def test_continue_arg_with_error():
    def fn():
        set_trace()
        x = 1
        y = 2
        z = 3
        return x+y+z

    _, lineno = inspect.getsourcelines(fn)
    line_z = lineno + 4

    check(fn, r"""
[NUM] > .*fn()
-> x = 1
   5 frames hidden .*
# c.foo
\*\*\* The specified object '.foo' is not a function or was not found along sys.path.
# c {break_lnum}
Breakpoint NUM at {filename}:{break_lnum}
Deleted breakpoint NUM
[NUM] > .*fn()
-> z = 3
   5 frames hidden .*
# c
    """.format(
        break_lnum=line_z,
        filename=RE_THIS_FILE_CANONICAL,
    ))


@pytest.mark.skipif(sys.version_info < (3, 7), reason="header kwarg is 3.7+")
def test_set_trace_header():
    """Handler header kwarg added with Python 3.7 in pdb.set_trace."""
    def fn():
        set_trace_via_module(header="my_header")

    check(fn, """
my_header
--Return--
[NUM] > .*fn()
-> set_trace.*
   5 frames hidden .*
# c
""")


def test_stdout_encoding_None():
    instance = PdbTest()
    instance.stdout = BytesIO()
    instance.stdout.encoding = None

    instance.ensure_file_can_write_unicode(instance.stdout)

    try:
        import cStringIO
    except ImportError:
        pass
    else:
        instance.stdout = cStringIO.StringIO()
        instance.ensure_file_can_write_unicode(instance.stdout)


def test_frame_cmd_changes_locals():
    def a():
        x = 42  # noqa: F841
        b()

    def b():
        fn()

    def fn():
        set_trace()
        return

    check(a, """
[NUM] > .*fn()
-> return
   5 frames hidden .*
# f {frame_num_a}
[NUM] > .*a()
-> b()
# p list(sorted(locals().keys()))
['b', 'x']
# c
""".format(frame_num_a=count_frames() + 2 - 5))


@pytest.mark.skipif(not hasattr(pdbpp.pdb.Pdb, "_cmdloop"),
                    reason="_cmdloop is not available")
def test_sigint_in_interaction_with_new_cmdloop():
    def fn():
        def inner():
            raise KeyboardInterrupt()
        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# debug inner()
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(#) c
--KeyboardInterrupt--
# c
""")


@pytest.mark.skipif(hasattr(pdbpp.pdb.Pdb, "_cmdloop"),
                    reason="_cmdloop is available")
def test_sigint_in_interaction_without_new_cmdloop():
    def fn():
        def inner():
            raise KeyboardInterrupt()
        set_trace()

    with pytest.raises(KeyboardInterrupt):
        check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# debug inner()
ENTERING RECURSIVE DEBUGGER
[NUM] > .*
(#) c
""")

    # Reset pdb, which did not clean up correctly.
    # Needed for PyPy (Python 2.7.13[pypy-7.1.0-final]) with coverage and
    # restoring trace function.
    pdbpp.local.GLOBAL_PDB.reset()


def test_debug_rebind_globals(monkeypatch):
    class PdbWithCustomDebug(pdbpp.pdb.Pdb):
        def do_debug(self, arg):
            if "PdbTest" not in globals():
                # Do not use assert here, since it might fail with "NameError:
                # name '@pytest_ar' is not defined" via pytest's assertion
                # rewriting then.
                import pytest
                pytest.fail("PdbTest is not in globals.")
            print("called_do_debug", Pdb, self)  # noqa: F821

    monkeypatch.setattr(pdbpp.pdb, "Pdb", PdbWithCustomDebug)

    class CustomPdbTest(PdbTest, PdbWithCustomDebug):
        pass

    def fn():
        def inner():
            pass

        set_trace(Pdb=CustomPdbTest)

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace(.*)
   5 frames hidden .*
# debug inner()
called_do_debug.*
# c
""")


@pytest.mark.skipif(not hasattr(pdbpp.pdb.Pdb, "_previous_sigint_handler"),
                    reason="_previous_sigint_handler is not available")
def test_interaction_restores_previous_sigint_handler():
    """Test is based on cpython's test_pdb_issue_20766."""
    def fn():
        i = 1
        while i <= 2:
            sess = PdbTest(nosigint=False)
            sess.set_trace(sys._getframe())
            print('pdb %d: %s' % (i, sess._previous_sigint_handler))
            i += 1

    check(fn, """
[NUM] > .*fn()
-> print('pdb %d: %s' % (i, sess._previous_sigint_handler))
   5 frames hidden .*
# c
pdb 1: <built-in function default_int_handler>
[NUM] > .*fn()
-> .*
   5 frames hidden .*
# c
pdb 2: <built-in function default_int_handler>
""")


def test_recursive_set_trace():
    def fn():
        global inner
        global count
        count = 0

        def inner():
            global count
            count += 1

            if count == 1:
                set_trace()
            else:
                set_trace(cleanup=False)

        inner()

    check(fn, """
--Return--
[NUM] > .*inner()
-> set_trace()
   5 frames hidden .*
# inner()
# c
""")


def test_steps_over_set_trace():
    def fn():
        set_trace()
        print(1)

        set_trace(cleanup=False)
        print(2)

    check(fn, """
[NUM] > .*fn()
-> print(1)
   5 frames hidden .*
# n
1
[NUM] > .*fn()
-> set_trace(cleanup=False)
   5 frames hidden .*
# n
[NUM] > .*fn()
-> print(2)
   5 frames hidden .*
# c
2
""")


def test_break_after_set_trace():
    def fn():
        set_trace()
        print(1)
        print(2)

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
[NUM] > .*fn()
-> print(1)
   5 frames hidden .*
# break {lineno}
Breakpoint . at .*:{lineno}
# c
1
[NUM] > .*fn()
-> print(2)
   5 frames hidden .*
# import pdb; pdbpp.local.GLOBAL_PDB.clear_all_breaks()
# c
2
""".format(lineno=lineno + 3))


def test_break_with_inner_set_trace():
    def fn():
        def inner():
            set_trace(cleanup=False)

        set_trace()
        inner()
        print(1)

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
[NUM] > .*fn()
-> inner()
   5 frames hidden .*
# break {lineno}
Breakpoint . at .*:{lineno}
# c
--Return--
[NUM] > .*inner()->None
-> set_trace(cleanup=False)
   5 frames hidden .*
# import pdb; pdbpp.local.GLOBAL_PDB.clear_all_breaks()
# c
1
""".format(lineno=lineno + 8))


@pytest.mark.skipif(
    sys.version_info < (3,), reason="no support for exit from interaction with pdbrc"
)
def test_pdbrc_continue(tmpdirhome):
    """Test that interaction is skipped with continue in pdbrc."""
    assert os.getcwd() == str(tmpdirhome)
    with open(".pdbrc", "w") as f:
        f.writelines([
            "p 'from_pdbrc'\n",
            "continue\n",
        ])

    def fn():
        set_trace(readrc=True)
        print("after_set_trace")

    check(fn, """
'from_pdbrc'
after_set_trace
""")


def test_python_m_pdb_usage():
    p = subprocess.Popen(
        [sys.executable, "-m", "pdb"],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    stdout, stderr = p.communicate()
    out = stdout.decode("utf8")
    err = stderr.decode("utf8")
    assert err == ""
    assert "usage: pdb.py" in out


@pytest.mark.parametrize('PDBPP_HIJACK_PDB', (1, 0))
def test_python_m_pdb_uses_pdbpp_and_env(PDBPP_HIJACK_PDB, monkeypatch, tmpdir):
    from sysconfig import get_path

    if PDBPP_HIJACK_PDB:
        pth = os.path.join(get_path("purelib"), "pdbpp_hijack_pdb.pth")
        if not os.path.exists(pth):
            pytest.skip("Missing pth file ({}), editable install?".format(pth))

    monkeypatch.setenv("PDBPP_HIJACK_PDB", str(PDBPP_HIJACK_PDB))

    f = tmpdir.ensure("test.py")
    f.write(textwrap.dedent("""
        import inspect
        import os
        import pdb

        fname = os.path.basename(inspect.getfile(pdb.Pdb))
        if {PDBPP_HIJACK_PDB}:
            assert fname == 'pdbpp.py', (fname, pdb, pdb.Pdb)
        else:
            assert fname in ('pdb.py', 'pdb.pyc'), (fname, pdb, pdb.Pdb)
        pdb.set_trace()
    """.format(PDBPP_HIJACK_PDB=PDBPP_HIJACK_PDB)))

    p = subprocess.Popen(
        [sys.executable, "-m", "pdb", str(f)],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        stdin=subprocess.PIPE
    )
    stdout, stderr = p.communicate(b"c\n")
    out = stdout.decode("utf8")
    err = stderr.decode("utf8")
    print(out)
    print(err, file=sys.stderr)
    assert err == ""
    if PDBPP_HIJACK_PDB:
        assert "(Pdb)" not in out
        assert "(Pdb++)" in out
        if sys.platform == 'win32' and sys.version_info < (3,):  # XXX ???
            assert out.endswith("\n(Pdb++) " + os.linesep)
        else:
            assert out.endswith("\n(Pdb++) \n")
    else:
        assert "(Pdb)" in out
        assert "(Pdb++)" not in out
        assert out.endswith("\n(Pdb) " + os.linesep)


def get_completions(text):
    """Get completions from the installed completer."""
    readline_ = pdbpp.local.GLOBAL_PDB.fancycompleter.config.readline
    complete = readline_.get_completer()
    comps = []
    assert complete.__self__ is pdbpp.local.GLOBAL_PDB
    while True:
        val = complete(text, len(comps))
        if val is None:
            break
        comps += [val]
    return comps


def test_set_trace_in_completion(monkeypatch_readline):
    def fn():
        class CompleteMe(object):
            attr_called = 0

            @property
            def set_trace_in_attrib(self):
                self.attr_called += 1
                set_trace(cleanup=False)
                print("inner_set_trace_was_ignored")

        obj = CompleteMe()

        def check_completions():
            monkeypatch_readline("obj.", 0, 4)
            comps = get_completions("obj.")
            assert obj.attr_called == 1, "attr was called"

            # Colorization only works with pyrepl, via pyrepl.readline._setup.
            assert any("set_trace_in_attrib" in comp for comp in comps), comps
            return True

        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
.*
   5 frames hidden .*
# check_completions()
inner_set_trace_was_ignored
True
# c
""")


def test_completes_from_pdb(monkeypatch_readline):
    """Test that pdb's original completion is used."""
    def fn():
        where = 1  # noqa: F841
        set_trace()

        def check_completions():
            # Patch readline to return expected results for "wher".
            monkeypatch_readline("wher", 0, 4)
            assert get_completions("wher") == ["where"]

            if sys.version_info > (3, ):
                # Patch readline to return expected results for "disable ".
                monkeypatch_readline("disable", 8, 8)

                # NOTE: number depends on bpb.Breakpoint class state, just ensure that
                #       is a number.
                completion = pdbpp.local.GLOBAL_PDB.complete("", 0)
                assert int(completion) > 0

                # Patch readline to return expected results for "p ".
                monkeypatch_readline("p ", 2, 2)
                comps = get_completions("")
                assert "where" in comps

                # Dunder members get completed only on second invocation.
                assert "__name__" not in comps
                comps = get_completions("")
                assert "__name__" in comps

            # Patch readline to return expected results for "help ".
            monkeypatch_readline("help ", 5, 5)
            comps = get_completions("")
            assert "help" in comps

            return True

        set_trace()

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
[NUM] > .*fn()
.*
   5 frames hidden .*
# break %d
Breakpoint NUM at .*
# c
--Return--
[NUM] > .*fn()
.*
   5 frames hidden .*
# check_completions()
True
# c
""" % lineno)


def test_completion_uses_tab_from_fancycompleter(monkeypatch_readline):
    """Test that pdb's original completion is used."""
    def fn():
        def check_completions():
            # Patch readline to return expected results for "C.f()".
            monkeypatch_readline("C.f()", 5, 5)
            assert get_completions("") == ["\t"]
            return True

        set_trace()

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
--Return--
[NUM] > .*fn()->None
.*
   5 frames hidden .*
# check_completions()
True
# c
""")


def test_complete_removes_duplicates_with_coloring(
    monkeypatch_readline, readline_param
):
    def fn():
        helpvar = 42  # noqa: F841

        class obj:
            foo = 1
            foobar = 2

        def check_completions():
            # Patch readline to return expected results for "help".
            monkeypatch_readline("help", 0, 4)

            if readline_param == "pyrepl":
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is True
                assert get_completions("help") == [
                    "\x1b[000;00m\x1b[00mhelp\x1b[00m",
                    "\x1b[001;00m\x1b[33;01mhelpvar\x1b[00m",
                    " ",
                ]
            else:
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is False
                assert get_completions("help") == ["help", "helpvar"]

            # Patch readline to return expected results for "p helpvar.".
            monkeypatch_readline("p helpvar.", 2, 10)
            if readline_param == "pyrepl":
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is True
                comps = get_completions("helpvar.")
                assert type(helpvar.denominator) == int
                assert any(
                    re.match(r"\x1b\[\d\d\d;00m\x1b\[33;01mdenominator\x1b\[00m", x)
                    for x in comps
                )
                assert " " in comps
            else:
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is False
                comps = get_completions("helpvar.")
                assert "denominator" in comps
                assert " " not in comps

            monkeypatch_readline("p obj.f", 2, 7)
            comps = get_completions("obj.f")
            assert comps == ['obj.foo']

            monkeypatch_readline("p obj.foo", 2, 9)
            comps = get_completions("obj.foo")
            if readline_param == "pyrepl":
                assert comps == [
                    '\x1b[000;00m\x1b[33;01mfoo\x1b[00m',
                    '\x1b[001;00m\x1b[33;01mfoobar\x1b[00m',
                    ' ']
            else:
                assert comps == ['foo', 'foobar', ' ']

            monkeypatch_readline("disp", 0, 4)
            comps = get_completions("disp")
            assert comps == ['display']

            return True

        set_trace()

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
--Return--
[NUM] > .*fn()->None
.*
   5 frames hidden .*
# check_completions()
True
# c
""")


class TestCompleteUnit:
    def test_fancy_prefix_with_same_in_pdb(self, patched_completions):
        assert patched_completions(
            "p foo.b",
            ["foo.bar"],
            ["foo.bar", "foo.barbaz"]
        ) == ["foo.bar"]
        assert patched_completions(
            "p foo.b",
            ["foo.bar"],
            ["foo.bar", "foo.barbaz", "foo.barbaz2"]
        ) == ["foo.bar"]

    def test_fancy_prefix_with_more_pdb(self, patched_completions):
        assert patched_completions(
            "p foo.b", ["foo.bar"], ["foo.bar", "foo.barbaz", "something else"]
        ) == ["foo.bar", "foo.barbaz", "something else"]

    def test_fancy_with_no_pdb(self, patched_completions,
                               fancycompleter_color_param):

        if fancycompleter_color_param == "color":
            fancy = [
                "\x1b[000;00m\x1b[33;01mfoo\x1b[00m",
                "\x1b[001;00m\x1b[33;01mfoobar\x1b[00m",
                " ",
            ]
        else:
            fancy = [
                "foo",
                "foobar",
                " ",
            ]
        assert patched_completions("foo", fancy, []) == fancy

    def test_fancy_with_prefixed_pdb(self, patched_completions):
        assert patched_completions("sys.version", [
            "version",
            "version_info",
            " ",
        ], [
            "sys.version",
            "sys.version_info",
        ]) == ["version", "version_info", " "]

    def test_fancy_with_prefixed_pdb_other_text(self, patched_completions):
        fancy = ["version", "version_info"]
        pdb = ["sys.version", "sys.version_info"]
        assert patched_completions("xxx", fancy, pdb) == fancy + pdb

    def test_fancy_tab_without_pdb(self, patched_completions):
        assert patched_completions("", ["\t"], []) == ["\t"]

    def test_fancy_tab_with_pdb(self, patched_completions):
        assert patched_completions("", ["\t"], ["help"]) == ["help"]


def test_complete_uses_attributes_only_from_orig_pdb(
    monkeypatch_readline, readline_param
):
    def fn():
        def check_completions():
            # Patch readline to return expected results for "p sys.version".
            monkeypatch_readline("p sys.version", 2, 13)

            if readline_param == "pyrepl":
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is True
                assert get_completions("sys.version") == [
                    "\x1b[000;00m\x1b[32;01mversion\x1b[00m",
                    "\x1b[001;00m\x1b[00mversion_info\x1b[00m",
                    " ",
                ]
            else:
                assert pdbpp.local.GLOBAL_PDB.fancycompleter.config.use_colors is False
                assert get_completions("sys.version") == [
                    "version",
                    "version_info",
                    " ",
                ]
            return True

        set_trace()

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
--Return--
[NUM] > .*fn()->None
.*
   5 frames hidden .*
# import sys
# check_completions()
True
# c
""")


@pytest.mark.skipif(sys.version_info < (3, ), reason="py2: no completion for break")
def test_completion_removes_tab_from_fancycompleter(monkeypatch_readline):
    def fn():
        def check_completions():
            # Patch readline to return expected results for "b ".
            monkeypatch_readline("b ", 2, 2)
            comps = get_completions("")
            assert "\t" not in comps
            assert "inspect" in comps
            return True

        set_trace()

    _, lineno = inspect.getsourcelines(fn)

    check(fn, """
--Return--
[NUM] > .*fn()
.*
   5 frames hidden .*
# check_completions()
True
# c
""")


def test_complete_with_bang(monkeypatch_readline):
    """Test that completion works after "!".

    This requires parseline to return "" for the command (bpo-35270).
    """
    def fn():
        a_var = 1  # noqa: F841

        def check_completions():
            # Patch readline to return expected results for "!a_va".
            monkeypatch_readline("!a_va", 0, 5)
            assert pdbpp.local.GLOBAL_PDB.complete("a_va", 0) == "a_var"

            # Patch readline to return expected results for "list(a_va".
            monkeypatch_readline("list(a_va", 5, 9)
            assert pdbpp.local.GLOBAL_PDB.complete("a_va", 0) == "a_var"
            return True

        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
.*
   5 frames hidden .*
# check_completions()
True
# c
""")


def test_completer_after_debug(monkeypatch_readline):
    def fn():
        myvar = 1  # noqa: F841

        def inner():
            myinnervar = 1  # noqa: F841

            def check_completions_inner():
                # Patch readline to return expected results for "myin".
                monkeypatch_readline("myin", 0, 4)
                assert "myinnervar" in get_completions("myin")
                return True

            print("inner_end")

        def check_completions():
            # Patch readline to return expected results for "myva".
            monkeypatch_readline("myva", 0, 4)
            assert "myvar" in get_completions("myva")
            return True

        set_trace()

        print("ok_end")
    check(fn, """
[NUM] > .*fn()
.*
   5 frames hidden .*
# pdbpp.local.GLOBAL_PDB.curframe.f_code.co_name
'fn'
# debug inner()
ENTERING RECURSIVE DEBUGGER
[1] > <string>(1)<module>()
(#) pdbpp.local.GLOBAL_PDB.curframe.f_code.co_name
'<module>'
(#) s
--Call--
[NUM] > .*inner()
-> def inner():
(#) pdbpp.local.GLOBAL_PDB.curframe.f_code.co_name
'inner'
(#) r
inner_end
--Return--
[NUM] > .*inner()->None
-> print("inner_end")
(#) check_completions_inner()
True
(#) q
LEAVING RECURSIVE DEBUGGER
# check_completions()
True
# c
ok_end
""")


def test_nested_completer(testdir):
    p1 = testdir.makepyfile(
        """
        import sys

        frames = []

        def inner():
            completeme_inner = 1
            frames.append(sys._getframe())

        inner()

        def outer():
            completeme_outer = 2
            __import__('pdb').set_trace()

        outer()
        """
    )
    with open(".fancycompleterrc.py", "w") as f:
        f.write(textwrap.dedent("""
            from fancycompleter import DefaultConfig

            class Config(DefaultConfig):
                use_colors = False
                prefer_pyrepl = False
            """))
    testdir.monkeypatch.setenv("PDBPP_COLORS", "0")
    child = testdir.spawn("{} {}".format(quote(sys.executable), str(p1)))
    child.send("completeme\t")
    child.expect_exact("\r\n(Pdb++) completeme_outer")
    child.send("\nimport pdbpp; _p = pdbpp.Pdb(); _p.reset()")
    child.send("\n_p.interaction(frames[0], None)\n")
    child.expect_exact("\r\n-> frames.append(sys._getframe())\r\n(Pdb++) ")
    child.send("completeme\t")
    child.expect_exact("completeme_inner")
    child.send("\nq\n")
    child.send("completeme\t")
    child.expect_exact("completeme_outer")
    child.send("\n")
    child.sendeof()


def test_ensure_file_can_write_unicode():
    out = io.BytesIO(b"")
    stdout = io.TextIOWrapper(out, encoding="latin1")

    p = Pdb(Config=DefaultConfig, stdout=stdout)

    assert p.stdout.stream is out

    p.stdout.write(u"test äöüß")
    out.seek(0)
    assert out.read().decode("utf-8") == u"test äöüß"


@pytest.mark.skipif(sys.version_info >= (3, 0),
                    reason="test is python2 specific")
def test_py2_ensure_file_can_write_unicode():
    import StringIO

    stdout = StringIO.StringIO()
    stdout.encoding = 'ascii'

    p = Pdb(Config=DefaultConfig, stdout=stdout)

    assert p.stdout.stream is stdout

    p.stdout.write(u"test äöüß")
    stdout.seek(0)
    assert stdout.read().decode('utf-8') == u"test äöüß"


def test_signal_in_nonmain_thread_with_interaction():
    def fn():
        import threading

        evt = threading.Event()

        def start_thread():
            evt.wait()
            set_trace(nosigint=False)

        t = threading.Thread(target=start_thread)
        t.start()
        set_trace(nosigint=False)
        evt.set()
        t.join()

    check(fn, """
[NUM] > .*fn()
-> evt.set()
   5 frames hidden .*
# c
--Return--
[NUM] > .*start_thread()->None
-> set_trace(nosigint=False)
# c
""")


def test_signal_in_nonmain_thread_with_continue():
    """Test for cpython issue 13120 (test_issue13120).

    Without the try/execept for ValueError in its do_continue it would
    display the exception, but work otherwise.
    """
    def fn():
        import threading

        def start_thread():
            a = 42  # noqa F841
            set_trace(nosigint=False)

        t = threading.Thread(target=start_thread)
        t.start()
        # set_trace(nosigint=False)
        t.join()

    check(fn, """
--Return--
[NUM] > .*start_thread()->None
-> set_trace(nosigint=False)
# p a
42
# c
""")


def test_next_at_end_of_stack_after_unhide():
    """Test that compute_stack returns correct length with show_hidden_frames."""
    class MyConfig(ConfigTest):
        def before_interaction_hook(self, pdb):
            pdb.stdout.write('before_interaction_hook\n')
            pdb.do_hf_unhide(arg=None)

    def fn():
        set_trace(Config=MyConfig)
        return 1

    check(fn, """
[NUM] > .*fn()
-> return 1
   5 frames hidden .*
before_interaction_hook
# n
--Return--
[NUM] > .*fn()->1
-> return 1
   5 frames hidden .*
before_interaction_hook
# c
""")


def test_compute_stack_keeps_frame():
    """With only hidden frames the last one is kept."""
    def fn():
        def raises():
            raise Exception("foo")

        try:
            raises()
        except Exception:
            tb = sys.exc_info()[2]
            tb_ = tb
            while tb_:
                tb_.tb_frame.f_locals["__tracebackhide__"] = True
                tb_ = tb_.tb_next
            pdbpp.post_mortem(tb, Pdb=PdbTest)
        return 1

    check(fn, """
[0] > .*raises()
-> raise Exception("foo")
   1 frame hidden (try 'help hidden_frames')
# bt
> [0] .*raises()
      raise Exception("foo")
# hf_unhide
# bt
  [0] .*fn()
      raises()
> [1] .*raises()
      raise Exception("foo")
# q
""")


def test_compute_stack_without_stack():
    pdb_ = PdbTest()
    assert pdb_.compute_stack([], idx=None) == ([], 0)
    assert pdb_.compute_stack([], idx=0) == ([], 0)
    assert pdb_.compute_stack([], idx=10) == ([], 10)


def test_rawinput_with_debug():
    """Test backport of fix for bpo-31078."""
    def fn():
        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# debug 1
ENTERING RECURSIVE DEBUGGER
[NUM] > <string>(1)<module>()->None
(#) import pdb; print(pdbpp.local.GLOBAL_PDB.use_rawinput)
1
(#) p sys._getframe().f_back.f_locals['self'].use_rawinput
1
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_error_with_traceback():
    def fn():
        def error():
            raise ValueError("error")

        set_trace()

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# error()
\\*\\*\\* ValueError: error
Traceback (most recent call last):
  File .*, in error
    raise ValueError("error")
# c
""")


def test_chained_syntaxerror_with_traceback():
    def fn():
        def compile_error():
            compile("invalid(", "<stdin>", "single")

        def error():
            try:
                compile_error()
            except Exception:
                raise AttributeError

        set_trace()

    if sys.version_info > (3,):
        check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# error()
\\*\\*\\* AttributeError.*
Traceback (most recent call last):
  File .*, in error
    compile_error()
  File .*, in compile_error
    compile.*
  File "<stdin>", line 1
    invalid(
    .*^
SyntaxError: .*

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File .*, in error
    raise AttributeError
# c
""")
    else:
        check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace()
   5 frames hidden .*
# error()
\\*\\*\\* AttributeError.*
Traceback (most recent call last):
  File .*, in error
    raise AttributeError
# c
""")


def test_error_with_traceback_disabled():
    class ConfigWithoutTraceback(ConfigTest):
        show_traceback_on_error = False

    def fn():
        def error():
            raise ValueError("error")

        set_trace(Config=ConfigWithoutTraceback)

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace(Config=ConfigWithoutTraceback)
   5 frames hidden .*
# error()
\\*\\*\\* ValueError: error
# c
""")


def test_error_with_traceback_limit():
    class ConfigWithLimit(ConfigTest):
        show_traceback_on_error_limit = 2

    def fn():
        def f(i):
            i -= 1
            if i <= 0:
                raise ValueError("the_end")
            f(i)

        def error():
            f(10)

        set_trace(Config=ConfigWithLimit)

    check(fn, """
--Return--
[NUM] > .*fn()
-> set_trace(Config=ConfigWithLimit)
   5 frames hidden .*
# error()
\\*\\*\\* ValueError: the_end
Traceback (most recent call last):
  File .*, in error
    f(10)
  File .*, in f
    f(i)
# c
""")


@pytest.mark.parametrize("show", (True, False))
def test_complete_displays_errors(show, monkeypatch, LineMatcher):
    class Config(ConfigTest):
        show_traceback_on_error = show

    def raises(*args):
        raise ValueError("err_complete")

    monkeypatch.setattr("pdbpp.Pdb._get_all_completions", raises)

    def fn():
        set_trace(Config=Config)

    out = runpdb(fn, ["get_completions('test')", "c"])
    lm = LineMatcher(out)
    if show:
        lm.fnmatch_lines([
            "--Return--",
            "[[]*[]] > *fn()->None",
            "-> set_trace(Config=Config)",
            "   5 frames hidden (try 'help hidden_frames')",
            "# get_completions('test')",
            "*** error during completion: err_complete",
            "ValueError: err_complete",
            "[[][]]",
            "# c",
        ])
    else:
        lm.fnmatch_lines([
            "[[]*[]] > *fn()->None",
            "-> set_trace(Config=Config)",
            "   5 frames hidden (try 'help hidden_frames')",
            "# get_completions('test')",
            "*** error during completion: err_complete",
            "??",
            "# c",
        ])


def test_next_with_exception_in_call():
    """Ensure that "next" works correctly with exception (in try/except).

    Previously it would display the frame where the exception occurred, and
    then "next" would continue, instead of stopping at the next statement.
    """
    def fn():
        def keyerror():
            raise KeyError

        set_trace()
        try:
            keyerror()
        except KeyError:
            print("got_keyerror")

    check(fn, """
[NUM] > .*fn()
-> try:
   5 frames hidden .*
# n
[NUM] > .*fn()
-> keyerror()
   5 frames hidden .*
# n
KeyError
[NUM] > .*fn()
-> keyerror()
   5 frames hidden .*
# n
[NUM] > .*fn()
-> except KeyError:
   5 frames hidden .*
# c
got_keyerror
""")


def test_locals():
    def fn():
        def f():
            set_trace()
            print("foo=%s" % foo)  # noqa: F821
            foo = 2  # noqa: F841

        f()

    check(fn, """
[NUM] > .*f()
-> print("foo=%s" % foo)
   5 frames hidden .*
# foo = 42
# foo
42
# pp foo
42
# p foo
42
# c
foo=42
""")


def test_locals_with_list_comprehension():
    def fn():
        mylocal = 1  # noqa: F841
        set_trace()
        print(mylocal)

    check(fn, """
[NUM] > .*fn()
-> print(mylocal)
   5 frames hidden .*
# mylocal
1
# [x for x in str(mylocal)]
['1']
# [mylocal for x in range(1)]
[1]
# mylocal = 42
# [x for x in str(mylocal)]
['4', '2']
# [mylocal for x in range(1)]
[42]
# c
42
""")


def test_get_editor_cmd(monkeypatch):
    _pdb = PdbTest()

    _pdb.config.editor = None
    monkeypatch.setenv("EDITOR", "nvim")
    assert _pdb._get_editor_cmd("fname", 42) == "nvim +42 fname"

    monkeypatch.setenv("EDITOR", "")
    with pytest.raises(RuntimeError, match=(
            r"Could not detect editor. Configure it or set \$EDITOR."
    )):
        _pdb._get_editor_cmd("fname", 42)

    monkeypatch.delenv("EDITOR")

    try:
        which = "shutil.which"
        monkeypatch.setattr(which, lambda x: None)
    except AttributeError:
        which = "distutils.spawn.find_executable"
        monkeypatch.setattr(which, lambda x: None)
    with pytest.raises(RuntimeError, match=(
            r"Could not detect editor. Configure it or set \$EDITOR."
    )):
        _pdb._get_editor_cmd("fname", 42)

    monkeypatch.setattr(which, lambda x: "vim")
    assert _pdb._get_editor_cmd("fname", 42) == "vim +42 fname"
    monkeypatch.setattr(which, lambda x: "vi")
    assert _pdb._get_editor_cmd("fname", 42) == "vi +42 fname"

    _format = _pdb._format_editcmd
    assert _format("subl {filename}:{lineno}", "with space", 12) == (
        "subl 'with space':12")
    assert _format("edit", "with space", 12) == (
        "edit +12 'with space'")
    assert _format("edit +%%%d %%%s%% %d", "with space", 12) == (
        "edit +%12 %'with space'% 12")


def test_edit_error(monkeypatch):
    class MyConfig(ConfigTest):
        editor = None

    monkeypatch.setenv("EDITOR", "")

    def fn():
        set_trace(Config=MyConfig)

    check(fn, r"""
--Return--
[NUM] > .*fn()
-> set_trace(Config=MyConfig)
   5 frames hidden .*
# edit
\*\*\* Could not detect editor. Configure it or set \$EDITOR.
# c
""")


def test_global_pdb_per_thread_with_input_lock():
    def fn():
        import threading

        evt1 = threading.Event()
        evt2 = threading.Event()

        def __t1__(evt1, evt2):
            set_trace(cleanup=False)

        def __t2__(evt2):
            evt2.set()
            set_trace(cleanup=False)

        t1 = threading.Thread(name="__t1__", target=__t1__, args=(evt1, evt2))
        t1.start()

        assert evt1.wait(1.0) is True
        t2 = threading.Thread(name="__t2__", target=__t2__, args=(evt2,))
        t2.start()

        t1.join()
        t2.join()

    check(fn, r"""
--Return--
[NUM] > .*__t1__()
-> set_trace(cleanup=False)
# evt1.set()
# import threading; threading.current_thread().name
'__t1__'
# assert evt2.wait(1.0) is True; import time; time.sleep(0.1)
--Return--
[NUM] > .*__t2__()->None
-> set_trace(cleanup=False)
# import threading; threading.current_thread().name
'__t2__'
# c
# import threading; threading.current_thread().name
'__t1__'
# c
""")


def test_usage_error_with_commands():
    def fn():
        set_trace()

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# commands invalid
.*Usage.*: commands [bnum]
        ...
        end
# c
""")


@pytest.mark.skipif(sys.version_info < (3,),
                    reason="py2 has no support for kwonly")
def test_rebind_globals_kwonly():
    exec("def func(*args, header=None): pass", globals())
    func = globals()["func"]

    sig = str(inspect.signature(func))
    assert sig == "(*args, header=None)"
    new = pdbpp.rebind_globals(func, globals())
    assert str(inspect.signature(new)) == sig


@pytest.mark.skipif(sys.version_info < (3,),
                    reason="py2 has no support for annotations")
def test_rebind_globals_annotations():
    exec("def func(ann: str = None): pass", globals())
    func = globals()["func"]

    sig = str(inspect.signature(func))
    if sys.version_info < (3, 5):
        assert sig == "(ann:str=None)"
    else:
        assert sig in (
             "(ann: str = None)",
             "(ann:str=None)",
        )
    new = pdbpp.rebind_globals(func, globals())
    assert str(inspect.signature(new)) == sig


def test_rebind_globals_with_partial():
    import functools

    global test_global
    test_global = 0

    def func(a, b):
        global test_global
        return a + b + test_global

    pfunc = functools.partial(func)
    assert pfunc(0, 0) == 0

    newglobals = globals().copy()
    newglobals['test_global'] = 1
    new = pdbpp.rebind_globals(pfunc, newglobals)
    assert new(1, 40) == 42


def test_debug_with_set_trace():
    def fn():
        def inner():
            def inner_inner():
                pass

            set_trace(cleanup=False)

        set_trace()
    check(fn, """
--Return--
[NUM] > .*fn()
.*
   5 frames hidden .*
# debug inner()
ENTERING RECURSIVE DEBUGGER
[NUM] > <string>(1)<module>()->None
(#) r
--Return--
[NUM] > .*inner()->None
-> set_trace(cleanup=False)
   5 frames hidden .*
(#) pdbpp.local.GLOBAL_PDB.curframe.f_code.co_name
'inner'
(#) debug inner_inner()
ENTERING RECURSIVE DEBUGGER
[NUM] > <string>(1)<module>()->None
((#)) c
LEAVING RECURSIVE DEBUGGER
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_set_trace_with_incomplete_pdb():
    def fn():
        existing_pdb = PdbTest()
        assert not hasattr(existing_pdb, "botframe")

        set_trace(cleanup=False)

        assert hasattr(existing_pdb, "botframe")
        assert pdbpp.local.GLOBAL_PDB is existing_pdb
    check(fn, """
[NUM] > .*fn()
.*
   5 frames hidden .*
# c
""")


def test_config_gets_start_filename():
    def fn():
        setup_lineno = set_trace.__code__.co_firstlineno + 8
        set_trace_lineno = sys._getframe().f_lineno + 8

        class MyConfig(ConfigTest):
            def setup(self, pdb):
                print("config_setup")
                assert pdb.start_filename.lower() == THIS_FILE_CANONICAL.lower()
                assert pdb.start_lineno == setup_lineno

        set_trace(Config=MyConfig)

        assert pdbpp.local.GLOBAL_PDB.start_lineno == set_trace_lineno

    check(fn, r"""
config_setup
[NUM] > .*fn()
-> assert pdbpp.local.GLOBAL_PDB.start_lineno == set_trace_lineno
   5 frames hidden .*
# c
""")


def test_do_bt():
    def fn():
        set_trace()

    expected_bt = []
    for i, entry in enumerate(traceback.extract_stack()[:-3]):
        expected_bt.append("  [%2d] .*" % i)
        expected_bt.append("  .*")

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# bt
{expected}
  [NUM] .*(NUM)runpdb()
       func()
> [NUM] .*(NUM)fn()->None
       set_trace()
# c
""".format(expected="\n".join(expected_bt)))


def test_do_bt_highlight():
    def fn():
        set_trace(Config=ConfigWithHighlight)

    expected_bt = []
    for i, entry in enumerate(traceback.extract_stack()[:-3]):
        expected_bt.append("  [%2d] .*" % i)
        expected_bt.append("  .*")

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config=ConfigWithHighlight)
   5 frames hidden .*
# bt
{expected}
  [NUM] ^[[33;01m.*\.py^[[00m(^[[36;01mNUM^[[00m)runpdb()
       func()
> [NUM] ^[[33;01m.*\.py^[[00m(^[[36;01mNUM^[[00m)fn()->None
       set_trace(Config=ConfigWithHighlight)
# c
""".format(expected="\n".join(expected_bt)))


def test_do_bt_pygments():
    def fn():
        set_trace(Config=ConfigWithPygments)

    expected_bt = []
    for i, entry in enumerate(traceback.extract_stack()[:-3]):
        expected_bt.append("  [%2d] .*" % i)
        expected_bt.append("  .*")

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
   5 frames hidden .*
# bt
{expected}
  [NUM] .*(NUM)runpdb()
       func()
> [NUM] .*\.py(NUM)fn()->None
       set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
# c
""".format(expected="\n".join(expected_bt)))


def test_debug_with_pygments():
    def fn():
        set_trace(Config=ConfigWithPygments)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
   5 frames hidden .*
# debug 1
ENTERING RECURSIVE DEBUGGER
[1] > <string>(1)<module>()->None
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_debug_with_pygments_and_highlight():
    def fn():
        set_trace(Config=ConfigWithPygmentsAndHighlight)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config^[[38;5;241m=^[[39mConfigWithPygmentsAndHighlight)
   5 frames hidden .*
# debug 1
ENTERING RECURSIVE DEBUGGER
[1] > ^[[33;01m<string>^[[00m(^[[36;01m1^[[00m)<module>()->None
(#) c
LEAVING RECURSIVE DEBUGGER
# c
""")


def test_set_trace_in_default_code():
    """set_trace while not tracing and should not (re)set the global pdb."""
    def fn():
        def f():
            before = pdbpp.local.GLOBAL_PDB
            set_trace(cleanup=False)
            assert before is pdbpp.local.GLOBAL_PDB
        set_trace()

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# f()
# import pdbpp; pdbpp.local.GLOBAL_PDB.curframe is not None
True
# l {line_num}, 2
NUM \t    def fn():
NUM \t        def f():
NUM \t            before = pdbpp.local.GLOBAL_PDB
# c
    """.format(
        line_num=fn.__code__.co_firstlineno,
    ))


def test_error_with_pp():
    def fn():
        class BadRepr:
            def __repr__(self):
                raise Exception('repr_exc')

        obj = BadRepr()  # noqa: F841
        set_trace()

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# p obj
\*\*\* Exception: repr_exc
# pp obj
\*\*\* Exception: repr_exc
# pp BadRepr.__repr__()
\*\*\* TypeError: .*__repr__.*
# c
""")


def test_count_with_pp():
    def fn():
        set_trace()

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# pp [1, 2, 3]
[1, 2, 3]
# 2pp [1, 2, 3]
[1,
 2,
 3]
# 80pp [1, 2, 3]
[1, 2, 3]
# c
""")


def test_ArgWithCount():
    from pdbpp import ArgWithCount

    obj = ArgWithCount("", None)
    assert obj == ""
    assert repr(obj) == "<ArgWithCount cmd_count=None value=''>"
    assert isinstance(obj, str)

    obj = ArgWithCount("foo", 42)
    assert obj == "foo"
    assert repr(obj) == "<ArgWithCount cmd_count=42 value='foo'>"


def test_do_source():
    def fn():
        set_trace()

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace()
   5 frames hidden .*
# source ConfigWithPygmentsAndHighlight
\d\d     class ConfigWithPygmentsAndHighlight(ConfigWithPygments, ConfigWithHigh$
\d\d         pass
# c
""")


def test_do_source_with_pygments():
    def fn():
        set_trace(Config=ConfigWithPygments)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config^[[38;5;241m=^[[39mConfigWithPygments)
   5 frames hidden .*
# source ConfigWithPygmentsAndHighlight
\d\d     ^[[38;5;28;01mclass^[[39;00m ^[[38;5;21;01mConfigWithPygmentsAndHighlight^[[39;00m(ConfigWithPygments, ConfigWithHigh$
\d\d         ^[[38;5;28;01mpass^[[39;00m
# c
""")  # noqa: E501


def test_do_source_with_highlight():
    def fn():
        set_trace(Config=ConfigWithHighlight)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config=ConfigWithHighlight)
   5 frames hidden .*
# source ConfigWithPygmentsAndHighlight
^[[36;01m\d\d^[[00m     class ConfigWithPygmentsAndHighlight(ConfigWithPygments, ConfigWithHigh$
^[[36;01m\d\d^[[00m         pass
# c
""")  # noqa: E501


def test_do_source_with_pygments_and_highlight():
    def fn():
        set_trace(Config=ConfigWithPygmentsAndHighlight)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config^[[38;5;241m=^[[39mConfigWithPygmentsAndHighlight)
   5 frames hidden .*
# source ConfigWithPygmentsAndHighlight
^[[36;01m\d\d^[[00m     ^[[38;5;28;01mclass^[[39;00m ^[[38;5;21;01mConfigWithPygmentsAndHighlight^[[39;00m(ConfigWithPygments, ConfigWithHigh$
^[[36;01m\d\d^[[00m         ^[[38;5;28;01mpass^[[39;00m
# c
""")  # noqa: E501


def test_do_source_without_truncating():
    def fn():
        class Config(ConfigTest):
            truncate_long_lines = False

        set_trace(Config=Config)

    check(fn, r"""
--Return--
[NUM] > .*fn()->None
-> set_trace(Config=Config)
   5 frames hidden .*
# source ConfigWithPygmentsAndHighlight
\d\d     class ConfigWithPygmentsAndHighlight(ConfigWithPygments, ConfigWithHighlight):$
\d\d         pass
# c
""")


def test_handles_set_trace_in_config(tmpdir):
    """Should not cause a RecursionError."""
    def fn():
        class Config(ConfigTest):
            def __init__(self, *args, **kwargs):
                print("Config.__init__")
                # Becomes a no-op.
                set_trace(Config=Config)
                print("after_set_trace")

        set_trace(Config=Config)

    check(fn, r"""
Config.__init__
pdb\+\+: using pdb.Pdb for recursive set_trace.
> .*__init__()
-> print("after_set_trace")
(Pdb) c
after_set_trace
--Return--
[NUM] > .*fn()->None
-> set_trace(Config=Config)
   5 frames hidden .*
# c
""")


def test_only_question_mark(monkeypatch):
    def fn():
        set_trace()
        a = 1
        return a

    monkeypatch.setattr(PdbTest, "do_help", lambda self, arg: print("do_help"))

    check(fn, """
[NUM] > .*fn()
-> a = 1
   5 frames hidden .*
# ?
do_help
# c
""")


@pytest.mark.parametrize('s,maxlength,expected', [
    pytest.param('foo', 3, 'foo', id='id1'),
    pytest.param('foo', 1, 'f', id='id2'),

    # Keeps trailing escape sequences (for reset at least).
    pytest.param("\x1b[39m1\x1b[39m23", 1, "\x1b[39m1\x1b[39m", id="id3"),
    pytest.param("\x1b[39m1\x1b[39m23", 2, "\x1b[39m1\x1b[39m2", id="id4"),
    pytest.param("\x1b[39m1\x1b[39m23", 3, "\x1b[39m1\x1b[39m23", id="id5"),
    pytest.param("\x1b[39m1\x1b[39m23", 100, "\x1b[39m1\x1b[39m23", id="id5"),

    pytest.param("\x1b[39m1\x1b[39m", 100, "\x1b[39m1\x1b[39m", id="id5"),
])
def test_truncate_to_visible_length(s, maxlength, expected):
    assert pdbpp.Pdb._truncate_to_visible_length(s, maxlength) == expected


def test_keeps_reset_escape_sequence_with_source_highlight():
    class MyConfig(ConfigWithPygmentsAndHighlight):
        sticky_by_default = True

    def fn():
        set_trace(Config=MyConfig)

        a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX"  # noqa: E501
        b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbX"  # noqa: E501
        return a, b

    check(fn, """
[NUM] > .*fn()

<COLORNUM>         ^[[38;5;28;01mdef^[[39;00m ^[[38;5;21mfn^[[39m():
<COLORNUM>             set_trace(Config^[[38;5;241m=^[[39mMyConfig)
<COLORNUM>     $
<COLORCURLINE>  ->         a ^[[38;5;241;44m=^[[39;44m ^[[38;5;124;44m"^[[39;44m^[[38;5;124;44maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^[[39;44m^[[38;5;124;44m<PYGMENTSRESET><COLORRESET>
<COLORNUM>             b ^[[38;5;241m=^[[39m ^[[38;5;124m"^[[39m^[[38;5;124mbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb^[[39m^[[38;5;124m<PYGMENTSRESET>
<COLORNUM>             ^[[38;5;28;01mreturn^[[39;00m a, b
# c
""")  # noqa: E501


@pytest.mark.parametrize("pass_stdout", (True, False))
def test_stdout_reconfigured(pass_stdout, monkeypatch):
    """Check that self.stdout is re-configured with global pdb."""
    def fn():
        import sys
        if sys.version_info > (3,):
            from io import StringIO
        else:
            from StringIO import StringIO

        patched_stdout = StringIO()

        with monkeypatch.context() as mp:
            mp.setattr(sys, "stdout", patched_stdout)

            class _PdbTestKeepRawInput(PdbTest):
                def __init__(
                    self, completekey="tab", stdin=None, stdout=None, *args, **kwargs
                ):
                    if pass_stdout:
                        stdout = sys.stdout
                    else:
                        stdout = None
                    super(_PdbTestKeepRawInput, self).__init__(
                        completekey, stdin, stdout, *args, **kwargs
                    )
                    # Keep this, which gets set to 0 with stdin being passed in.
                    self.use_rawinput = True

            set_trace(Pdb=_PdbTestKeepRawInput)
            assert pdbpp.local.GLOBAL_PDB.stdout is patched_stdout

            print(patched_stdout.getvalue())
            patched_stdout.close()

        set_trace(Pdb=_PdbTestKeepRawInput, cleanup=False)
        assert pdbpp.local.GLOBAL_PDB.stdout is sys.stdout
        print('# c')  # Hack to reflect output in test.
        return

    check(fn, """
[NUM] > .*fn()
-> assert pdbpp.local.GLOBAL_PDB.stdout is sys.stdout
   5 frames hidden .*
# c
# c
""")


def test_position_of_obj_unwraps():
    import contextlib

    @contextlib.contextmanager
    def cm():
        raise NotImplementedError()

    pdb_ = PdbTest()
    pos = pdb_._get_position_of_obj(cm)

    if hasattr(inspect, "unwrap"):
        assert pos[0] == THIS_FILE_CANONICAL
        assert pos[2] == [
            "    @contextlib.contextmanager\n",
            "    def cm():\n",
            "        raise NotImplementedError()\n",
        ]
    else:
        contextlib_file = contextlib.__file__
        if sys.platform == 'win32':
            contextlib_file = contextlib_file.lower()
        assert pos[0] == contextlib_file.rstrip("c")


def test_set_trace_in_skipped_module(testdir):
    def fn():
        class SkippingPdbTest(PdbTest):
            def __init__(self, *args, **kwargs):
                kwargs["skip"] = ["testing.test_pdb"]
                super(SkippingPdbTest, self).__init__(*args, **kwargs)

                self.calls = []

            def is_skipped_module(self, module_name):
                self.calls.append(module_name)
                if len(self.calls) == 1:
                    print("is_skipped_module?", module_name)
                    ret = super(SkippingPdbTest, self).is_skipped_module(module_name)
                    assert module_name == "testing.test_pdb"
                    assert ret is True
                    return True
                return False

        set_trace(Pdb=SkippingPdbTest)  # 1
        set_trace(Pdb=SkippingPdbTest, cleanup=False)  # 2
        set_trace(Pdb=SkippingPdbTest, cleanup=False)  # 3

    check(fn, r"""
[NUM] > .*fn()
-> set_trace(Pdb=SkippingPdbTest, cleanup=False)  # 2
   5 frames hidden (try 'help hidden_frames')
# n
is_skipped_module\? testing.test_pdb
[NUM] > .*fn()
-> set_trace(Pdb=SkippingPdbTest, cleanup=False)  # 3
   5 frames hidden (try 'help hidden_frames')
# c
--Return--
[NUM] > .*fn()
-> set_trace(Pdb=SkippingPdbTest, cleanup=False)  # 3
   5 frames hidden (try 'help hidden_frames')
# c
""")


def test_exception_info_main(testdir):
    """Test that interaction adds __exception__ similar to user_exception."""
    p1 = testdir.makepyfile(
        """
        def f():
            raise ValueError("foo")

        f()
        """
    )
    testdir.monkeypatch.setenv("PDBPP_COLORS", "0")
    result = testdir.run(
        sys.executable, "-m", "pdb", str(p1),
        stdin=b"cont\nsticky\n",
    )
    result.stdout.fnmatch_lines(
        [
            '*Uncaught exception. Entering post mortem debugging',
            "*[[]5[]] > *test_exception_info_main.py(2)f()",
            "",
            "1     def f():",
            '2  ->     raise ValueError("foo")',
            "ValueError: foo",
        ]
    )


def test_interaction_no_exception():
    """Check that it does not display `None`."""
    def outer():
        try:
            raise ValueError()
        except ValueError:
            return sys.exc_info()[2]

    def fn():
        tb = outer()
        pdb_ = PdbTest()
        pdb_.reset()
        pdb_.interaction(None, tb)

    check(fn, """
[NUM] > .*outer()
-> raise ValueError()
# sticky
<CLEARSCREEN>
[0] > .*outer()

NUM         def outer():
NUM             try:
NUM  >>             raise ValueError()
NUM             except ValueError:
NUM  ->             return sys.exc_info()[2]
# q
""")