test_debugger.py
579 lines
| 14.3 KiB
| text/x-python
|
PythonLexer
|
r7087 | """Tests for debugging machinery. | ||
""" | ||||
|
r22742 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r7087 | |||
|
r25839 | import builtins | ||
import os | ||||
|
r7929 | import sys | ||
|
r27325 | import platform | ||
|
r26824 | |||
|
r25569 | from tempfile import NamedTemporaryFile | ||
|
r25839 | from textwrap import dedent | ||
|
r25606 | from unittest.mock import patch | ||
|
r7929 | |||
|
r7087 | from IPython.core import debugger | ||
|
r25839 | from IPython.testing import IPYTHON_TESTING_TIMEOUT_SCALE | ||
from IPython.testing.decorators import skip_win32 | ||||
|
r27325 | import pytest | ||
|
r7087 | |||
#----------------------------------------------------------------------------- | ||||
|
r7929 | # Helper classes, from CPython's Pdb test suite | ||
#----------------------------------------------------------------------------- | ||||
class _FakeInput(object): | ||||
""" | ||||
A fake input stream for pdb's interactive debugger. Whenever a | ||||
line is read, print it (to simulate the user typing it), and then | ||||
return it. The set of lines to return is specified in the | ||||
constructor; they should not have trailing newlines. | ||||
""" | ||||
def __init__(self, lines): | ||||
self.lines = iter(lines) | ||||
def readline(self): | ||||
line = next(self.lines) | ||||
|
r13348 | print(line) | ||
|
r7929 | return line+'\n' | ||
class PdbTestInput(object): | ||||
"""Context manager that makes testing Pdb in doctests easier.""" | ||||
def __init__(self, input): | ||||
self.input = input | ||||
def __enter__(self): | ||||
self.real_stdin = sys.stdin | ||||
sys.stdin = _FakeInput(self.input) | ||||
def __exit__(self, *exc): | ||||
sys.stdin = self.real_stdin | ||||
#----------------------------------------------------------------------------- | ||||
|
r7087 | # Tests | ||
#----------------------------------------------------------------------------- | ||||
|
r7929 | def test_ipdb_magics(): | ||
'''Test calling some IPython magics from ipdb. | ||||
First, set up some test functions and classes which we can inspect. | ||||
>>> class ExampleClass(object): | ||||
... """Docstring for ExampleClass.""" | ||||
... def __init__(self): | ||||
... """Docstring for ExampleClass.__init__""" | ||||
... pass | ||||
... def __str__(self): | ||||
... return "ExampleClass()" | ||||
>>> def example_function(x, y, z="hello"): | ||||
... """Docstring for example_function.""" | ||||
... pass | ||||
|
r12286 | >>> old_trace = sys.gettrace() | ||
|
r7929 | Create a function which triggers ipdb. | ||
>>> def trigger_ipdb(): | ||||
... a = ExampleClass() | ||||
... debugger.Pdb().set_trace() | ||||
>>> with PdbTestInput([ | ||||
... 'pdef example_function', | ||||
... 'pdoc ExampleClass', | ||||
|
r21851 | ... 'up', | ||
... 'down', | ||||
... 'list', | ||||
|
r7929 | ... 'pinfo a', | ||
|
r21851 | ... 'll', | ||
|
r7929 | ... 'continue', | ||
... ]): | ||||
... trigger_ipdb() | ||||
--Return-- | ||||
None | ||||
> <doctest ...>(3)trigger_ipdb() | ||||
1 def trigger_ipdb(): | ||||
2 a = ExampleClass() | ||||
----> 3 debugger.Pdb().set_trace() | ||||
<BLANKLINE> | ||||
ipdb> pdef example_function | ||||
example_function(x, y, z='hello') | ||||
ipdb> pdoc ExampleClass | ||||
|
r15712 | Class docstring: | ||
|
r7929 | Docstring for ExampleClass. | ||
|
r15712 | Init docstring: | ||
|
r7929 | Docstring for ExampleClass.__init__ | ||
|
r21851 | ipdb> up | ||
> <doctest ...>(11)<module>() | ||||
|
r21945 | 7 'pinfo a', | ||
8 'll', | ||||
|
r21851 | 9 'continue', | ||
10 ]): | ||||
---> 11 trigger_ipdb() | ||||
<BLANKLINE> | ||||
ipdb> down | ||||
None | ||||
> <doctest ...>(3)trigger_ipdb() | ||||
1 def trigger_ipdb(): | ||||
2 a = ExampleClass() | ||||
----> 3 debugger.Pdb().set_trace() | ||||
<BLANKLINE> | ||||
ipdb> list | ||||
1 def trigger_ipdb(): | ||||
2 a = ExampleClass() | ||||
----> 3 debugger.Pdb().set_trace() | ||||
<BLANKLINE> | ||||
|
r7929 | ipdb> pinfo a | ||
|
r15711 | Type: ExampleClass | ||
|
r15712 | String form: ExampleClass() | ||
|
r15711 | Namespace: Local... | ||
Docstring: Docstring for ExampleClass. | ||||
|
r15712 | Init docstring: Docstring for ExampleClass.__init__ | ||
|
r21851 | ipdb> ll | ||
1 def trigger_ipdb(): | ||||
2 a = ExampleClass() | ||||
----> 3 debugger.Pdb().set_trace() | ||||
<BLANKLINE> | ||||
|
r7929 | ipdb> continue | ||
|
r12286 | |||
Restore previous trace function, e.g. for coverage.py | ||||
>>> sys.settrace(old_trace) | ||||
|
r7929 | ''' | ||
|
r8929 | |||
|
r11124 | def test_ipdb_magics2(): | ||
|
r8929 | '''Test ipdb with a very short function. | ||
|
r12286 | |||
>>> old_trace = sys.gettrace() | ||||
|
r8929 | |||
>>> def bar(): | ||||
... pass | ||||
Run ipdb. | ||||
>>> with PdbTestInput([ | ||||
... 'continue', | ||||
... ]): | ||||
... debugger.Pdb().runcall(bar) | ||||
> <doctest ...>(2)bar() | ||||
1 def bar(): | ||||
----> 2 pass | ||||
<BLANKLINE> | ||||
ipdb> continue | ||||
|
r12286 | |||
Restore previous trace function, e.g. for coverage.py | ||||
>>> sys.settrace(old_trace) | ||||
|
r8929 | ''' | ||
|
r22569 | |||
def can_quit(): | ||||
'''Test that quit work in ipydb | ||||
>>> old_trace = sys.gettrace() | ||||
>>> def bar(): | ||||
... pass | ||||
>>> with PdbTestInput([ | ||||
... 'quit', | ||||
... ]): | ||||
... debugger.Pdb().runcall(bar) | ||||
> <doctest ...>(2)bar() | ||||
1 def bar(): | ||||
----> 2 pass | ||||
<BLANKLINE> | ||||
ipdb> quit | ||||
Restore previous trace function, e.g. for coverage.py | ||||
>>> sys.settrace(old_trace) | ||||
''' | ||||
def can_exit(): | ||||
'''Test that quit work in ipydb | ||||
>>> old_trace = sys.gettrace() | ||||
>>> def bar(): | ||||
... pass | ||||
>>> with PdbTestInput([ | ||||
... 'exit', | ||||
... ]): | ||||
... debugger.Pdb().runcall(bar) | ||||
> <doctest ...>(2)bar() | ||||
1 def bar(): | ||||
----> 2 pass | ||||
<BLANKLINE> | ||||
ipdb> exit | ||||
Restore previous trace function, e.g. for coverage.py | ||||
>>> sys.settrace(old_trace) | ||||
''' | ||||
|
r25569 | |||
def test_interruptible_core_debugger(): | ||||
|
r25587 | """The debugger can be interrupted. | ||
|
r25606 | |||
The presumption is there is some mechanism that causes a KeyboardInterrupt | ||||
(this is implemented in ipykernel). We want to ensure the | ||||
KeyboardInterrupt cause debugging to cease. | ||||
|
r25587 | """ | ||
|
r25649 | def raising_input(msg="", called=[0]): | ||
called[0] += 1 | ||||
|
r27087 | assert called[0] == 1, "input() should only be called once!" | ||
raise KeyboardInterrupt() | ||||
|
r25606 | |||
|
r26957 | tracer_orig = sys.gettrace() | ||
try: | ||||
with patch.object(builtins, "input", raising_input): | ||||
debugger.InterruptiblePdb().set_trace() | ||||
# The way this test will fail is by set_trace() never exiting, | ||||
# resulting in a timeout by the test runner. The alternative | ||||
# implementation would involve a subprocess, but that adds issues | ||||
# with interrupting subprocesses that are rather complex, so it's | ||||
# simpler just to do it this way. | ||||
finally: | ||||
# restore the original trace function | ||||
sys.settrace(tracer_orig) | ||||
|
r25839 | |||
@skip_win32 | ||||
def test_xmode_skip(): | ||||
"""that xmode skip frames | ||||
Not as a doctest as pytest does not run doctests. | ||||
""" | ||||
import pexpect | ||||
env = os.environ.copy() | ||||
env["IPY_TEST_SIMPLE_PROMPT"] = "1" | ||||
child = pexpect.spawn( | ||||
sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env | ||||
) | ||||
child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE | ||||
child.expect("IPython") | ||||
child.expect("\n") | ||||
child.expect_exact("In [1]") | ||||
block = dedent( | ||||
""" | ||||
|
r26594 | def f(): | ||
__tracebackhide__ = True | ||||
g() | ||||
|
r25839 | |||
|
r26594 | def g(): | ||
raise ValueError | ||||
|
r25839 | |||
|
r26594 | f() | ||
|
r25839 | """ | ||
) | ||||
for line in block.splitlines(): | ||||
child.sendline(line) | ||||
child.expect_exact(line) | ||||
child.expect_exact("skipping") | ||||
block = dedent( | ||||
""" | ||||
|
r26594 | def f(): | ||
__tracebackhide__ = True | ||||
g() | ||||
|
r25839 | |||
|
r26594 | def g(): | ||
from IPython.core.debugger import set_trace | ||||
set_trace() | ||||
|
r25839 | |||
|
r26594 | f() | ||
|
r25839 | """ | ||
) | ||||
for line in block.splitlines(): | ||||
child.sendline(line) | ||||
child.expect_exact(line) | ||||
child.expect("ipdb>") | ||||
child.sendline("w") | ||||
child.expect("hidden") | ||||
child.expect("ipdb>") | ||||
child.sendline("skip_hidden false") | ||||
child.sendline("w") | ||||
child.expect("__traceba") | ||||
child.expect("ipdb>") | ||||
child.close() | ||||
|
r26594 | |||
|
r26810 | skip_decorators_blocks = ( | ||
""" | ||||
|
r26824 | def helpers_helper(): | ||
pass # should not stop here except breakpoint | ||||
""", | ||||
""" | ||||
|
r26810 | def helper_1(): | ||
|
r26824 | helpers_helper() # should not stop here | ||
|
r26810 | """, | ||
""" | ||||
def helper_2(): | ||||
pass # should not stop here | ||||
""", | ||||
""" | ||||
|
r26824 | def pdb_skipped_decorator2(function): | ||
def wrapped_fn(*args, **kwargs): | ||||
__debuggerskip__ = True | ||||
helper_2() | ||||
__debuggerskip__ = False | ||||
result = function(*args, **kwargs) | ||||
__debuggerskip__ = True | ||||
helper_2() | ||||
return result | ||||
return wrapped_fn | ||||
""", | ||||
""" | ||||
|
r26810 | def pdb_skipped_decorator(function): | ||
def wrapped_fn(*args, **kwargs): | ||||
__debuggerskip__ = True | ||||
helper_1() | ||||
__debuggerskip__ = False | ||||
result = function(*args, **kwargs) | ||||
__debuggerskip__ = True | ||||
helper_2() | ||||
return result | ||||
return wrapped_fn | ||||
""", | ||||
""" | ||||
@pdb_skipped_decorator | ||||
|
r26824 | @pdb_skipped_decorator2 | ||
|
r26810 | def bar(x, y): | ||
return x * y | ||||
""", | ||||
"""import IPython.terminal.debugger as ipdb""", | ||||
""" | ||||
def f(): | ||||
ipdb.set_trace() | ||||
bar(3, 4) | ||||
""", | ||||
""" | ||||
f() | ||||
""", | ||||
) | ||||
def _decorator_skip_setup(): | ||||
import pexpect | ||||
env = os.environ.copy() | ||||
env["IPY_TEST_SIMPLE_PROMPT"] = "1" | ||||
|
r27691 | env["PROMPT_TOOLKIT_NO_CPR"] = "1" | ||
|
r26810 | |||
child = pexpect.spawn( | ||||
sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env | ||||
) | ||||
|
r27235 | child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE | ||
|
r26810 | |||
child.expect("IPython") | ||||
child.expect("\n") | ||||
|
r27235 | child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE | ||
|
r27690 | child.str_last_chars = 500 | ||
|
r27235 | |||
|
r26810 | dedented_blocks = [dedent(b).strip() for b in skip_decorators_blocks] | ||
in_prompt_number = 1 | ||||
for cblock in dedented_blocks: | ||||
child.expect_exact(f"In [{in_prompt_number}]:") | ||||
in_prompt_number += 1 | ||||
for line in cblock.splitlines(): | ||||
child.sendline(line) | ||||
child.expect_exact(line) | ||||
child.sendline("") | ||||
return child | ||||
|
r27695 | @pytest.mark.skip(reason="recently fail for unknown reason on CI") | ||
|
r26810 | @skip_win32 | ||
def test_decorator_skip(): | ||||
"""test that decorator frames can be skipped.""" | ||||
child = _decorator_skip_setup() | ||||
|
r27694 | child.expect_exact("ipython-input-8") | ||
|
r26810 | child.expect_exact("3 bar(3, 4)") | ||
child.expect("ipdb>") | ||||
child.expect("ipdb>") | ||||
child.sendline("step") | ||||
child.expect_exact("step") | ||||
|
r27694 | child.expect_exact("--Call--") | ||
child.expect_exact("ipython-input-6") | ||||
|
r26810 | |||
child.expect_exact("1 @pdb_skipped_decorator") | ||||
child.sendline("s") | ||||
child.expect_exact("return x * y") | ||||
child.close() | ||||
|
r27695 | @pytest.mark.skip(reason="recently fail for unknown reason on CI") | ||
|
r27357 | @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="issues on PyPy") | ||
|
r26810 | @skip_win32 | ||
def test_decorator_skip_disabled(): | ||||
"""test that decorator frame skipping can be disabled""" | ||||
child = _decorator_skip_setup() | ||||
child.expect_exact("3 bar(3, 4)") | ||||
for input_, expected in [ | ||||
("skip_predicates debuggerskip False", ""), | ||||
("skip_predicates", "debuggerskip : False"), | ||||
("step", "---> 2 def wrapped_fn"), | ||||
("step", "----> 3 __debuggerskip__"), | ||||
("step", "----> 4 helper_1()"), | ||||
("step", "---> 1 def helper_1():"), | ||||
|
r26824 | ("next", "----> 2 helpers_helper()"), | ||
|
r26810 | ("next", "--Return--"), | ||
("next", "----> 5 __debuggerskip__ = False"), | ||||
]: | ||||
child.expect("ipdb>") | ||||
child.sendline(input_) | ||||
child.expect_exact(input_) | ||||
child.expect_exact(expected) | ||||
child.close() | ||||
|
r28798 | @pytest.mark.xfail( | ||
sys.version_info.releaselevel not in ("final", "candidate"), | ||||
reason="fails on 3.13.dev", | ||||
|
r28800 | strict=True, | ||
|
r28798 | ) | ||
|
r27357 | @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="issues on PyPy") | ||
|
r26596 | @skip_win32 | ||
|
r26824 | def test_decorator_skip_with_breakpoint(): | ||
"""test that decorator frame skipping can be disabled""" | ||||
import pexpect | ||||
env = os.environ.copy() | ||||
env["IPY_TEST_SIMPLE_PROMPT"] = "1" | ||||
|
r27692 | env["PROMPT_TOOLKIT_NO_CPR"] = "1" | ||
|
r26824 | |||
child = pexpect.spawn( | ||||
sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env | ||||
) | ||||
|
r27235 | child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE | ||
|
r27692 | child.str_last_chars = 500 | ||
|
r26824 | |||
child.expect("IPython") | ||||
child.expect("\n") | ||||
|
r27235 | child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE | ||
|
r26824 | ### we need a filename, so we need to exec the full block with a filename | ||
with NamedTemporaryFile(suffix=".py", dir=".", delete=True) as tf: | ||||
name = tf.name[:-3].split("/")[-1] | ||||
tf.write("\n".join([dedent(x) for x in skip_decorators_blocks[:-1]]).encode()) | ||||
tf.flush() | ||||
codeblock = f"from {name} import f" | ||||
dedented_blocks = [ | ||||
codeblock, | ||||
"f()", | ||||
] | ||||
in_prompt_number = 1 | ||||
for cblock in dedented_blocks: | ||||
child.expect_exact(f"In [{in_prompt_number}]:") | ||||
in_prompt_number += 1 | ||||
for line in cblock.splitlines(): | ||||
child.sendline(line) | ||||
child.expect_exact(line) | ||||
child.sendline("") | ||||
# as the filename does not exists, we'll rely on the filename prompt | ||||
child.expect_exact("47 bar(3, 4)") | ||||
for input_, expected in [ | ||||
(f"b {name}.py:3", ""), | ||||
("step", "1---> 3 pass # should not stop here except"), | ||||
("step", "---> 38 @pdb_skipped_decorator"), | ||||
("continue", ""), | ||||
]: | ||||
child.expect("ipdb>") | ||||
child.sendline(input_) | ||||
child.expect_exact(input_) | ||||
child.expect_exact(expected) | ||||
child.close() | ||||
@skip_win32 | ||||
|
r26594 | def test_where_erase_value(): | ||
|
r26595 | """Test that `where` does not access f_locals and erase values.""" | ||
|
r26594 | import pexpect | ||
env = os.environ.copy() | ||||
env["IPY_TEST_SIMPLE_PROMPT"] = "1" | ||||
child = pexpect.spawn( | ||||
sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env | ||||
) | ||||
child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE | ||||
child.expect("IPython") | ||||
child.expect("\n") | ||||
child.expect_exact("In [1]") | ||||
block = dedent( | ||||
""" | ||||
def simple_f(): | ||||
myvar = 1 | ||||
print(myvar) | ||||
1/0 | ||||
print(myvar) | ||||
simple_f() """ | ||||
) | ||||
for line in block.splitlines(): | ||||
child.sendline(line) | ||||
child.expect_exact(line) | ||||
child.expect_exact("ZeroDivisionError") | ||||
child.expect_exact("In [2]:") | ||||
child.sendline("%debug") | ||||
## | ||||
child.expect("ipdb>") | ||||
child.sendline("myvar") | ||||
child.expect("1") | ||||
## | ||||
child.expect("ipdb>") | ||||
child.sendline("myvar = 2") | ||||
## | ||||
child.expect_exact("ipdb>") | ||||
child.sendline("myvar") | ||||
child.expect_exact("2") | ||||
## | ||||
child.expect("ipdb>") | ||||
child.sendline("where") | ||||
## | ||||
child.expect("ipdb>") | ||||
child.sendline("myvar") | ||||
child.expect_exact("2") | ||||
child.expect("ipdb>") | ||||
child.close() | ||||