[migration/pdb] add option to use pdb.post_mortem if traceback is provided

Post mortem is a mode where the pdb shell is opened where the exception as occured instead at the breakpoint for set_trace. This is way more useful for debugging for the user because is will have the full context of the error.

Closes #17219827

authorLaurent Peuch <cortex@worlddomination.be>
changesetcc681b6fcffa
branchdefault
phasepublic
hiddenno
parent revision#19aef4729d45 [migration/shell] select ipdb if present on (d)ebug mode
child revision#5c432a7fc442 [migration/pdb] display traceback instead of only the exception for easier debugging
files modified by this revision
cubicweb/migration.py
cubicweb/server/migractions.py
cubicweb/test/unittest_migration.py
# HG changeset patch
# User Laurent Peuch <cortex@worlddomination.be>
# Date 1558537806 -7200
# Wed May 22 17:10:06 2019 +0200
# Node ID cc681b6fcffab8fea64b33fa2736769ed6276797
# Parent 19aef4729d45ed4cfebe5c4e4837c3f8a23a2125
[migration/pdb] add option to use pdb.post_mortem if traceback is provided

Post mortem is a mode where the pdb shell is opened **where** the exception as
occured instead at the breakpoint for set_trace. This is way more useful for
debugging for the user because is will have the full context of the error.

Closes #17219827

diff --git a/cubicweb/migration.py b/cubicweb/migration.py
@@ -198,11 +198,11 @@
1              ask_confirm = True
2          if not ask_confirm or self.confirm(msg):
3              return meth(*args, **kwargs)
4 
5      def confirm(self, question, # pylint: disable=E0202
6 -                shell=True, abort=True, retry=False, pdb=False, default='y'):
7 +                shell=True, abort=True, retry=False, pdb=False, default='y', traceback=None):
8          """ask for confirmation and return true on positive answer
9 
10          if `retry` is true the r[etry] answer may return 2
11          """
12          possibleanswers = ['y', 'n']
@@ -224,15 +224,18 @@
13              return 2
14          if answer == 'abort':
15              raise SystemExit(1)
16          if answer == 'shell':
17              self.interactive_shell()
18 -            return self.confirm(question, shell, abort, retry, pdb, default)
19 +            return self.confirm(question, shell, abort, retry, pdb, default, traceback)
20          if answer == 'pdb':
21              pdb = utils.get_pdb()
22 -            pdb.set_trace()
23 -            return self.confirm(question, shell, abort, retry, pdb, default)
24 +            if traceback:
25 +                pdb.post_mortem(traceback)
26 +            else:
27 +                pdb.set_trace()
28 +            return self.confirm(question, shell, abort, retry, pdb, default, traceback)
29          return True
30 
31      def interactive_shell(self):
32          self.confirm = yes
33          self.need_wrap = False
diff --git a/cubicweb/server/migractions.py b/cubicweb/server/migractions.py
@@ -260,13 +260,14 @@
34                      format = written_format
35          repo = self.repo = repository.Repository(self.config)
36          source = repo.system_source
37          try:
38              source.restore(osp.join(tmpdir, source.uri), self.confirm, drop, format)
39 -        except Exception as exc:
40 +        except Exception:
41 +            _, exc, traceback_ = sys.exc_info()
42              print('-> error trying to restore %s [%s]' % (source.uri, exc))
43 -            if not self.confirm('Continue anyway?', default='n'):
44 +            if not self.confirm('Continue anyway?', default='n', pdb=True, traceback=traceback_):
45                  raise SystemExit(1)
46          finally:
47              shutil.rmtree(tmpdir)
48          # call hooks
49          repo.bootstrap()
@@ -1452,12 +1453,13 @@
50          """
51          if not ask_confirm or self.confirm('Execute sql: %s ?' % sql):
52              try:
53                  cu = self.cnx.system_sql(sql, args)
54              except Exception:
55 -                ex = sys.exc_info()[1]
56 -                if self.confirm('Error: %s\nabort?' % ex, pdb=True):
57 +                _, ex, traceback_ = sys.exc_info()
58 +                if self.confirm('Error: %s\nabort?' % ex,
59 +                                pdb=True, traceback=traceback_):
60                      raise
61                  return
62              try:
63                  return cu.fetchall()
64              except Exception:
@@ -1477,12 +1479,14 @@
65              else:
66                  msg = rql
67              if not ask_confirm or self.confirm('Execute rql: %s ?' % msg):
68                  try:
69                      res = execute(rql, kwargs, build_descr=build_descr)
70 -                except Exception as ex:
71 -                    if self.confirm('Error: %s\nabort?' % ex, pdb=True):
72 +                except Exception:
73 +                    _, ex, traceback_ = sys.exc_info()
74 +                    if self.confirm('Error: %s\nabort?' % ex,
75 +                                    pdb=True, traceback=traceback_):
76                          raise
77          return res
78 
79      def rqliter(self, rql, kwargs=None, ask_confirm=True):
80          return ForRqlIterator(self, rql, kwargs, ask_confirm)
@@ -1566,12 +1570,14 @@
81          if self.ask_confirm:
82              if not self._h.confirm('Execute rql: %s ?' % msg):
83                  raise StopIteration
84          try:
85              return self._h._cw.execute(rql, kwargs)
86 -        except Exception as ex:
87 -            if self._h.confirm('Error: %s\nabort?' % ex):
88 +        except Exception:
89 +            _, ex, traceback_ = sys.exc_info()
90 +            if self._h.confirm('Error: %s\nabort?' % ex,
91 +                               pdb=True, traceback=traceback_):
92                  raise
93              else:
94                  raise StopIteration
95 
96      def __next__(self):
diff --git a/cubicweb/test/unittest_migration.py b/cubicweb/test/unittest_migration.py
@@ -16,18 +16,22 @@
97  # You should have received a copy of the GNU Lesser General Public License along
98  # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
99  """cubicweb.migration unit tests"""
100 
101  from os.path import dirname, join
102 +from unittest.mock import patch
103 +
104  from logilab.common.testlib import TestCase, unittest_main
105 
106 -from cubicweb import devtools
107 +from cubicweb import devtools, utils
108 +from logilab.common.shellutils import ASK
109  from cubicweb.cwconfig import CubicWebConfiguration
110  from cubicweb.migration import (
111      filter_scripts,
112      split_constraint,
113      version_strictly_lower,
114 +    MigrationHelper,
115  )
116 
117 
118  class Schema(dict):
119      def has_entity(self, e_type):
@@ -126,7 +130,56 @@
120      assert split_constraint("< 0.2.0") == ("<", "0.2.0")
121      assert split_constraint("<=42.1.0") == ("<=", "42.1.0")
122      assert split_constraint("<= 42.1.0") == ("<=", "42.1.0")
123 
124 
125 +class WontColideWithOtherExceptionsException(Exception):
126 +    pass
127 +
128 +
129 +class MigrationHelperTC(TestCase):
130 +    @patch.object(utils, 'get_pdb')
131 +    @patch.object(ASK, 'ask', return_value="pdb")
132 +    def test_confirm_no_traceback(self, ask, get_pdb):
133 +        post_mortem = get_pdb.return_value.post_mortem
134 +        set_trace = get_pdb.return_value.set_trace
135 +
136 +        # we need to break after post_mortem is called otherwise we get
137 +        # infinite recursion
138 +        set_trace.side_effect = WontColideWithOtherExceptionsException
139 +
140 +        mh = MigrationHelper(config=None)
141 +
142 +        with self.assertRaises(WontColideWithOtherExceptionsException):
143 +            mh.confirm("some question")
144 +
145 +        get_pdb.assert_called_once()
146 +        set_trace.assert_called_once()
147 +        post_mortem.assert_not_called()
148 +
149 +    @patch.object(utils, 'get_pdb')
150 +    @patch.object(ASK, 'ask', return_value="pdb")
151 +    def test_confirm_got_traceback(self, ask, get_pdb):
152 +        post_mortem = get_pdb.return_value.post_mortem
153 +        set_trace = get_pdb.return_value.set_trace
154 +
155 +        # we need to break after post_mortem is called otherwise we get
156 +        # infinite recursion
157 +        post_mortem.side_effect = WontColideWithOtherExceptionsException
158 +
159 +        mh = MigrationHelper(config=None)
160 +
161 +        fake_traceback = object()
162 +
163 +        with self.assertRaises(WontColideWithOtherExceptionsException):
164 +            mh.confirm("some question", traceback=fake_traceback)
165 +
166 +        get_pdb.assert_called_once()
167 +        set_trace.assert_not_called()
168 +        post_mortem.assert_called_once()
169 +
170 +        # we want post_mortem to actually receive the traceback
171 +        self.assertEqual(post_mortem.call_args, ((fake_traceback,),))
172 +
173 +
174  if __name__ == '__main__':
175      unittest_main()