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

Proposed by Martin Packman on 2010-08-15
Status: Superseded
Proposed branch: lp:~gz/bzr/cleanup_testcases_by_collection_613247
Merge into: lp:bzr
Diff against target: 838 lines (+212/-143)
24 files modified
bzrlib/branch.py (+4/-1)
bzrlib/cleanup.py (+4/-1)
bzrlib/decorators.py (+16/-4)
bzrlib/errors.py (+1/-0)
bzrlib/osutils.py (+4/-1)
bzrlib/tests/TestUtil.py (+17/-1)
bzrlib/tests/__init__.py (+48/-89)
bzrlib/tests/blackbox/test_breakin.py (+5/-4)
bzrlib/tests/blackbox/test_log.py (+1/-0)
bzrlib/tests/blackbox/test_serve.py (+2/-1)
bzrlib/tests/per_repository/test_check.py (+2/-2)
bzrlib/tests/per_workingtree/test_smart_add.py (+1/-0)
bzrlib/tests/test_chk_map.py (+1/-1)
bzrlib/tests/test_clean_tree.py (+0/-1)
bzrlib/tests/test_errors.py (+16/-11)
bzrlib/tests/test_http.py (+4/-3)
bzrlib/tests/test_knit.py (+1/-0)
bzrlib/tests/test_merge.py (+2/-2)
bzrlib/tests/test_registry.py (+4/-3)
bzrlib/tests/test_repository.py (+2/-2)
bzrlib/tests/test_selftest.py (+70/-10)
bzrlib/tests/test_smart.py (+1/-1)
bzrlib/tests/test_smart_transport.py (+1/-0)
bzrlib/tests/test_workingtree_4.py (+5/-5)
To merge this branch: bzr merge lp:~gz/bzr/cleanup_testcases_by_collection_613247
Reviewer Review Type Date Requested Status
Vincent Ladeuil 2010-08-15 Needs Fixing on 2010-08-19
Review via email: mp+32711@code.launchpad.net

This proposal has been superseded by a proposal from 2010-10-20.

Description of the Change

This branch ensures that after each test case is run, they are cleaned up rather than lingering until the entire suite completes. On my machine this reduces the memory usage of a full test run from ~575MB to ~140MB, which prevents OOM related errors towards the end. In fact, the first revision is a sufficient fix, however Robert's intention in removing an earlier hack was to allow each test case instance to be deallocated after being run. To make that happen, a lot of code needs to be touched to make sure decrementing the refcount after the test completes will actually result in its deallocation.

The most visible change is there will now be a whine printed to the output stream if a testcase stays alive. As this style has proved rather too easy to ignore, I may try an alternative approach once the remaining ones are fixed. The test result has already been recorded by this point, but adding a new stub test to run next that fails may be an option.

As written, the code relies on refcounting working, so cycles need to be avoided. The alternative is to run a full gc collection after every test, which slow enough to be worth avoiding.

Some changes to testtools are needed to avoid some needless exc_info cycles, but even with that fixed we're hamstrung on a few fronts. Firstly, cases with errors or failures are kept in lists on the result object. In testtools, there is also a list for expected failures but bazaar doesn't use this one. However, the new unittest _ExpectedFailure gets the old exc_info which creates cycles.

The last change on the branch guts the TestDecorator classes. This is needed because otherwise they were keeping extra references to test cases alive, but I found the way they worked previously overly complicated anyway. I'd like to simplify them further if there's not a good backward compatibility reason for keeping the old style.

The branch needs a bit more work still, about a hundred tests still need poking to avoid cycles, and needs a NEWS entry and so on. I'd like some feedback on if people think this is a worthwhile change over just stripping the __dict__ and if there are any other issues I've not realised.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

So, I think this is more correct; fixing cycles is pretty important
for us in other ways : I like that we're finding latent issues here.

I'm not worried about the references to test cases in errors/failures
lists : they are a benefit (and the __dict__ stripping was a _REAL_
pain there in the past).

-Rob

Martin Packman (gz) wrote :

> I'm not worried about the references to test cases in errors/failures
> lists : they are a benefit (and the __dict__ stripping was a _REAL_
> pain there in the past).

Indeed that mostly won't be an issue (and can sometimes be useful), my one worry is the occasional breaks-everything rev will then go back to swapping my machine to death. That's why the current implementation still wipes all uncollected tests, but I'm okay with the idea of removing that once everything else is sorted.

A few things I forgot to mention in the initial review comment.

The lock decorators now use this horribly obnoxious pattern:

    self.lock_read()
    try:
        result = unbound(self, *args, **kwargs)
    except:
        import sys
        exc_info = sys.exc_info()
        try:
            self.unlock()
        finally:
            try:
                raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                del exc_info
    else:
        self.unlock()
        return result

If anyone can think of a sane way of spelling that, please say.

Cycles needing to be avoided will have to be documented if we go with this, and can be annoying to understand without decent knowledge of python internals. I've been using a rough reference tracking function to fix things, but it's not worth landing. Is there any package that can help contributors diagnose things like this?

Would there be any benefit in running all tests with gc disabled by default, and only enabling it for those that explicitly require it?

Is there a better way of telling if the test you just ran failed other than comparing failure counts on the result object?

Robert Collins (lifeless) wrote :

> Is there a better way of telling if the test you just ran failed other than comparing failure counts on the result object?

Where do you need to know this?

Martin Packman (gz) wrote :

> > Is there a better way of telling if the test you just ran failed other than
> comparing failure counts on the result object?
>
> Where do you need to know this?

It has to be outside all the normal methods unfortunately. Even stopTest is too early, as that's passed a reference to the test case, so clearly the caller is going to be keeping it alive. Instead I added the collection accounting in TestSuite.run which was already popping testcases from its internal list, now it uses weakref to check that they really expire after being run.

I've put up <lp:~gz/testtools/avoid_exc_info_cycles> with the testtools changes I needed.

Vincent Ladeuil (vila) wrote :

Hmm, I appreciate the effort to reduce the memory footprint, but unfortunately this seems far from landable :-/

BZR_PLUGIN_PATH=-site ./bzr selftest --parallel=fork
bzr selftest: /home/vila/src/bzr/reviews/cleanup_testcases_by_collection_613247/bzr
   /home/vila/src/bzr/reviews/cleanup_testcases_by_collection_613247/bzrlib
   bzr-2.2b3 python-2.6.5 Linux-2.6.32-24-generic-x86_64-with-Ubuntu-10.04-lucid

----------------------------------------------------------------------
Ran 8 tests in 0.489s

OK
2 tests skipped

Huh ? Where are my tests gone ? (Strong suspicion around your modification of __iter__)

Ran 24856 tests in 1050.614s

FAILED (failures=2, errors=2, known_failure_count=48)

Including failures for bb.test_log so your fix is wrong there.

On the overall, with a superficial reading, the way you avoid ref cycles seem hard to understand
so very likely to be re-introduced. How about breaking the ref-cycles with addCleanup calls instead
(by deleting the offending attributes) ?

Now, a way to track the uncollectable test case sounds like a good way to progress but given the amount of work, could you instead devise a patch where the check depends on some option ?
Once we reach 0 we can toggle the default value for this option.

For the run above, I also get ~4000 Uncollected test cases, far too much to land this patch :-/

review: Needs Fixing
Martin Packman (gz) wrote :

Thanks for looking at this Vincent.

> Huh ? Where are my tests gone ? (Strong suspicion around your modification of
> __iter__)

I suspect this is related to --parallel which I thought might be affected but didn't have any easy way of testing on my machine, I'll take another look at that code.

> Including failures for bb.test_log so your fix is wrong there.

Gah, how did I miss those...

> On the overall, with a superficial reading, the way you avoid ref cycles seem
> hard to understand
> so very likely to be re-introduced. How about breaking the ref-cycles with
> addCleanup calls instead
> (by deleting the offending attributes) ?

Yes, that it's hard to understand is a big concern. At least this patch, with retroactively changing a bunch of tests in subtle ways, is the worst part of it. It's much easier to adjust the test you're writing right now because you get told there's a cycle. So, moving in the direction of actually causing a failure once all existing issues are fixed would I think prevent regressions without being too taxing on contributors.

Note, that sometimes cycles do have to be avoided, because it's easier than trying to break them later, particularly with exc_info and closures where it's internal objects pointing at each other rather than public TestCase attributes.

> Now, a way to track the uncollectable test case sounds like a good way to
> progress but given the amount of work, could you instead devise a patch where
> the check depends on some option ?
> Once we reach 0 we can toggle the default value for this option.

Well, the "print junk to stdout" is my less intrusive option, but the aim is to get it to 0 before landing. However...

> For the run above, I also get ~4000 Uncollected test cases, far too much to
> land this patch :-/

This could well be just needing the testtools exc_info cycle cleanup as well, branch for that linked in previous comment.

...getting 4000 complaints from not having the testtools change probably does mean this needs to land quieter, making everyone patch testtools or be spammed isn't polite.

Vincent Ladeuil (vila) wrote :
Download full text (3.2 KiB)

> Thanks for looking at this Vincent.
>
> > Huh ? Where are my tests gone ? (Strong suspicion around your modification
> of
> > __iter__)
>
> I suspect this is related to --parallel which I thought might be affected but
> didn't have any easy way of testing on my machine, I'll take another look at
> that code.

Clearly --parallel specific and at least one __iter__ you've modified should be involved.

>
> > Including failures for bb.test_log so your fix is wrong there.
>
> Gah, how did I miss those...

I'm pretty sure the way the log formatter are registered requires that a single instance is used
and you've changed that. In this case I think breaking the cycle with an addCleanup should be ok.

>
> > On the overall, with a superficial reading, the way you avoid ref cycles
> seem
> > hard to understand
> > so very likely to be re-introduced. How about breaking the ref-cycles with
> > addCleanup calls instead
> > (by deleting the offending attributes) ?
>
> Yes, that it's hard to understand is a big concern. At least this patch, with
> retroactively changing a bunch of tests in subtle ways, is the worst part of
> it. It's much easier to adjust the test you're writing right now because you
> get told there's a cycle.

I understand that, but avoiding the cycle may make the code less clear than breaking it during tearDown,
especially if there is an accompanying comment ;)

> So, moving in the direction of actually causing a
> failure once all existing issues are fixed would I think prevent regressions
> without being too taxing on contributors.

Yup, +1 on that.

> Note, that sometimes cycles do have to be avoided, because it's easier than
> trying to break them later, particularly with exc_info and closures where it's
> internal objects pointing at each other rather than public TestCase
> attributes.

No problem with that, but again adding a comment will help document such cases (so people
can learn from them) and avoid regressions.

> > Now, a way to track the uncollectable test case sounds like a good way to
> > progress but given the amount of work, could you instead devise a patch
> where
> > the check depends on some option ?
> > Once we reach 0 we can toggle the default value for this option.
>
> Well, the "print junk to stdout" is my less intrusive option, but the aim is
> to get it to 0 before landing. However...

I'm fine with that as long as an option control it, there are precedents, look for the lock checks.

> > For the run above, I also get ~4000 Uncollected test cases, far too much to
> > land this patch :-/
>
> This could well be just needing the testtools exc_info cycle cleanup as well,
> branch for that linked in previous comment.

Right, testing with your testtools patch, I still get ~2000 uncollected tests.

>
> ...getting 4000 complaints from not having the testtools change probably does
> mean this needs to land quieter, making everyone patch testtools or be spammed
> isn't polite.

Yup, that's why I suggest an option to control the output that could be stay off by default
until you reach 0 uncollected tests which you can then target in several steps.

In fact, ideally, the first step could be a patch that just imp...

Read more...

5357. By Martin Packman on 2010-08-23

Revert changes to bb.test_log which caused some failures

5358. By Martin Packman on 2010-08-26

Instead break cycle from closure over testcase instance in bb.test_log

5359. By Martin Packman on 2010-10-19

Merge bzr.dev to pick up leak fixes

5360. By Martin Packman on 2010-10-19

Use jam's trick with getDetails rather than hacking around double underscore name mangling

5361. By Martin Packman on 2010-10-20

A few more simple test changes to avoid or break cycles

5362. By Martin Packman on 2010-10-20

Adapt test cleanup accounting to subunit quirks

Martin Packman (gz) wrote :

Quick followup on some points from review last time round.

> > > Huh ? Where are my tests gone ? (Strong suspicion around your modification
> > of
> > > __iter__)
> >
> > I suspect this is related to --parallel which I thought might be affected
> but
> > didn't have any easy way of testing on my machine, I'll take another look at
> > that code.
>
> Clearly --parallel specific and at least one __iter__ you've modified should
> be involved.

This was a combination of subunit playing very differently to the normal selftest result object, and bug 625551 meaning the errors from the children were silently eaten so I didn't notice.

> I'm pretty sure the way the log formatter are registered requires that a
> single instance is used
> and you've changed that. In this case I think breaking the cycle with an
> addCleanup should be ok.

Made this change.

> I'm fine with that as long as an option control it, there are precedents, look
> for the lock checks.

I'm still avoiding this for the moment, so people with 8GB ram at least get some console spam before they completely break my box.

> Right, testing with your testtools patch, I still get ~2000 uncollected tests.

The testtools change landed for 0.9.7 and I'd be interested in a new count. Should now not break with subunit which should help.

5363. By Martin Packman on 2010-11-08

Resort to relying on subunit internals to avoid messing around with stdout in testing

5364. By Martin Packman on 2010-11-08

Add testing for uncollected case warnings under subunit

5365. By Martin Packman on 2010-11-08

Only fiddle with gc for tests if it is enabled, as noted by spiv in review

5366. By Martin Packman on 2010-11-12

Tidy up and comment some of the test changes

5367. By Martin Packman on 2011-05-18

Merge bzr.dev to renew work

5368. By Martin Packman on 2011-05-18

Fix mixup in lazy hook restoration and clear dict

5369. By Martin Packman on 2011-05-18

Put reporting of uncollected tests behind a -Ecollection flag

5370. By Martin Packman on 2011-05-18

Cleanup testcase logging internals and avoid a cycle

5371. By Martin Packman on 2011-05-18

Clear extra lists with test instances in fork_for_tests

5372. By Martin Packman on 2011-05-20

Supersede all of the split out work that jam did

5373. By John A Meinel on 2011-05-25

Bring bzr.dev 5912 back into gz's branch.

5374. By Martin Packman on 2011-05-26

Use 'uncollected_cases' as the flag name which seems a bit clearer

5375. By Martin Packman on 2011-05-26

Fix tests to work regardless of flag state

5376. By Martin Packman on 2011-05-26

Add test subclass for collection of cases under --parallel=fork

5377. By Martin Packman on 2011-05-26

Fix subunit collection tests on Python 2.7 with a little copy code

5378. By Martin Packman on 2011-05-26

Make the fork tests check the right code on single core machines

5379. By Martin Packman on 2011-05-26

Really fix the fork tests by screwing around with subunit internals

5380. By Martin Packman on 2011-05-26

Tweaks and comments

5381. By Martin Packman on 2011-05-26

Merge bzr.dev to resolve conflicts

5382. By Martin Packman on 2011-06-16

Fix fork tests when run using fork, this was a pain to debug

5383. By Martin Packman on 2011-07-27

Use tweaks for subunit protocol client with --parallel=fork as well as --subunit

5384. By Martin Packman on 2011-09-16

Merge bzr.dev to unify _type_equality_funcs workarounds

5385. By Martin Packman on 2011-09-16

Factor out _type_equality_funcs to a new function

5386. By Martin Packman on 2011-09-16

Use pseudo testcase to always fail as suggested by lifeless in review rather than hacking the subunit stream

5387. By Martin Packman on 2011-09-16

Stop making zombie testcases for now as it's an extra risk

5388. By Martin Packman on 2011-10-04

Ensure all inner tests in TestUncollectedWarnings are run

5389. By Martin Packman on 2011-10-04

After adding a pseudo-failure update the stored test count again

5390. By Martin Packman on 2011-10-04

Add release notes

5391. By Martin Packman on 2011-10-05

Revert checking of test count in TestUncollectedCases.test_random_seed

5392. By Martin Packman on 2011-10-18

Dedent release notes to right format

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/branch.py'
2--- bzrlib/branch.py 2010-10-12 09:46:37 +0000
3+++ bzrlib/branch.py 2010-10-20 23:41:10 +0000
4@@ -3258,7 +3258,10 @@
5 try:
6 target.unlock()
7 finally:
8- raise exc_info[0], exc_info[1], exc_info[2]
9+ try:
10+ raise exc_info[0], exc_info[1], exc_info[2]
11+ finally:
12+ del exc_info
13 else:
14 target.unlock()
15 return result
16
17=== modified file 'bzrlib/cleanup.py'
18--- bzrlib/cleanup.py 2010-04-27 07:29:11 +0000
19+++ bzrlib/cleanup.py 2010-10-20 23:41:10 +0000
20@@ -188,7 +188,10 @@
21 # but don't propagate them.
22 _run_cleanup(cleanup, *c_args, **kwargs)
23 if exc_info is not None:
24- raise exc_info[0], exc_info[1], exc_info[2]
25+ try:
26+ raise exc_info[0], exc_info[1], exc_info[2]
27+ finally:
28+ del exc_info
29 # No error, so we can return the result
30 return result
31
32
33=== modified file 'bzrlib/decorators.py'
34--- bzrlib/decorators.py 2010-02-17 17:11:16 +0000
35+++ bzrlib/decorators.py 2010-10-20 23:41:10 +0000
36@@ -101,7 +101,10 @@
37 try:
38 self.unlock()
39 finally:
40- raise exc_info[0], exc_info[1], exc_info[2]
41+ try:
42+ raise exc_info[0], exc_info[1], exc_info[2]
43+ finally:
44+ del exc_info
45 else:
46 self.unlock()
47 return result
48@@ -144,7 +147,10 @@
49 try:
50 self.unlock()
51 finally:
52- raise exc_info[0], exc_info[1], exc_info[2]
53+ try:
54+ raise exc_info[0], exc_info[1], exc_info[2]
55+ finally:
56+ del exc_info
57 else:
58 self.unlock()
59 return result
60@@ -166,7 +172,10 @@
61 try:
62 self.unlock()
63 finally:
64- raise exc_info[0], exc_info[1], exc_info[2]
65+ try:
66+ raise exc_info[0], exc_info[1], exc_info[2]
67+ finally:
68+ del exc_info
69 else:
70 self.unlock()
71 return result
72@@ -197,7 +206,10 @@
73 try:
74 self.unlock()
75 finally:
76- raise exc_info[0], exc_info[1], exc_info[2]
77+ try:
78+ raise exc_info[0], exc_info[1], exc_info[2]
79+ finally:
80+ del exc_info
81 else:
82 self.unlock()
83 return result
84
85=== modified file 'bzrlib/errors.py'
86--- bzrlib/errors.py 2010-10-13 08:01:36 +0000
87+++ bzrlib/errors.py 2010-10-20 23:41:10 +0000
88@@ -1638,6 +1638,7 @@
89
90 def __init__(self, exc_info):
91 import traceback
92+ # GZ 2010-08-10: Cycle with exc_tb/exc_info affects at least one test
93 self.exc_type, self.exc_value, self.exc_tb = exc_info
94 self.exc_info = exc_info
95 traceback_strings = traceback.format_exception(
96
97=== modified file 'bzrlib/osutils.py'
98--- bzrlib/osutils.py 2010-09-16 09:52:29 +0000
99+++ bzrlib/osutils.py 2010-10-20 23:41:10 +0000
100@@ -269,7 +269,10 @@
101 else:
102 rename_func(tmp_name, new)
103 if failure_exc is not None:
104- raise failure_exc[0], failure_exc[1], failure_exc[2]
105+ try:
106+ raise failure_exc[0], failure_exc[1], failure_exc[2]
107+ finally:
108+ del failure_exc
109
110
111 # In Python 2.4.2 and older, os.path.abspath and os.path.realpath
112
113=== modified file 'bzrlib/tests/TestUtil.py'
114--- bzrlib/tests/TestUtil.py 2010-09-21 03:20:09 +0000
115+++ bzrlib/tests/TestUtil.py 2010-10-20 23:41:10 +0000
116@@ -19,6 +19,7 @@
117 import sys
118 import logging
119 import unittest
120+import weakref
121
122 from bzrlib import pyutils
123
124@@ -79,14 +80,29 @@
125 tests = list(self)
126 tests.reverse()
127 self._tests = []
128+ stored_count = 0
129 while tests:
130 if result.shouldStop:
131 self._tests = reversed(tests)
132 break
133- tests.pop().run(result)
134+ case = _run_and_collect_case(tests.pop(), result)()
135+ new_stored_count = getattr(result, "_count_stored_tests", int)()
136+ if case is not None and isinstance(case, unittest.TestCase):
137+ if stored_count == new_stored_count:
138+ # Testcase didn't fail, but somehow is still alive
139+ print "Uncollected test case: %s" % (case.id(),)
140+ # Zombie the testcase but leave a working stub id method
141+ case.__dict__ = {"id": lambda _id=case.id(): _id}
142+ stored_count = new_stored_count
143 return result
144
145
146+def _run_and_collect_case(case, res):
147+ """Run test case against result and use weakref to drop the refcount"""
148+ case.run(res)
149+ return weakref.ref(case)
150+
151+
152 class TestLoader(unittest.TestLoader):
153 """Custom TestLoader to extend the stock python one."""
154
155
156=== modified file 'bzrlib/tests/__init__.py'
157--- bzrlib/tests/__init__.py 2010-10-18 17:06:37 +0000
158+++ bzrlib/tests/__init__.py 2010-10-20 23:41:10 +0000
159@@ -306,13 +306,21 @@
160 if addCleanup is not None:
161 addCleanup(self._check_leaked_threads, test)
162
163+ def stopTest(self, test):
164+ super(ExtendedTestResult, self).stopTest(test)
165+ # Manually break cycles, means touching various private things but hey
166+ getDetails = getattr(test, "getDetails", None)
167+ if getDetails is not None:
168+ getDetails().clear()
169+ type_equality_funcs = getattr(test, "_type_equality_funcs", None)
170+ if type_equality_funcs is not None:
171+ type_equality_funcs.clear()
172+ self._traceback_from_test = None
173+
174 def startTests(self):
175 self.report_tests_starting()
176 self._active_threads = threading.enumerate()
177
178- def stopTest(self, test):
179- self._traceback_from_test = None
180-
181 def _check_leaked_threads(self, test):
182 """See if any threads have leaked since last call
183
184@@ -400,6 +408,10 @@
185 self.not_applicable_count += 1
186 self.report_not_applicable(test, reason)
187
188+ def _count_stored_tests(self):
189+ """Count of tests instances kept alive due to not succeeding"""
190+ return self.error_count + self.failure_count + self.known_failure_count
191+
192 def _post_mortem(self, tb=None):
193 """Start a PDB post mortem session."""
194 if os.environ.get('BZR_TEST_PDB', None):
195@@ -1195,13 +1207,13 @@
196 try:
197 def capture():
198 orig_log_exception_quietly()
199- captured.append(sys.exc_info())
200+ captured.append(sys.exc_info()[1])
201 trace.log_exception_quietly = capture
202 func(*args, **kwargs)
203 finally:
204 trace.log_exception_quietly = orig_log_exception_quietly
205 self.assertLength(1, captured)
206- err = captured[0][1]
207+ err = captured[0]
208 self.assertIsInstance(err, exception_class)
209 return err
210
211@@ -1601,6 +1613,7 @@
212 def _restoreHooks(self):
213 for klass, (name, hooks) in self._preserved_hooks.items():
214 setattr(klass, name, hooks)
215+ self._preserved_hooks.clear()
216
217 def knownFailure(self, reason):
218 """This test has failed for some known reason."""
219@@ -2982,6 +2995,9 @@
220 result_decorators=result_decorators,
221 )
222 runner.stop_on_failure=stop_on_failure
223+ if isinstance(suite, unittest.TestSuite):
224+ # Empty out _tests list of passed suite and populate new TestSuite
225+ suite._tests[:], suite = [], TestSuite(suite)
226 # built in decorator factories:
227 decorators = [
228 random_order(random_seed, runner),
229@@ -3085,34 +3101,17 @@
230
231 class TestDecorator(TestUtil.TestSuite):
232 """A decorator for TestCase/TestSuite objects.
233-
234- Usually, subclasses should override __iter__(used when flattening test
235- suites), which we do to filter, reorder, parallelise and so on, run() and
236- debug().
237+
238+ Contains rather than flattening suite passed on construction
239 """
240
241- def __init__(self, suite):
242- TestUtil.TestSuite.__init__(self)
243- self.addTest(suite)
244-
245- def countTestCases(self):
246- cases = 0
247- for test in self:
248- cases += test.countTestCases()
249- return cases
250-
251- def debug(self):
252- for test in self:
253- test.debug()
254-
255- def run(self, result):
256- # Use iteration on self, not self._tests, to allow subclasses to hook
257- # into __iter__.
258- for test in self:
259- if result.shouldStop:
260- break
261- test.run(result)
262- return result
263+ def __init__(self, suite=None):
264+ super(TestDecorator, self).__init__()
265+ if suite is not None:
266+ self.addTest(suite)
267+
268+ # Don't need subclass run method with suite emptying
269+ run = unittest.TestSuite.run
270
271
272 class CountingDecorator(TestDecorator):
273@@ -3129,90 +3128,50 @@
274 """A decorator which excludes test matching an exclude pattern."""
275
276 def __init__(self, suite, exclude_pattern):
277- TestDecorator.__init__(self, suite)
278- self.exclude_pattern = exclude_pattern
279- self.excluded = False
280-
281- def __iter__(self):
282- if self.excluded:
283- return iter(self._tests)
284- self.excluded = True
285- suite = exclude_tests_by_re(self, self.exclude_pattern)
286- del self._tests[:]
287- self.addTests(suite)
288- return iter(self._tests)
289+ super(ExcludeDecorator, self).__init__(
290+ exclude_tests_by_re(suite, exclude_pattern))
291
292
293 class FilterTestsDecorator(TestDecorator):
294 """A decorator which filters tests to those matching a pattern."""
295
296 def __init__(self, suite, pattern):
297- TestDecorator.__init__(self, suite)
298- self.pattern = pattern
299- self.filtered = False
300-
301- def __iter__(self):
302- if self.filtered:
303- return iter(self._tests)
304- self.filtered = True
305- suite = filter_suite_by_re(self, self.pattern)
306- del self._tests[:]
307- self.addTests(suite)
308- return iter(self._tests)
309+ super(FilterTestsDecorator, self).__init__(
310+ filter_suite_by_re(suite, pattern))
311
312
313 class RandomDecorator(TestDecorator):
314 """A decorator which randomises the order of its tests."""
315
316 def __init__(self, suite, random_seed, stream):
317- TestDecorator.__init__(self, suite)
318- self.random_seed = random_seed
319- self.randomised = False
320- self.stream = stream
321-
322- def __iter__(self):
323- if self.randomised:
324- return iter(self._tests)
325- self.randomised = True
326- self.stream.write("Randomizing test order using seed %s\n\n" %
327- (self.actual_seed()))
328+ random_seed = self.actual_seed(random_seed)
329+ stream.write("Randomizing test order using seed %s\n\n" %
330+ (random_seed,))
331 # Initialise the random number generator.
332- random.seed(self.actual_seed())
333- suite = randomize_suite(self)
334- del self._tests[:]
335- self.addTests(suite)
336- return iter(self._tests)
337+ random.seed(random_seed)
338+ super(RandomDecorator, self).__init__(randomize_suite(suite))
339
340- def actual_seed(self):
341- if self.random_seed == "now":
342+ @staticmethod
343+ def actual_seed(seed):
344+ if seed == "now":
345 # We convert the seed to a long to make it reuseable across
346 # invocations (because the user can reenter it).
347- self.random_seed = long(time.time())
348+ return long(time.time())
349 else:
350 # Convert the seed to a long if we can
351 try:
352- self.random_seed = long(self.random_seed)
353- except:
354+ return long(seed)
355+ except (TypeError, ValueError):
356 pass
357- return self.random_seed
358+ return seed
359
360
361 class TestFirstDecorator(TestDecorator):
362 """A decorator which moves named tests to the front."""
363
364 def __init__(self, suite, pattern):
365- TestDecorator.__init__(self, suite)
366- self.pattern = pattern
367- self.filtered = False
368-
369- def __iter__(self):
370- if self.filtered:
371- return iter(self._tests)
372- self.filtered = True
373- suites = split_suite_by_re(self, self.pattern)
374- del self._tests[:]
375- self.addTests(suites)
376- return iter(self._tests)
377+ super(TestFirstDecorator, self).__init__()
378+ self.addTests(split_suite_by_re(suite, pattern))
379
380
381 def partition_tests(suite, count):
382
383=== modified file 'bzrlib/tests/blackbox/test_breakin.py'
384--- bzrlib/tests/blackbox/test_breakin.py 2009-09-28 03:36:45 +0000
385+++ bzrlib/tests/blackbox/test_breakin.py 2010-10-20 23:41:10 +0000
386@@ -42,10 +42,6 @@
387 def setUp(self):
388 super(TestBreakin, self).setUp()
389 self.requireFeature(tests.BreakinFeature)
390- if sys.platform == 'win32':
391- self._send_signal = self._send_signal_win32
392- else:
393- self._send_signal = self._send_signal_via_kill
394
395 def _send_signal_via_kill(self, pid, sig_type):
396 if sig_type == 'break':
397@@ -89,6 +85,11 @@
398 exit_code = breakin.determine_signal()
399 ctypes.windll.kernel32.TerminateProcess(pid, exit_code)
400
401+ if sys.platform == 'win32':
402+ _send_signal = _send_signal_win32
403+ else:
404+ _send_signal = _send_signal_via_kill
405+
406 def _popen(self, *args, **kwargs):
407 if sys.platform == 'win32':
408 CREATE_NEW_PROCESS_GROUP = 512
409
410=== modified file 'bzrlib/tests/blackbox/test_log.py'
411--- bzrlib/tests/blackbox/test_log.py 2010-07-09 16:16:11 +0000
412+++ bzrlib/tests/blackbox/test_log.py 2010-10-20 23:41:10 +0000
413@@ -77,6 +77,7 @@
414 self.log_catcher = test_log.LogCatcher(*args, **kwargs)
415 # Always return our own log formatter
416 return self.log_catcher
417+ self.addCleanup(setattr, MyLogFormatter, "__new__", None)
418
419 def getme(branch):
420 # Always return our own log formatter class hijacking the
421
422=== modified file 'bzrlib/tests/blackbox/test_serve.py'
423--- bzrlib/tests/blackbox/test_serve.py 2010-06-20 11:18:38 +0000
424+++ bzrlib/tests/blackbox/test_serve.py 2010-10-20 23:41:10 +0000
425@@ -276,7 +276,8 @@
426
427 class TestUserdirExpansion(TestCaseWithMemoryTransport):
428
429- def fake_expanduser(self, path):
430+ @staticmethod
431+ def fake_expanduser(path):
432 """A simple, environment-independent, function for the duration of this
433 test.
434
435
436=== modified file 'bzrlib/tests/per_repository/test_check.py'
437--- bzrlib/tests/per_repository/test_check.py 2010-02-17 17:11:16 +0000
438+++ bzrlib/tests/per_repository/test_check.py 2010-10-20 23:41:10 +0000
439@@ -113,8 +113,8 @@
440 needed_refs.setdefault(ref, []).append(tree.branch)
441 self.tree_check = tree._check
442 self.branch_check = tree.branch.check
443- tree._check = self.tree_callback
444- tree.branch.check = self.branch_callback
445+ self.overrideAttr(tree, "_check", self.tree_callback)
446+ self.overrideAttr(tree.branch, "check", self.branch_callback)
447 self.callbacks = []
448 tree.branch.repository.check([revid], callback_refs=needed_refs)
449 self.assertNotEqual([], self.callbacks)
450
451=== modified file 'bzrlib/tests/per_workingtree/test_smart_add.py'
452--- bzrlib/tests/per_workingtree/test_smart_add.py 2010-02-23 07:43:11 +0000
453+++ bzrlib/tests/per_workingtree/test_smart_add.py 2010-10-20 23:41:10 +0000
454@@ -265,6 +265,7 @@
455 super(TestSmartAddTreeUnicode, self).setUp()
456 self.build_tree([u'a\u030a'])
457 self.wt = self.make_branch_and_tree('.')
458+ self.addCleanup(setattr, self, "wt", None)
459 self.overrideAttr(osutils, 'normalized_filename')
460
461 def test_accessible_explicit(self):
462
463=== modified file 'bzrlib/tests/test_chk_map.py'
464--- bzrlib/tests/test_chk_map.py 2010-10-08 02:13:25 +0000
465+++ bzrlib/tests/test_chk_map.py 2010-10-20 23:41:10 +0000
466@@ -1110,7 +1110,7 @@
467 if ('sha1:1adf7c0d1b9140ab5f33bb64c6275fa78b1580b7',) in keys:
468 self.fail("'aaa' pointer was followed %r" % keys)
469 return basis_get(keys, order, fulltext)
470- basis._store.get_record_stream = get_record_stream
471+ self.overrideAttr(basis._store, "get_record_stream", get_record_stream)
472 result = sorted(list(target.iter_changes(basis)))
473 for change in result:
474 if change[0] == ('aaa',):
475
476=== modified file 'bzrlib/tests/test_clean_tree.py'
477--- bzrlib/tests/test_clean_tree.py 2010-08-23 17:54:07 +0000
478+++ bzrlib/tests/test_clean_tree.py 2010-10-20 23:41:10 +0000
479@@ -99,7 +99,6 @@
480 def _dummy_rmtree(path, ignore_errors=False, onerror=None):
481 """Call user supplied error handler onerror.
482 """
483- self.assertTrue(isinstance(onerror, types.FunctionType))
484 # Indicate failure in removing 'path' if path is subdir0
485 # We later check to ensure that this is indicated
486 # to the user as a warning. We raise OSError to construct
487
488=== modified file 'bzrlib/tests/test_errors.py'
489--- bzrlib/tests/test_errors.py 2010-09-13 02:32:44 +0000
490+++ bzrlib/tests/test_errors.py 2010-10-20 23:41:10 +0000
491@@ -575,12 +575,15 @@
492 try:
493 1/0
494 except ZeroDivisionError:
495- exc_info = sys.exc_info()
496- err = errors.HookFailed('hook stage', 'hook name', exc_info, warn=False)
497- self.assertStartsWith(
498- str(err), 'Hook \'hook name\' during hook stage failed:\n')
499- self.assertEndsWith(
500- str(err), 'integer division or modulo by zero')
501+ err = errors.HookFailed('hook stage', 'hook name', sys.exc_info(),
502+ warn=False)
503+ try:
504+ self.assertStartsWith(
505+ str(err), 'Hook \'hook name\' during hook stage failed:\n')
506+ self.assertEndsWith(
507+ str(err), 'integer division or modulo by zero')
508+ finally:
509+ del err
510
511 def test_tip_change_rejected(self):
512 err = errors.TipChangeRejected(u'Unicode message\N{INTERROBANG}')
513@@ -609,11 +612,13 @@
514 try:
515 raise Exception("example error")
516 except Exception:
517- exc_info = sys.exc_info()
518- err = errors.SmartMessageHandlerError(exc_info)
519- self.assertStartsWith(
520- str(err), "The message handler raised an exception:\n")
521- self.assertEndsWith(str(err), "Exception: example error\n")
522+ err = errors.SmartMessageHandlerError(sys.exc_info())
523+ try:
524+ self.assertStartsWith(
525+ str(err), "The message handler raised an exception:\n")
526+ self.assertEndsWith(str(err), "Exception: example error\n")
527+ finally:
528+ del err
529
530 def test_must_have_working_tree(self):
531 err = errors.MustHaveWorkingTree('foo', 'bar')
532
533=== modified file 'bzrlib/tests/test_http.py'
534--- bzrlib/tests/test_http.py 2010-10-12 07:24:46 +0000
535+++ bzrlib/tests/test_http.py 2010-10-20 23:41:10 +0000
536@@ -1977,11 +1977,12 @@
537 tests.TestCase.setUp(self)
538 self.server = self._activity_server(self._protocol_version)
539 self.server.start_server()
540- self.activities = {}
541+ _activities = {} # Don't close over self and create a cycle
542 def report_activity(t, bytes, direction):
543- count = self.activities.get(direction, 0)
544+ count = _activities.get(direction, 0)
545 count += bytes
546- self.activities[direction] = count
547+ _activities[direction] = count
548+ self.activities = _activities
549
550 # We override at class level because constructors may propagate the
551 # bound method and render instance overriding ineffective (an
552
553=== modified file 'bzrlib/tests/test_knit.py'
554--- bzrlib/tests/test_knit.py 2010-06-20 11:18:38 +0000
555+++ bzrlib/tests/test_knit.py 2010-10-20 23:41:10 +0000
556@@ -415,6 +415,7 @@
557 except _TestException, e:
558 retry_exc = errors.RetryWithNewPacks(None, reload_occurred=False,
559 exc_info=sys.exc_info())
560+ # GZ 2010-08-10: Cycle with exc_info affects 3 tests
561 return retry_exc
562
563 def test_read_from_several_packs(self):
564
565=== modified file 'bzrlib/tests/test_merge.py'
566--- bzrlib/tests/test_merge.py 2010-07-29 04:07:27 +0000
567+++ bzrlib/tests/test_merge.py 2010-10-20 23:41:10 +0000
568@@ -2843,14 +2843,14 @@
569
570 def get_merger_factory(self):
571 # Allows the inner methods to access the test attributes
572- test = self
573+ calls = self.calls
574
575 class FooMerger(_mod_merge.ConfigurableFileMerger):
576 name_prefix = "foo"
577 default_files = ['bar']
578
579 def merge_text(self, params):
580- test.calls.append('merge_text')
581+ calls.append('merge_text')
582 return ('not_applicable', None)
583
584 def factory(merger):
585
586=== modified file 'bzrlib/tests/test_registry.py'
587--- bzrlib/tests/test_registry.py 2010-09-21 03:20:09 +0000
588+++ bzrlib/tests/test_registry.py 2010-10-20 23:41:10 +0000
589@@ -216,11 +216,12 @@
590 # We create a registry with "official" objects and "hidden"
591 # objects. The later represent the side effects that led to bug #277048
592 # and #430510
593- self.registry = registry.Registry()
594+ self_registry = registry.Registry()
595
596 def register_more():
597- self.registry.register('hidden', None)
598+ self_registry.register('hidden', None)
599
600+ self.registry = self_registry
601 self.registry.register('passive', None)
602 self.registry.register('active', register_more)
603 self.registry.register('passive-too', None)
604@@ -230,7 +231,7 @@
605 def get_obj(inner_self):
606 # Surprise ! Getting a registered object (think lazy loaded
607 # module) register yet another object !
608- self.registry.register('more hidden', None)
609+ self_registry.register('more hidden', None)
610 return inner_self._obj
611
612 self.registry.register('hacky', None)
613
614=== modified file 'bzrlib/tests/test_repository.py'
615--- bzrlib/tests/test_repository.py 2010-08-05 19:25:52 +0000
616+++ bzrlib/tests/test_repository.py 2010-10-20 23:41:10 +0000
617@@ -1658,7 +1658,7 @@
618 self.addCleanup(target.unlock)
619 source = source_tree.branch.repository._get_source(target._format)
620 self.orig_pack = target.pack
621- target.pack = self.log_pack
622+ self.overrideAttr(target, "pack", self.log_pack)
623 search = target.search_missing_revision_ids(
624 source_tree.branch.repository, tip)
625 stream = source.get_stream(search)
626@@ -1682,7 +1682,7 @@
627 self.addCleanup(target.unlock)
628 source = source_tree.branch.repository
629 self.orig_pack = target.pack
630- target.pack = self.log_pack
631+ self.overrideAttr(target, "pack", self.log_pack)
632 target.fetch(source)
633 if expect_pack_called:
634 self.assertLength(1, self.calls)
635
636=== modified file 'bzrlib/tests/test_selftest.py'
637--- bzrlib/tests/test_selftest.py 2010-10-15 11:20:45 +0000
638+++ bzrlib/tests/test_selftest.py 2010-10-20 23:41:10 +0000
639@@ -18,6 +18,7 @@
640
641 from cStringIO import StringIO
642 from doctest import ELLIPSIS
643+import gc
644 import os
645 import signal
646 import sys
647@@ -36,7 +37,7 @@
648 DocTestMatches,
649 Equals,
650 )
651-import testtools.tests.helpers
652+import testtools.testresult.doubles
653
654 import bzrlib
655 from bzrlib import (
656@@ -705,7 +706,7 @@
657
658 def test_profiles_tests(self):
659 self.requireFeature(test_lsprof.LSProfFeature)
660- terminal = testtools.tests.helpers.ExtendedTestResult()
661+ terminal = testtools.testresult.doubles.ExtendedTestResult()
662 result = tests.ProfileResult(terminal)
663 class Sample(tests.TestCase):
664 def a(self):
665@@ -728,7 +729,7 @@
666 descriptions=0,
667 verbosity=1,
668 )
669- capture = testtools.tests.helpers.ExtendedTestResult()
670+ capture = testtools.testresult.doubles.ExtendedTestResult()
671 test_case.run(MultiTestResult(result, capture))
672 run_case = capture._events[0][1]
673 timed_string = result._testTimeString(run_case)
674@@ -1069,7 +1070,7 @@
675 test = unittest.TestSuite()
676 test.addTest(Test("known_failure_test"))
677 def failing_test():
678- self.fail('foo')
679+ raise AssertionError('foo')
680 test.addTest(unittest.FunctionTestCase(failing_test))
681 stream = StringIO()
682 runner = tests.TextTestRunner(stream=stream)
683@@ -1083,7 +1084,7 @@
684 '^----------------------------------------------------------------------\n'
685 'Traceback \\(most recent call last\\):\n'
686 ' .*' # File .*, line .*, in failing_test' - but maybe not from .pyc
687- ' self.fail\\(\'foo\'\\)\n'
688+ ' raise AssertionError\\(\'foo\'\\)\n'
689 '.*'
690 '^----------------------------------------------------------------------\n'
691 '.*'
692@@ -1095,7 +1096,7 @@
693 # the final output.
694 class Test(tests.TestCase):
695 def known_failure_test(self):
696- self.expectFailure('failed', self.assertTrue, False)
697+ self.knownFailure("Never works...")
698 test = Test("known_failure_test")
699 stream = StringIO()
700 runner = tests.TextTestRunner(stream=stream)
701@@ -2052,17 +2053,17 @@
702
703 def test_lsprof_tests(self):
704 self.requireFeature(test_lsprof.LSProfFeature)
705- calls = []
706+ results = []
707 class Test(object):
708 def __call__(test, result):
709 test.run(result)
710 def run(test, result):
711- self.assertIsInstance(result, ExtendedToOriginalDecorator)
712- calls.append("called")
713+ results.append(result)
714 def countTestCases(self):
715 return 1
716 self.run_selftest(test_suite_factory=Test, lsprof_tests=True)
717- self.assertLength(1, calls)
718+ self.assertLength(1, results)
719+ self.assertIsInstance(results.pop(), ExtendedToOriginalDecorator)
720
721 def test_random(self):
722 # test randomising by listing a number of tests.
723@@ -3394,3 +3395,62 @@
724 self.verbosity)
725 tests.run_suite(suite, runner_class=MyRunner, stream=StringIO())
726 self.assertLength(1, calls)
727+
728+
729+class TestUncollectedWarnings(tests.TestCase):
730+ """Check reference cycles keeping a test case alive emits in a warning"""
731+
732+ class Test(tests.TestCase):
733+ def test_pass(self):
734+ pass
735+ def test_self_ref(self):
736+ self.also_self = self.test_self_ref
737+ def test_skip(self):
738+ self.skip("Don't need")
739+
740+ def _get_suite(self):
741+ return TestUtil.TestSuite([
742+ self.Test("test_pass"),
743+ self.Test("test_self_ref"),
744+ self.Test("test_skip"),
745+ ])
746+
747+ def _run_selftest_with_suite(self, **kwargs):
748+ sio = StringIO()
749+ gc.disable()
750+ try:
751+ tests.selftest(test_suite_factory=self._get_suite, stream=sio,
752+ **kwargs)
753+ finally:
754+ gc.enable()
755+ output = sio.getvalue()
756+ self.assertNotContainsRe(output, "Uncollected test case.*test_pass")
757+ self.assertContainsRe(output, "Uncollected test case.*test_self_ref")
758+ return output
759+
760+ def test_testsuite(self):
761+ self._run_selftest_with_suite()
762+
763+ def test_pattern(self):
764+ out = self._run_selftest_with_suite(pattern="test_(?:pass|self_ref)$")
765+ self.assertNotContainsRe(out, "test_skip")
766+
767+ def test_exclude_pattern(self):
768+ out = self._run_selftest_with_suite(exclude_pattern="test_skip$")
769+ self.assertNotContainsRe(out, "test_skip")
770+
771+ def test_random_seed(self):
772+ self._run_selftest_with_suite(random_seed="now")
773+
774+ def test_matching_tests_first(self):
775+ self._run_selftest_with_suite(matching_tests_first=True,
776+ pattern="test_self_ref$")
777+
778+ def test_starting_with_and_exclude(self):
779+ out = self._run_selftest_with_suite(starting_with=["bt."],
780+ exclude_pattern="test_skip$")
781+ self.assertNotContainsRe(out, "test_skip")
782+
783+ def test_additonal_decorator(self):
784+ out = self._run_selftest_with_suite(
785+ suite_decorators=[tests.TestDecorator])
786
787=== modified file 'bzrlib/tests/test_smart.py'
788--- bzrlib/tests/test_smart.py 2010-05-13 16:17:54 +0000
789+++ bzrlib/tests/test_smart.py 2010-10-20 23:41:10 +0000
790@@ -103,7 +103,7 @@
791 # the default or a parameterized class, but rather use the
792 # TestCaseWithTransport infrastructure to set up a smart server and
793 # transport.
794- self.transport_server = self.make_transport_server
795+ self.overrideAttr(self, "transport_server", self.make_transport_server)
796
797 def make_transport_server(self):
798 return test_server.SmartTCPServer_for_testing('-' + self.id())
799
800=== modified file 'bzrlib/tests/test_smart_transport.py'
801--- bzrlib/tests/test_smart_transport.py 2010-07-01 15:25:41 +0000
802+++ bzrlib/tests/test_smart_transport.py 2010-10-20 23:41:10 +0000
803@@ -1496,6 +1496,7 @@
804 smart_protocol._has_dispatched = True
805 smart_protocol.request = _mod_request.SmartServerRequestHandler(
806 None, _mod_request.request_handlers, '/')
807+ # GZ 2010-08-10: Cycle with closure affects 4 tests
808 class FakeCommand(_mod_request.SmartServerRequest):
809 def do_body(self_cmd, body_bytes):
810 self.end_received = True
811
812=== modified file 'bzrlib/tests/test_workingtree_4.py'
813--- bzrlib/tests/test_workingtree_4.py 2010-07-21 09:58:42 +0000
814+++ bzrlib/tests/test_workingtree_4.py 2010-10-20 23:41:10 +0000
815@@ -174,9 +174,9 @@
816 # it's given; any calls to forbidden methods will raise an
817 # AssertionError
818 repo = tree.branch.repository
819- repo.get_revision = self.fail
820- repo.get_inventory = self.fail
821- repo._get_inventory_xml = self.fail
822+ self.overrideAttr(repo, "get_revision", self.fail)
823+ self.overrideAttr(repo, "get_inventory", self.fail)
824+ self.overrideAttr(repo, "_get_inventory_xml", self.fail)
825 # try to set the parent trees.
826 tree.set_parent_trees([(rev1, rev1_tree)])
827
828@@ -214,8 +214,8 @@
829 # answer 'get_parent_ids' for the revision tree- dirstate does not
830 # cache the parents of a parent tree at this point.
831 #repo.get_revision = self.fail
832- repo.get_inventory = self.fail
833- repo._get_inventory_xml = self.fail
834+ self.overrideAttr(repo, "get_inventory", self.fail)
835+ self.overrideAttr(repo, "_get_inventory_xml", self.fail)
836 # set the parent trees.
837 tree.set_parent_trees([(rev1, rev1_tree), (rev2, rev2_tree)])
838 # read the first tree