diff --git a/Lib/bdb.py b/Lib/bdb.py --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -70,7 +70,10 @@ # XXX 'arg' is no longer used if self.botframe is None: # First call of dispatch since reset() - self.botframe = frame.f_back # (CT) Note that this may also be None! + # Note that frame may be a debugger frame. The derived class must + # ensure that self.botframe is a debuggee frame and not a debugger + # frame. + self.botframe = frame return self.trace_dispatch if not (self.stop_here(frame) or self.break_anywhere(frame)): # No need to trace this function diff --git a/Lib/pdb.py b/Lib/pdb.py --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -193,9 +193,10 @@ raise KeyboardInterrupt self.message("\nProgram interrupted. (Use 'cont' to resume).") self.set_step() - self.set_trace(frame) # restore previous signal handler signal.signal(signal.SIGINT, self._previous_sigint_handler) + frame.f_trace = self.trace_dispatch + sys.settrace(self.trace_dispatch) def reset(self): bdb.Bdb.reset(self) @@ -261,6 +262,7 @@ if (self.mainpyfile != self.canonic(frame.f_code.co_filename) or frame.f_lineno <= 0): return + self.botframe = frame self._wait_for_mainpyfile = False if self.bp_commands(frame): self.interaction(frame, None) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3,14 +3,20 @@ import imp import pdb import sys +import signal import unittest import subprocess import textwrap +try: + import threading +except ImportError: + threading = None from test import support # This little helper class is essential for testing pdb under doctest. from test.test_doctest import _FakeInput +mswindows = (sys.platform == "win32") class PdbTestInput(object): """Context manager that makes testing Pdb in doctests easier.""" @@ -599,7 +605,7 @@ class PdbTestCase(unittest.TestCase): - def run_pdb(self, script, commands): + def run_pdb(self, script, commands, sigint=False): """Run 'script' lines with pdb and the pdb 'commands'.""" filename = 'main.py' with open(filename, 'w') as f: @@ -611,6 +617,14 @@ stdin=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: + # Interrupt the spawned process. + if threading and sigint: + def kill(): + # Wait for the subprocess to be fully started before + # sending the signal. + import time; time.sleep(1) + proc.send_signal(signal.SIGINT) + threading.Thread(target=kill).start() stdout, stderr = proc.communicate(str.encode(commands)) stdout = stdout and bytes.decode(stdout) stderr = stderr and bytes.decode(stderr) @@ -667,6 +681,57 @@ any('main.py(5)foo()->None' in l for l in stdout.splitlines()), 'Fail to step into the caller after a return') + def test_issue_14743_1(self): + script = "x = 1" + commands = """ + step + step + quit + """ + stdout, stderr = self.run_pdb(script, commands) + self.assertTrue( + any('The program finished and will be restarted' + in l for l in stdout.splitlines()), + 'Debugging session not terminated after returning from initial' + ' user frame.') + + @unittest.skipIf(mswindows or not threading, "posix with threads test") + def test_issue_14743_2(self): + script = """ + import bar + + def foo(): + bar.bar() + + foo() + pass # for setting the breakpoint + """ + commands = """ + break 8 + continue + !i=0 + continue + step + step + quit + """ + bar = """ + import time + def bar(): + i = 1 + while i: + time.sleep(.100) + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + stdout, stderr = self.run_pdb(script, commands, sigint=True) + self.assertTrue( + any('The program finished and will be restarted' + in l for l in stdout.splitlines()), + 'Interrupted debugging session not terminated after returning' + ' from initial user frame.') + def tearDown(self): support.unlink(support.TESTFN)