Ticket #5795: startdebuggermode.3.patch

File startdebuggermode.3.patch, 16.5 KB (added by Julian Berman, 8 years ago)
  • twisted/python/failure.py

    diff --git a/twisted/python/failure.py b/twisted/python/failure.py
    index 90d0afc..ce9b7fb 100644
    a b import sys 
    1818import linecache
    1919import inspect
    2020import opcode
     21import bdb
    2122import pdb
    2223from inspect import getmro
    2324
    class Failure: 
    170171    # throwExceptionIntoGenerator.
    171172    _yieldOpcode = chr(opcode.opmap["YIELD_VALUE"])
    172173
    173     def __init__(self, exc_value=None, exc_type=None, exc_tb=None,
    174                  captureVars=False):
     174
     175    def _realInit(self, exc_value=None, exc_type=None, exc_tb=None,
     176                  captureVars=False):
    175177        """
    176178        Initialize me with an explanation of the error.
    177179
    class Failure: 
    314316        else:
    315317            self.parents = [self.type]
    316318
     319
     320    __init__ = _realInit
     321
     322
     323    @classmethod
     324    def startDebugMode(cls, postMortem=None):
     325        """
     326        Enable debug hooks for L{Failure}s.
     327
     328        @param postMortem: a callable taking a single argument, a traceback to
     329            post-mortem. By default (or if calling the provided post mortem
     330            function fails with an exception other than L{bdb.BdbQuit}),
     331            L{pdb.post_mortem} is used.
     332        """
     333
     334        if postMortem is None:
     335            postMortem = pdb.post_mortem
     336
     337        def _debugInit(self, exc_value=None, exc_type=None, exc_tb=None,
     338                       captureVars=False):
     339            """
     340            Initialize me, possibly spawning the debugger.
     341            """
     342            if (exc_value, exc_type, exc_tb) == (None, None, None):
     343                exc = sys.exc_info()
     344                if not exc[0] == self.__class__:
     345                    try:
     346                        strrepr = str(exc[1])
     347                    except:
     348                        strrepr = "broken str"
     349                    print(
     350                        "Jumping into debugger for post-mortem of "
     351                        "exception '%s'" % (strrepr,))
     352
     353                    try:
     354                        postMortem(exc[2])
     355                    except bdb.BdbQuit:
     356                        pass
     357                    except Exception:
     358                        print(
     359                            "Debugging with %r failed. Falling back to pdb." %
     360                            (postMortem,))
     361
     362                        pdb.post_mortem(exc[2])
     363
     364            self._realInit(exc_value, exc_type, exc_tb, captureVars)
     365
     366        cls.__init__ = _debugInit
     367
     368
     369    @classmethod
     370    def stopDebugMode(cls):
     371        """
     372        Disable debug hooks for L{Failure}s.
     373        """
     374
     375        cls.__init__ = cls._realInit
     376
     377
    317378    def trap(self, *errorTypes):
    318379        """Trap this failure if its type is in a predetermined list.
    319380
    def _safeReprVars(varsDictItems): 
    627688    return [(name, reflect.safe_repr(obj)) for (name, obj) in varsDictItems]
    628689
    629690
    630 # slyphon: make post-morteming exceptions tweakable
    631 
    632 DO_POST_MORTEM = True
    633 _Failure__init__ = Failure.__init__
    634 
    635 
    636 def startDebugMode(debugger=None):
    637     """
    638     Enable debug hooks for L{Failure}s.
    639     """
    640 
    641     if debugger is None:
    642         debugger = pdb
    643     post_mortem = getattr(debugger, "post_mortem", pdb.post_mortem)
    644 
    645     def _debuginit(self, exc_value=None, exc_type=None, exc_tb=None,
    646                    captureVars=False):
    647         """
    648         Initialize failure object, possibly spawning the debugger.
    649         """
    650         if (exc_value, exc_type, exc_tb) == (None, None, None):
    651             exc = sys.exc_info()
    652             if not exc[0] == self.__class__ and DO_POST_MORTEM:
    653                 try:
    654                     strrepr = str(exc[1])
    655                 except:
    656                     strrepr = "broken str"
    657                 print("Jumping into debugger for post-mortem of exception '%s':" % (strrepr,))
    658                 post_mortem(exc[2])
    659         _Failure__init__(self, exc_value, exc_type, exc_tb, captureVars)
    660 
    661     Failure.__init__ = _debuginit
    662 
    663 
    664 def stopDebugMode():
    665     """
    666     Disable debug hooks for L{Failure}s.
    667     """
    668     Failure.__init__ = _Failure__init__
     691startDebugMode = Failure.startDebugMode
     692stopDebugMode = Failure.stopDebugMode
  • twisted/scripts/trial.py

    diff --git a/twisted/scripts/trial.py b/twisted/scripts/trial.py
    index 5ffed53..fa72ae1 100644
    a b def _reporterAction(): 
    9393                               plugin.getPlugins(itrial.IReporter)])
    9494
    9595
    96 def _wrappedPdb():
    97     """
    98     Wrap an instance of C{pdb.Pdb} with readline support and load any .rcs.
    99 
    100     """
    101 
    102     dbg = pdb.Pdb()
    103     try:
    104         import readline
    105     except ImportError:
    106         print "readline module not available"
    107         sys.exc_clear()
    108     for path in ('.pdbrc', 'pdbrc'):
    109         if os.path.exists(path):
    110             try:
    111                 rcFile = file(path, 'r')
    112             except IOError:
    113                 sys.exc_clear()
    114             else:
    115                 dbg.rcLines.extend(rcFile.readlines())
    116     return dbg
    117 
    118 
    11996class _BasicOptions(object):
    12097    """
    12198    Basic options shared between trial and its local workers.
    class _BasicOptions(object): 
    230207
    231208
    232209    def opt_debugger(self, debugger):
    233         """
    234         Load a debugger.
    235         """
    236         if debugger == 'pdb':
    237             self["debugger"] = _wrappedPdb()
    238             return
    239 
    240210        try:
    241211            self["debugger"] = reflect.namedAny(debugger)
    242212        except reflect.ModuleNotFound:
    class Options(_BasicOptions, usage.Options, app.ReactorSelectionMixin): 
    358328                ]
    359329
    360330    optParameters = [
    361         ["debugger", None, _wrappedPdb(),
     331        ["debugger", None, pdb,
    362332         "the fully qualified name of a debugger to use if --debug is passed"],
    363333        ["logfile", "l", "test.log", "log file name"],
    364334        ["jobs", "j", None, "Number of local workers to run"]
    class Options(_BasicOptions, usage.Options, app.ReactorSelectionMixin): 
    416386                if self[option]:
    417387                    raise usage.UsageError(
    418388                        "You can't specify --%s when using --jobs" % option)
    419         if self['nopm']:
    420             if not self['debug']:
    421                 raise usage.UsageError("You must specify --debug when using "
    422                                        "--nopm ")
    423             failure.DO_POST_MORTEM = False
    424389
    425390
    426391
    427392def _initialDebugSetup(config):
    428393    # do this part of debug setup first for easy debugging of import failures
    429     if config['debug']:
    430         failure.startDebugMode(debugger=config["debugger"])
     394    if config['debug'] and not config['nopm']:
     395        debugger = config['debugger']
     396        postMortem = getattr(debugger, "post_mortem", pdb.post_mortem)
     397        failure.startDebugMode(postMortem)
    431398    if config['debug'] or config['debug-stacktraces']:
    432399        defer.setDebugging(True)
    433400
  • twisted/test/test_failure.py

    diff --git a/twisted/test/test_failure.py b/twisted/test/test_failure.py
    index 69f8e23..b55507b 100644
    a b from __future__ import division, absolute_import 
    1010import re
    1111import sys
    1212import traceback
     13import bdb
    1314import pdb
    1415import linecache
    1516
    from twisted.python.compat import NativeStringIO, _PY3 
    1718from twisted.python import _reflectpy3 as reflect
    1819from twisted.python import failure
    1920
     21from twisted.trial.test.test_runner import CapturingDebugger
    2022from twisted.trial.unittest import SynchronousTestCase
    2123
    2224
    class TestFrameAttributes(SynchronousTestCase): 
    751753
    752754
    753755
    754 class StubDebugger(object):
    755     """
    756     Used to test L{Failure.startDebugMode}.
    757     """
    758     def __init__(self):
    759         self.mortems = []
    760         self.post_mortem = self.mortems.append
    761 
    762 
    763 
    764756class TestDebugMode(SynchronousTestCase):
    765757    """
    766758    Failure's debug mode should allow jumping into a debugger.
    767759    """
    768760
    769761    def setUp(self):
    770         self.debugger = StubDebugger()
     762        self.debugger = CapturingDebugger()
     763
    771764
    772765    def tearDown(self):
    773766        failure.stopDebugMode()
    774767
     768
    775769    def test_regularFailure(self):
    776770        """
    777         If L{Failure.startDebugMode} is called, instantiating L{Failure} will
    778         first call the debugger's C{post_mortem} method with the traceback.
     771        If L{Failure.startDebugMode} is called with a C{postMortem} function,
     772        instantiating L{Failure} will first call the post mortem function with
     773        the traceback.
    779774        """
    780         failure.startDebugMode(self.debugger)
     775        failure.startDebugMode(self.debugger.post_mortem)
    781776
    782777        try:
    783778            raise ZeroDivisionError()
    class TestDebugMode(SynchronousTestCase): 
    793788        If L{Failure.startDebugMode} is called, instantiating L{Failure} with
    794789        C{captureVars} still works.
    795790        """
    796         failure.startDebugMode(self.debugger)
     791        failure.startDebugMode(self.debugger.post_mortem)
    797792
    798793        try:
    799794            raise ZeroDivisionError()
    class TestDebugMode(SynchronousTestCase): 
    820815        self.assertEqual(self.debugger.mortems, [tb])
    821816
    822817
    823     def test_missingPostMortem(self):
     818    def test_postMortemQuit(self):
    824819        """
    825         If L{Failure.startDebugMode} is called with a debugger without a
    826         C{post_mortem}, method L{pdb} is used.
     820        If L{Failure.startDebugMode} is called with a post mortem function that
     821        quit via L{bdb.BdbQuit}, it does not fall back to L{pdb.post_mortem}.
     822        """
     823
     824        def postMortem(tb):
     825            raise bdb.BdbQuit()
     826
     827        self.patch(pdb, "post_mortem", self.debugger.post_mortem)
     828        failure.startDebugMode(postMortem)
     829
     830        try:
     831            raise ZeroDivisionError()
     832        except:
     833            _, _, tb = sys.exc_info()
     834            f = failure.Failure()
     835        self.assertEqual(self.debugger.mortems, [])
     836
     837
     838    def test_postMortemRaisedException(self):
     839        """
     840        If L{Failure.startDebugMode} is called with a post mortem function that
     841        raised an exception, it falls back to L{pdb.post_mortem}.
    827842        """
    828843        self.patch(pdb, "post_mortem", self.debugger.post_mortem)
    829844        failure.startDebugMode(object())
    class TestDebugMode(SynchronousTestCase): 
    836851        self.assertEqual(self.debugger.mortems, [tb])
    837852
    838853
     854    def test_stopDebugMode(self):
     855        failure.startDebugMode(self.debugger.post_mortem)
     856        failure.stopDebugMode()
     857
     858        try:
     859            raise ZeroDivisionError()
     860        except:
     861            _, _, tb = sys.exc_info()
     862            f = failure.Failure()
     863        self.assertEqual(self.debugger.mortems, [])
     864
     865
    839866
    840867class ExtendedGeneratorTests(SynchronousTestCase):
    841868    """
  • twisted/trial/test/test_runner.py

    diff --git a/twisted/trial/test/test_runner.py b/twisted/trial/test/test_runner.py
    index f0a2fef..8dfc75f 100644
    a b pyunit = __import__('unittest') 
    2323
    2424
    2525class CapturingDebugger(object):
     26    """
     27    Debugger that keeps a log of all actions performed on it.
     28    """
    2629
    2730    def __init__(self):
    28         self._calls = []
     31        self.calls = []
     32        self.mortems = []
    2933
    30     def runcall(self, *args, **kwargs):
    31         self._calls.append('runcall')
    32         args[0](*args[1:], **kwargs)
     34
     35    def post_mortem(self, tb):
     36        self.mortems.append(tb)
     37
     38
     39    def runcall(self, fn, *args, **kwargs):
     40        self.calls.append((args, kwargs))
     41        fn(*args, **kwargs)
     42
     43
     44    @classmethod
     45    def createAndCleanup(cls, test, attr):
     46        """
     47        Create a debugger at the given C{attr} and remove it after the test.
     48
     49        Exists because command line arguments like C{--debugger} will not have
     50        access to the instance, they need to find a debugger by fully qualified
     51        name on the class.
     52
     53        @param test: a test case instance where the debugger will live
     54        @param attr: a L{str} which is the name of the attribute to set and
     55            unset after the test has run. The attribute will be set on the
     56            I{class} of the given test case instance.
     57
     58        """
     59
     60        debugger = cls()
     61        setattr(test.__class__, attr, debugger)
     62        test.addCleanup(delattr, test.__class__, attr)
    3363
    3464
    3565
    class TestRunner(unittest.SynchronousTestCase): 
    542572        self.assertTrue(self.runcall_called)
    543573
    544574
    545     cdebugger = CapturingDebugger()
    546 
    547 
    548575    def test_runnerDebugger(self):
    549576        """
    550577        Trial uses specified debugger if the debugger is available.
    551578        """
     579
     580        CapturingDebugger.createAndCleanup(self, "capturingDebugger")
     581
    552582        self.parseOptions([
    553583            '--reporter', 'capturing',
    554584            '--debugger',
    555             'twisted.trial.test.test_runner.TestRunner.cdebugger',
     585            'twisted.trial.test.test_runner.TestRunner.capturingDebugger',
    556586            '--debug',
    557587            'twisted.trial.test.sample',
    558588        ])
  • twisted/trial/test/test_script.py

    diff --git a/twisted/trial/test/test_script.py b/twisted/trial/test/test_script.py
    index e8280a5..a93674e 100644
    a b  
    11# Copyright (c) Twisted Matrix Laboratories.
    22# See LICENSE for details.
    33
     4import StringIO
    45import gc
    5 import StringIO, sys, types
     6import pdb
     7import sys
     8import types
    69
    710from twisted.trial import unittest
    811from twisted.trial.runner import (
    from twisted.python.usage import UsageError 
    1417from twisted.python.filepath import FilePath
    1518
    1619from twisted.trial.test.test_loader import testNames
     20from twisted.trial.test.test_runner import CapturingDebugger
    1721
    1822pyunit = __import__('unittest')
    1923
    class MakeRunnerTestCase(unittest.TestCase): 
    571575            ["--debug", "--debugger", "doNotFind"])
    572576
    573577
     578
    574579class TestRun(unittest.TestCase):
    575580    """
    576581    Tests for the L{run} function.
    class TestRun(unittest.TestCase): 
    587592    def test_setsUpFailureDebugMode(self):
    588593        """
    589594        When a debug mode is enabled, L{failure.startDebugMode} is called with
    590         the provided debugger.
     595        the provided debugger's C{post_mortem} method.
    591596
    592597        """
    593598
     599        CapturingDebugger.createAndCleanup(self, "capturingDebugger")
     600
    594601        self.argv.extend(
    595602            [
    596603                "--debug",
    597604                "--debugger",
    598                 "twisted.trial.test.test_runner.TestRunner.cdebugger",
     605                "twisted.trial.test.test_script.TestRun.capturingDebugger",
    599606            ],
    600607        )
    601608
    602         def recordDebugger(debugger):
    603             self.debugger = debugger
    604         self.patch(failure, "startDebugMode", recordDebugger)
     609        def recordPostMortem(postMortem):
     610            self.postMortem = postMortem
     611        self.patch(failure, "startDebugMode", recordPostMortem)
    605612
    606613        try:
    607614            trial.run()
    608615        except SystemExit:
    609616            pass
    610617
    611         from twisted.trial.test.test_runner import TestRunner
    612         self.assertEqual(self.debugger, TestRunner.cdebugger)
     618        self.assertEqual(self.postMortem, self.capturingDebugger.post_mortem)
     619
     620
     621    def test_noPostMortemMethod(self):
     622        """
     623        When a debug mode is enabled and the provided debugger lacks a
     624        C{post_mortem} method, L{pdb.post_method} is used.
     625
     626        """
     627
     628        CapturingDebugger.createAndCleanup(self, "capturingDebugger")
     629        postMortem = CapturingDebugger.post_mortem
     630        del CapturingDebugger.post_mortem
     631        self.addCleanup(setattr, CapturingDebugger, "post_mortem", postMortem)
     632
     633        self.argv.extend(
     634            [
     635                "--debug",
     636                "--debugger",
     637                "twisted.trial.test.test_script.TestRun.capturingDebugger",
     638            ],
     639        )
     640
     641        def recordPostMortem(postMortem):
     642            self.postMortem = postMortem
     643        self.patch(failure, "startDebugMode", recordPostMortem)
     644
     645        try:
     646            trial.run()
     647        except SystemExit:
     648            pass
     649
     650        self.assertEqual(self.postMortem, pdb.post_mortem)
     651
     652
     653    def test_nopm(self):
     654        """
     655        When C{--nopm} is passed, L{failure.startDebugMode} is not called so
     656        that L{Failure} objects do not automatically invoke a call to post
     657        mortem.
     658
     659        """
     660
     661        CapturingDebugger.createAndCleanup(self, "capturingDebugger")
     662
     663        self.argv.extend(
     664            [
     665                "--debug",
     666                "--debugger",
     667                "twisted.trial.test.test_script.TestRun.capturingDebugger",
     668                "--nopm",
     669            ],
     670        )
     671
     672        def startDebugMode(postMortem):
     673            self.fail("startDebugMode should not have been called!")
     674        self.patch(failure, "startDebugMode", startDebugMode)
     675
     676        try:
     677            trial.run()
     678        except SystemExit:
     679            pass
    613680
    614681
    615682    def test_debuggerNotFound(self):