diff --git a/TESTING.rst b/TESTING.rst index f5ca3bb3ee..d99696f8bf 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -184,7 +184,22 @@ overwritten during the next tox run. Post-mortem debugging ~~~~~~~~~~~~~~~~~~~~~ -Setting OS_POST_MORTEM_DEBUG=1 in the shell environment will ensure -that pdb.post_mortem() will be invoked on test failure:: +Setting OS_POST_MORTEM_DEBUGGER in the shell environment will ensure +that the debugger .post_mortem() method will be invoked on test failure:: - $ OS_POST_MORTEM_DEBUG=1 ./run_tests.sh -d [test module path] + $ OS_POST_MORTEM_DEBUGGER=pdb ./run_tests.sh -d [test module path] + +Supported debuggers are pdb, and pudb. Pudb is full-screen, console-based +visual debugger for Python which let you inspect variables, the stack, +and breakpoints in a very visual way, keeping a high degree of compatibility +with pdb:: + + $ ./.venv/bin/pip install pudb + + $ OS_POST_MORTEM_DEBUGGER=pudb ./run_tests.sh -d [test module path] + +References +========== + +.. [#pudb] PUDB debugger: + https://pypi.python.org/pypi/pudb diff --git a/neutron/tests/base.py b/neutron/tests/base.py index dae1320de5..b4bd93a4cd 100644 --- a/neutron/tests/base.py +++ b/neutron/tests/base.py @@ -73,8 +73,10 @@ class BaseTestCase(testtools.TestCase): super(BaseTestCase, self).setUp() # Configure this first to ensure pm debugging support for setUp() - if os.environ.get('OS_POST_MORTEM_DEBUG') in TRUE_STRING: - self.addOnException(post_mortem_debug.exception_handler) + debugger = os.environ.get('OS_POST_MORTEM_DEBUGGER') + if debugger: + self.addOnException(post_mortem_debug.get_exception_handler( + debugger)) if os.environ.get('OS_DEBUG') in TRUE_STRING: _level = std_logging.DEBUG diff --git a/neutron/tests/post_mortem_debug.py b/neutron/tests/post_mortem_debug.py index 70b99412b0..8e6ac61a2c 100644 --- a/neutron/tests/post_mortem_debug.py +++ b/neutron/tests/post_mortem_debug.py @@ -13,18 +13,38 @@ # License for the specific language governing permissions and limitations # under the License. -import pdb +import functools import traceback +DEFAULT_DEBUGGER = 'pdb' -def exception_handler(exc_info): + +def get_exception_handler(debugger_name=None): + debugger = _get_debugger(debugger_name or DEFAULT_DEBUGGER) + return functools.partial(_exception_handler, debugger) + + +def _get_debugger(debugger_name): + try: + debugger = __import__(debugger_name) + if 'post_mortem' in dir(debugger): + return debugger + except ImportError: + raise ValueError( + _("can't import %s module as a post mortem debugger") % + debugger_name) + raise ValueError( + _("%s is not a supported post mortem debugger") % debugger_name) + + +def _exception_handler(debugger, exc_info): """Exception handler enabling post-mortem debugging. A class extending testtools.TestCase can add this handler in setUp(): self.addOnException(post_mortem_debug.exception_handler) - When an exception occurs, the user will be dropped into a pdb + When an exception occurs, the user will be dropped into a debugger session in the execution environment of the failure. Frames associated with the testing framework are excluded so that @@ -37,7 +57,7 @@ def exception_handler(exc_info): if ignored_traceback: tb = FilteredTraceback(tb, ignored_traceback) traceback.print_exception(exc_info[0], exc_info[1], tb) - pdb.post_mortem(tb) + debugger.post_mortem(tb) def get_ignored_traceback(tb): diff --git a/neutron/tests/unit/test_post_mortem_debug.py b/neutron/tests/unit/test_post_mortem_debug.py index c42a3d68fb..3237558c17 100644 --- a/neutron/tests/unit/test_post_mortem_debug.py +++ b/neutron/tests/unit/test_post_mortem_debug.py @@ -34,13 +34,26 @@ class TestTesttoolsExceptionHandler(base.BaseTestCase): with mock.patch.object(post_mortem_debug, 'get_ignored_traceback', return_value=mock.Mock()): - post_mortem_debug.exception_handler(exc_info) + post_mortem_debug.get_exception_handler()(exc_info) # traceback will become post_mortem_debug.FilteredTraceback filtered_exc_info = (exc_info[0], exc_info[1], mock.ANY) mock_print_exception.assert_called_once_with(*filtered_exc_info) mock_post_mortem.assert_called_once_with(mock.ANY) + def test__get_debugger(self): + def import_mock(name, *args): + mod_mock = mock.Mock() + mod_mock.__name__ = name + mod_mock.post_mortem = mock.Mock() + return mod_mock + + with mock.patch('__builtin__.__import__', side_effect=import_mock): + pdb_debugger = post_mortem_debug._get_debugger('pdb') + pudb_debugger = post_mortem_debug._get_debugger('pudb') + self.assertEqual('pdb', pdb_debugger.__name__) + self.assertEqual('pudb', pudb_debugger.__name__) + class TestFilteredTraceback(base.BaseTestCase):