Merge lp:~gz/bzr/unbreak_BZR_TEST_PDB_504070 into lp:bzr

Proposed by Martin Packman
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 5470
Proposed branch: lp:~gz/bzr/unbreak_BZR_TEST_PDB_504070
Merge into: lp:bzr
Diff against target: 161 lines (+92/-4)
2 files modified
bzrlib/tests/__init__.py (+21/-4)
bzrlib/tests/test_selftest.py (+71/-0)
To merge this branch: bzr merge lp:~gz/bzr/unbreak_BZR_TEST_PDB_504070
Reviewer Review Type Date Requested Status
John A Meinel Approve
Review via email: mp+37434@code.launchpad.net

Commit message

Fix BZR_TEST_PDB. (#504070)

Description of the change

Fix regression from introduction of testtools which made BZR_TEST_PDB useless as it didn't get you the actual traceback from the test.

As explained in the bug, because of the different way testtools runs test cases, we need to jump through some extra hoops to get the real traceback from the test, stash it somewhere, and then pass it on in addError and addFailure. The traceback is cleared in stopTest to break the cycle.

There's one case this branch doesn't handle, where the test passes but an exception is raised from a testtools cleanup. There the onException handler doesn't get given the traceback, so pdb will raise a ValueError as there's nothing for it to look at.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

seems good to me.

review: Approve
Revision history for this message
Andrew Bennetts (spiv) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/tests/__init__.py'
2--- bzrlib/tests/__init__.py 2010-09-27 19:31:45 +0000
3+++ bzrlib/tests/__init__.py 2010-10-04 07:45:58 +0000
4@@ -195,6 +195,7 @@
5 self._strict = strict
6 self._first_thread_leaker_id = None
7 self._tests_leaking_threads_count = 0
8+ self._traceback_from_test = None
9
10 def stopTestRun(self):
11 run = self.testsRun
12@@ -280,6 +281,14 @@
13 what = re.sub(r'^bzrlib\.tests\.', '', what)
14 return what
15
16+ # GZ 2010-10-04: Cloned tests may end up harmlessly calling this method
17+ # multiple times in a row, because the handler is added for
18+ # each test but the container list is shared between cases.
19+ # See lp:498869 lp:625574 and lp:637725 for background.
20+ def _record_traceback_from_test(self, exc_info):
21+ """Store the traceback from passed exc_info tuple till"""
22+ self._traceback_from_test = exc_info[2]
23+
24 def startTest(self, test):
25 super(ExtendedTestResult, self).startTest(test)
26 if self.count == 0:
27@@ -288,6 +297,10 @@
28 self.report_test_start(test)
29 test.number = self.count
30 self._recordTestStartTime()
31+ # Make testtools cases give us the real traceback on failure
32+ addOnException = getattr(test, "addOnException", None)
33+ if addOnException is not None:
34+ addOnException(self._record_traceback_from_test)
35 # Only check for thread leaks if the test case supports cleanups
36 addCleanup = getattr(test, "addCleanup", None)
37 if addCleanup is not None:
38@@ -297,6 +310,9 @@
39 self.report_tests_starting()
40 self._active_threads = threading.enumerate()
41
42+ def stopTest(self, test):
43+ self._traceback_from_test = None
44+
45 def _check_leaked_threads(self, test):
46 """See if any threads have leaked since last call
47
48@@ -323,7 +339,7 @@
49 Called from the TestCase run() method when the test
50 fails with an unexpected error.
51 """
52- self._post_mortem()
53+ self._post_mortem(self._traceback_from_test)
54 super(ExtendedTestResult, self).addError(test, err)
55 self.error_count += 1
56 self.report_error(test, err)
57@@ -336,7 +352,7 @@
58 Called from the TestCase run() method when the test
59 fails because e.g. an assert() method failed.
60 """
61- self._post_mortem()
62+ self._post_mortem(self._traceback_from_test)
63 super(ExtendedTestResult, self).addFailure(test, err)
64 self.failure_count += 1
65 self.report_failure(test, err)
66@@ -384,10 +400,11 @@
67 self.not_applicable_count += 1
68 self.report_not_applicable(test, reason)
69
70- def _post_mortem(self):
71+ def _post_mortem(self, tb=None):
72 """Start a PDB post mortem session."""
73 if os.environ.get('BZR_TEST_PDB', None):
74- import pdb;pdb.post_mortem()
75+ import pdb
76+ pdb.post_mortem(tb)
77
78 def progress(self, offset, whence):
79 """The test is adjusting the count of tests to run."""
80
81=== modified file 'bzrlib/tests/test_selftest.py'
82--- bzrlib/tests/test_selftest.py 2010-09-27 19:31:45 +0000
83+++ bzrlib/tests/test_selftest.py 2010-10-04 07:45:58 +0000
84@@ -3301,6 +3301,77 @@
85 self.assertContainsString(result.stream.getvalue(), "leaking threads")
86
87
88+class TestPostMortemDebugging(tests.TestCase):
89+ """Check post mortem debugging works when tests fail or error"""
90+
91+ class TracebackRecordingResult(tests.ExtendedTestResult):
92+ def __init__(self):
93+ tests.ExtendedTestResult.__init__(self, StringIO(), 0, 1)
94+ self.postcode = None
95+ def _post_mortem(self, tb=None):
96+ """Record the code object at the end of the current traceback"""
97+ tb = tb or sys.exc_info()[2]
98+ if tb is not None:
99+ next = tb.tb_next
100+ while next is not None:
101+ tb = next
102+ next = next.tb_next
103+ self.postcode = tb.tb_frame.f_code
104+ def report_error(self, test, err):
105+ pass
106+ def report_failure(self, test, err):
107+ pass
108+
109+ def test_location_unittest_error(self):
110+ """Needs right post mortem traceback with erroring unittest case"""
111+ class Test(unittest.TestCase):
112+ def runTest(self):
113+ raise RuntimeError
114+ result = self.TracebackRecordingResult()
115+ Test().run(result)
116+ self.assertEqual(result.postcode, Test.runTest.func_code)
117+
118+ def test_location_unittest_failure(self):
119+ """Needs right post mortem traceback with failing unittest case"""
120+ class Test(unittest.TestCase):
121+ def runTest(self):
122+ raise self.failureException
123+ result = self.TracebackRecordingResult()
124+ Test().run(result)
125+ self.assertEqual(result.postcode, Test.runTest.func_code)
126+
127+ def test_location_bt_error(self):
128+ """Needs right post mortem traceback with erroring bzrlib.tests case"""
129+ class Test(tests.TestCase):
130+ def test_error(self):
131+ raise RuntimeError
132+ result = self.TracebackRecordingResult()
133+ Test("test_error").run(result)
134+ self.assertEqual(result.postcode, Test.test_error.func_code)
135+
136+ def test_location_bt_failure(self):
137+ """Needs right post mortem traceback with failing bzrlib.tests case"""
138+ class Test(tests.TestCase):
139+ def test_failure(self):
140+ raise self.failureException
141+ result = self.TracebackRecordingResult()
142+ Test("test_failure").run(result)
143+ self.assertEqual(result.postcode, Test.test_failure.func_code)
144+
145+ def test_env_var_triggers_post_mortem(self):
146+ """Check pdb.post_mortem is called iff BZR_TEST_PDB is set"""
147+ import pdb
148+ result = tests.ExtendedTestResult(StringIO(), 0, 1)
149+ post_mortem_calls = []
150+ self.overrideAttr(pdb, "post_mortem", post_mortem_calls.append)
151+ self.addCleanup(osutils.set_or_unset_env, "BZR_TEST_PDB",
152+ osutils.set_or_unset_env("BZR_TEST_PDB", None))
153+ result._post_mortem(1)
154+ os.environ["BZR_TEST_PDB"] = "on"
155+ result._post_mortem(2)
156+ self.assertEqual([2], post_mortem_calls)
157+
158+
159 class TestRunSuite(tests.TestCase):
160
161 def test_runner_class(self):