[cubicweb-ctl] add '--pdb' global option to launch (i)pdb on exception
authorLaurent Peuch <cortex@worlddomination.be>
Tue, 21 May 2019 16:47:13 +0200
changeset 12692 8673da7c2f85
parent 12691 3f0e64630d94
child 12693 5e7212209ea6
[cubicweb-ctl] add '--pdb' global option to launch (i)pdb on exception
cubicweb/cwctl.py
cubicweb/test/unittest_cwctl.py
doc/changes/3.27.rst
--- a/cubicweb/cwctl.py	Mon Jul 22 10:54:22 2019 +0200
+++ b/cubicweb/cwctl.py	Tue May 21 16:47:13 2019 +0200
@@ -97,6 +97,16 @@
     return [drop_prefix(cube) for cube in cwcfg.available_cubes()]
 
 
+def get_pdb():
+    try:
+        import ipdb
+    except ImportError:
+        import pdb
+        return pdb
+    else:
+        return ipdb
+
+
 class InstanceCommand(Command):
     """base class for command taking one instance id as arguments"""
     arguments = '<instance>'
@@ -111,6 +121,11 @@
           'help': 'force command without asking confirmation',
           }
          ),
+        ("pdb",
+         {'action': 'store_true', 'default': False,
+          'help': 'launch pdb on exception',
+          }
+         ),
     )
     actionverb = None
 
@@ -130,6 +145,7 @@
         except Exception as ex:
             import traceback
             traceback.print_exc()
+
             sys.stderr.write('instance %s not %s: %s\n' % (
                 appid, self.actionverb, ex))
             status = 8
@@ -138,6 +154,11 @@
             sys.stderr.write('%s aborted\n' % self.name)
             status = 2  # specific error code
 
+        if status != 0 and self.config.pdb:
+            exception_type, exception, traceback_ = sys.exc_info()
+            pdb = get_pdb()
+            pdb.post_mortem(traceback_)
+
         sys.exit(status)
 
 
--- a/cubicweb/test/unittest_cwctl.py	Mon Jul 22 10:54:22 2019 +0200
+++ b/cubicweb/test/unittest_cwctl.py	Tue May 21 16:47:13 2019 +0200
@@ -24,6 +24,7 @@
 
 from logilab.common.clcommands import CommandLine
 
+from cubicweb import cwctl
 from cubicweb.cwctl import ListCommand, InstanceCommand
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.migractions import ServerMigrationHelper
@@ -89,24 +90,64 @@
         pass
 
 
+class _TestFailCommand(InstanceCommand):
+    "I need some doc"
+    name = "test_fail"
+
+    def test_fail_instance(self, appid):
+        raise Exception()
+
+
 class InstanceCommandTest(unittest.TestCase):
-    def test_getting_called(self):
-        CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
-                            version=cw_version, check_duplicated_command=False)
+    def setUp(self):
+        self.CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
+                                 version=cw_version, check_duplicated_command=False)
         cwcfg.load_cwctl_plugins()
-        CWCTL.register(_TestCommand)
-
-        _TestCommand.test_instance = MagicMock(return_value=0)
+        self.CWCTL.register(_TestCommand)
+        self.CWCTL.register(_TestFailCommand)
 
         # pretend that this instance exists
         cwcfg.config_for = MagicMock(return_value=object())
 
+    def test_getting_called(self):
+        _TestCommand.test_instance = MagicMock(return_value=0)
+
         try:
-            CWCTL.run(["test", "some_instance"])
+            self.CWCTL.run(["test", "some_instance"])
         except SystemExit as ex:  # CWCTL will finish the program after that
             self.assertEqual(ex.code, 0)
         _TestCommand.test_instance.assert_called_with("some_instance")
 
+    @patch.object(cwctl, 'get_pdb')
+    def test_pdb_not_called(self, get_pdb):
+        try:
+            self.CWCTL.run(["test", "some_instance"])
+        except SystemExit as ex:  # CWCTL will finish the program after that
+            self.assertEqual(ex.code, 0)
+
+        get_pdb.assert_not_called()
+
+    @patch.object(cwctl, 'get_pdb')
+    def test_pdb_called(self, get_pdb):
+        post_mortem = get_pdb.return_value.post_mortem
+        try:
+            self.CWCTL.run(["test_fail", "some_instance", "--pdb"])
+        except SystemExit as ex:  # CWCTL will finish the program after that
+            self.assertEqual(ex.code, 8)
+
+        get_pdb.assert_called_once()
+        post_mortem.assert_called_once()
+
+    @patch.dict(sys.modules, ipdb=MagicMock())
+    def test_ipdb_selected_and_called(self):
+        ipdb = sys.modules['ipdb']
+        try:
+            self.CWCTL.run(["test_fail", "some_instance", "--pdb"])
+        except SystemExit as ex:  # CWCTL will finish the program after that
+            self.assertEqual(ex.code, 8)
+
+        ipdb.post_mortem.assert_called_once()
+
 
 if __name__ == '__main__':
     unittest.main()
--- a/doc/changes/3.27.rst	Mon Jul 22 10:54:22 2019 +0200
+++ b/doc/changes/3.27.rst	Tue May 21 16:47:13 2019 +0200
@@ -17,6 +17,9 @@
   exist in the instance directory, the `pyramid.ini` file will be generated
   with the needed secrets.
 
+* add a --pdb flag to all cubicweb-ctl command to launch (i)pdb if an exception
+  occurs during a command execution.
+
 Backwards incompatible changes
 ------------------------------