--- 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
------------------------------