Merge lp:~jml/launchpad/use-testtools into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Approved by: Jonathan Lange
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/use-testtools
Merge into: lp:launchpad
Diff against target: 596 lines (+59/-325)
10 files modified
lib/canonical/launchpad/testing/tests/test_testcase.py (+0/-163)
lib/lp/codehosting/puller/tests/test_scheduler.py (+5/-0)
lib/lp/registry/windmill/tests/test_add_milestone.py.disabled (+1/-0)
lib/lp/registry/windmill/tests/test_datetime_picker.py (+1/-0)
lib/lp/registry/windmill/tests/test_plusnew_step1.py (+1/-0)
lib/lp/registry/windmill/tests/test_plusnew_step2.py (+1/-0)
lib/lp/registry/windmill/tests/test_team_index.py (+1/-0)
lib/lp/registry/windmill/tests/test_timeline_graph.py (+2/-0)
lib/lp/testing/__init__.py (+46/-161)
utilities/sourcedeps.conf (+1/-1)
To merge this branch: bzr merge lp:~jml/launchpad/use-testtools
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Aaron Bentley (community) Approve
Review via email: mp+16711@code.launchpad.net

Commit message

Use testtools, really. Make it an egg, and delete all of the Launchpad code that duplicates testtools.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

This is a bit preliminary, because there are still XXX comments and I don't really know how to do this sort of thing, but it's worth getting feedback now.

This branch changes Launchpad to use a testtools release, rather than the branch. It then changes our base test case to inherit from testtools.TestCase rather than unittest.TestCase directly. This lets us delete a whole bunch of code.

While looking at lp/testing/__init__.py, I noticed a few things that could be improved. The ones I could easily fix (like the duplicated code in 'assertRaisesWithContent'), I fixed. Others, I've marked with XXXs.

I haven't run the tests yet, partly since I'm afraid of committing the tarball for testtools to the download-cache.

Revision history for this message
Jonathan Lange (jml) wrote :

I got over my fear & committed the tarball for testtools to download-cache, like this:

$ bzr st
added:
  dist/testtools-0.9.2.tar.gz

Testing now.

Revision history for this message
Aaron Bentley (abentley) wrote :

This looks totally reasonable to me.

review: Approve
Revision history for this message
Gary Poster (gary) wrote :

You have some diff cruft problems visible in lines 193 through 201 of the diff as I see it right now. Other than that it looks good to me.

Gary

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'lib/canonical/launchpad/testing/tests/test_testcase.py'
2--- lib/canonical/launchpad/testing/tests/test_testcase.py 2009-06-25 05:30:52 +0000
3+++ lib/canonical/launchpad/testing/tests/test_testcase.py 1970-01-01 00:00:00 +0000
4@@ -1,163 +0,0 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6-# GNU Affero General Public License version 3 (see the file LICENSE).
7-
8-"""Tests for the base TestCase classes."""
9-
10-__metaclass__ = type
11-
12-import unittest
13-from lp.testing import TestCase
14-
15-
16-class LoggingResult(unittest.TestResult):
17- """TestResult that logs its event to a list."""
18-
19- def __init__(self, log):
20- self._events = log
21- super(LoggingResult, self).__init__()
22-
23- def startTest(self, test):
24- self._events.append(('startTest', test))
25- super(LoggingResult, self).startTest(test)
26-
27- def stopTest(self, test):
28- self._events.append(('stopTest', test))
29- super(LoggingResult, self).stopTest(test)
30-
31- def addFailure(self, test, error):
32- self._events.append(('addFailure', test, error))
33- super(LoggingResult, self).addFailure(test, error)
34-
35- def addError(self, test, error):
36- self._events.append(('addError', test, error))
37- super(LoggingResult, self).addError(test, error)
38-
39- def addSuccess(self, test):
40- self._events.append(('addSuccess', test))
41- super(LoggingResult, self).addSuccess(test)
42-
43-
44-class TestAddCleanup(unittest.TestCase):
45- """Tests for TestCase.addCleanup."""
46-
47- class LoggingTest(TestCase):
48- """A test that logs calls to setUp, runTest and tearDown."""
49-
50- def setUp(self):
51- self._calls = ['setUp']
52-
53- def brokenSetUp(self):
54- # A tearDown that deliberately fails.
55- self._calls = ['brokenSetUp']
56- raise RuntimeError('Deliberate Failure')
57-
58- def runTest(self):
59- self._calls.append('runTest')
60-
61- def tearDown(self):
62- self._calls.append('tearDown')
63-
64- def setUp(self):
65- self._result_calls = []
66- self.test = TestAddCleanup.LoggingTest('runTest')
67- self.logging_result = LoggingResult(self._result_calls)
68-
69- def assertErrorLogEqual(self, messages):
70- self.assertEqual(messages, [call[0] for call in self._result_calls])
71-
72- def assertTestLogEqual(self, messages):
73- """Assert that the call log equals `messages`."""
74- self.assertEqual(messages, self.test._calls)
75-
76- def logAppender(self, message):
77- """Return a cleanup that appends `message` to the tests log.
78-
79- Cleanups are callables that are added to a test by addCleanup. To
80- verify that our cleanups run in the right order, we add strings to a
81- list that acts as a log. This method returns a cleanup that will add
82- the given message to that log when run.
83- """
84- self.test._calls.append(message)
85-
86- def test_fixture(self):
87- # A normal run of self.test logs 'setUp', 'runTest' and 'tearDown'.
88- # This test doesn't test addCleanup itself, it just sanity checks the
89- # fixture.
90- self.test.run(self.logging_result)
91- self.assertTestLogEqual(['setUp', 'runTest', 'tearDown'])
92-
93- def test_cleanup_run_before_tearDown(self):
94- # Cleanup functions added with 'addCleanup' are called before tearDown
95- # runs.
96- self.test.addCleanup(self.logAppender, 'cleanup')
97- self.test.run(self.logging_result)
98- self.assertTestLogEqual(['setUp', 'runTest', 'cleanup', 'tearDown'])
99-
100- def test_add_cleanup_called_if_setUp_fails(self):
101- # Cleanup functions added with 'addCleanup' are called even if setUp
102- # fails. Note that tearDown has a different behavior: it is only
103- # called when setUp succeeds.
104- self.test.setUp = self.test.brokenSetUp
105- self.test.addCleanup(self.logAppender, 'cleanup')
106- self.test.run(self.logging_result)
107- self.assertTestLogEqual(['brokenSetUp', 'cleanup'])
108-
109- def test_addCleanup_called_in_reverse_order(self):
110- # Cleanup functions added with 'addCleanup' are called in reverse
111- # order.
112- #
113- # One of the main uses of addCleanup is to dynamically create
114- # resources that need some sort of explicit tearDown. Often one
115- # resource will be created in terms of another, e.g.,
116- # self.first = self.makeFirst()
117- # self.second = self.makeSecond(self.first)
118- #
119- # When this happens, we generally want to clean up the second resource
120- # before the first one, since the second depends on the first.
121- self.test.addCleanup(self.logAppender, 'first')
122- self.test.addCleanup(self.logAppender, 'second')
123- self.test.run(self.logging_result)
124- self.assertTestLogEqual(
125- ['setUp', 'runTest', 'second', 'first', 'tearDown'])
126-
127- def test_tearDown_runs_after_cleanup_failure(self):
128- # tearDown runs even if a cleanup function fails.
129- self.test.addCleanup(lambda: 1/0)
130- self.test.run(self.logging_result)
131- self.assertTestLogEqual(['setUp', 'runTest', 'tearDown'])
132-
133- def test_cleanups_continue_running_after_error(self):
134- # All cleanups are always run, even if one or two of them fail.
135- self.test.addCleanup(self.logAppender, 'first')
136- self.test.addCleanup(lambda: 1/0)
137- self.test.addCleanup(self.logAppender, 'second')
138- self.test.run(self.logging_result)
139- self.assertTestLogEqual(
140- ['setUp', 'runTest', 'second', 'first', 'tearDown'])
141-
142- def test_error_in_cleanups_are_captured(self):
143- # If a cleanup raises an error, we want to record it and fail the
144- # test, even though we go on to run other cleanups.
145- self.test.addCleanup(lambda: 1/0)
146- self.test.run(self.logging_result)
147- self.assertErrorLogEqual(['startTest', 'addError', 'stopTest'])
148-
149- def test_keyboard_interrupt_not_caught(self):
150- # If a cleanup raises KeyboardInterrupt, it gets reraised.
151- def raiseKeyboardInterrupt():
152- raise KeyboardInterrupt()
153- self.test.addCleanup(raiseKeyboardInterrupt)
154- self.assertRaises(
155- KeyboardInterrupt, self.test.run, self.logging_result)
156-
157- def test_multipleErrorsReported(self):
158- # Errors from all failing cleanups are reported.
159- self.test.addCleanup(lambda: 1/0)
160- self.test.addCleanup(lambda: 1/0)
161- self.test.run(self.logging_result)
162- self.assertErrorLogEqual(
163- ['startTest', 'addError', 'addError', 'stopTest'])
164-
165-
166-def test_suite():
167- return unittest.TestLoader().loadTestsFromName(__name__)
168
169=== modified file 'lib/lp/codehosting/puller/tests/test_scheduler.py'
170--- lib/lp/codehosting/puller/tests/test_scheduler.py 2009-12-22 23:35:26 +0000
171+++ lib/lp/codehosting/puller/tests/test_scheduler.py 2010-01-14 04:34:14 +0000
172@@ -640,6 +640,11 @@
173
174 layer = TwistedAppServerLayer
175
176+ # XXX: Hacky workaround for Trial. It interprets the skip method as a
177+ # reason to skip the tests, but the default test result object doesn't
178+ # support skipping, hence errors.
179+ skip = None
180+
181 def setUp(self):
182 TrialTestCase.setUp(self)
183 PullerBranchTestCase.setUp(self)
184
185=== modified file 'lib/lp/registry/windmill/tests/test_add_milestone.py.disabled'
186--- lib/lp/registry/windmill/tests/test_add_milestone.py.disabled 2010-01-12 22:43:49 +0000
187+++ lib/lp/registry/windmill/tests/test_add_milestone.py.disabled 2010-01-14 04:34:14 +0000
188@@ -87,6 +87,7 @@
189 layer = RegistryWindmillLayer
190
191 def setUp(self):
192+ super(TestAddMilestone, self).setUp()
193 self.client = WindmillTestClient('AddMilestone')
194
195 def test_adding_milestone_on_addrelease_page(self):
196
197=== modified file 'lib/lp/registry/windmill/tests/test_datetime_picker.py'
198--- lib/lp/registry/windmill/tests/test_datetime_picker.py 2009-12-10 17:52:56 +0000
199+++ lib/lp/registry/windmill/tests/test_datetime_picker.py 2010-01-14 04:34:14 +0000
200@@ -22,6 +22,7 @@
201 layer = RegistryWindmillLayer
202
203 def setUp(self):
204+ super(TestDateTimeCalendarWidget, self).setUp()
205 self.client = WindmillTestClient('DateTimeCalendarWidget')
206
207 def test_datetime_calendar_widget(self):
208
209=== modified file 'lib/lp/registry/windmill/tests/test_plusnew_step1.py'
210--- lib/lp/registry/windmill/tests/test_plusnew_step1.py 2009-12-11 19:54:04 +0000
211+++ lib/lp/registry/windmill/tests/test_plusnew_step1.py 2010-01-14 04:34:14 +0000
212@@ -24,6 +24,7 @@
213 layer = RegistryWindmillLayer
214
215 def setUp(self):
216+ super(TestNewProjectStep1, self).setUp()
217 self.client = WindmillTestClient('TestNewProjectStep1')
218
219 def test_projects_plusnew_text_fields(self):
220
221=== modified file 'lib/lp/registry/windmill/tests/test_plusnew_step2.py'
222--- lib/lp/registry/windmill/tests/test_plusnew_step2.py 2009-12-11 19:54:04 +0000
223+++ lib/lp/registry/windmill/tests/test_plusnew_step2.py 2010-01-14 04:34:14 +0000
224@@ -22,6 +22,7 @@
225 layer = RegistryWindmillLayer
226
227 def setUp(self):
228+ super(TestNewProjectStep2, self).setUp()
229 self.client = WindmillTestClient('TestNewProjectStep2')
230
231 def test_projects_plusnew_step_two(self):
232
233=== modified file 'lib/lp/registry/windmill/tests/test_team_index.py'
234--- lib/lp/registry/windmill/tests/test_team_index.py 2009-12-18 05:46:01 +0000
235+++ lib/lp/registry/windmill/tests/test_team_index.py 2010-01-14 04:34:14 +0000
236@@ -24,6 +24,7 @@
237 layer = RegistryWindmillLayer
238
239 def setUp(self):
240+ super(TestTeamIndex, self).setUp()
241 self.client = WindmillTestClient(__name__)
242
243 def test_addmember(self):
244
245=== modified file 'lib/lp/registry/windmill/tests/test_timeline_graph.py'
246--- lib/lp/registry/windmill/tests/test_timeline_graph.py 2009-12-10 17:52:56 +0000
247+++ lib/lp/registry/windmill/tests/test_timeline_graph.py 2010-01-14 04:34:14 +0000
248@@ -13,12 +13,14 @@
249 from lp.registry.windmill.testing import RegistryWindmillLayer
250 from lp.testing import TestCaseWithFactory
251
252+
253 class TestTimelineGraph(TestCaseWithFactory):
254 """Test timeline graph widget."""
255
256 layer = RegistryWindmillLayer
257
258 def setUp(self):
259+ super(TestTimelineGraph, self).setUp()
260 self.client = WindmillTestClient('TimelineGraph')
261
262 def test_timeline_graph(self):
263
264=== modified file 'lib/lp/testing/__init__.py'
265--- lib/lp/testing/__init__.py 2009-12-22 23:50:27 +0000
266+++ lib/lp/testing/__init__.py 2010-01-14 04:34:14 +0000
267@@ -1,24 +1,47 @@
268-# Copyright 2009 Canonical Ltd. This software is licensed under the
269+# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
270 # GNU Affero General Public License version 3 (see the file LICENSE).
271
272 # pylint: disable-msg=W0401,C0301
273
274 __metaclass__ = type
275+__all__ = [
276+ 'ANONYMOUS',
277+ 'capture_events',
278+ 'FakeTime',
279+ 'get_lsb_information',
280+ 'is_logged_in',
281+ 'login',
282+ 'login_person',
283+ 'logout',
284+ 'map_branch_contents',
285+ 'normalize_whitespace',
286+ 'record_statements',
287+ 'run_with_login',
288+ 'run_with_storm_debug',
289+ 'run_script',
290+ 'TestCase',
291+ 'TestCaseWithFactory',
292+ 'test_tales',
293+ 'time_counter',
294+ # XXX: This really shouldn't be exported from here. People should import
295+ # it from Zope.
296+ 'verifyObject',
297+ 'validate_mock_class',
298+ 'with_anonymous_login',
299+ ]
300
301+import copy
302 from datetime import datetime, timedelta
303-from pprint import pformat
304-import copy
305 from inspect import getargspec, getmembers, getmro, isclass, ismethod
306 import os
307+from pprint import pformat
308 import shutil
309 import subprocess
310 import tempfile
311 import time
312-import unittest
313
314 from bzrlib.branch import Branch as BzrBranch
315 from bzrlib.bzrdir import BzrDir, format_registry
316-from bzrlib.errors import InvalidURLJoin
317 from bzrlib.transport import get_transport
318
319 import pytz
320@@ -26,7 +49,11 @@
321 from storm.store import Store
322 from storm.tracer import install_tracer, remove_tracer_type
323
324+import testtools
325 import transaction
326+
327+from twisted.python.util import mergeFunctionMetadata
328+
329 from zope.component import getUtility
330 import zope.event
331 from zope.interface.verify import verifyClass, verifyObject
332@@ -34,18 +61,17 @@
333 isinstance as zope_isinstance, removeSecurityProxy)
334
335 from canonical.launchpad.webapp import errorlog
336+from canonical.config import config
337+from canonical.launchpad.webapp.interfaces import ILaunchBag
338 from lp.codehosting.vfs import branch_id_to_path, get_multi_server
339-from canonical.config import config
340 # Import the login and logout functions here as it is a much better
341 # place to import them from in tests.
342-from canonical.launchpad.webapp.interfaces import ILaunchBag
343-
344 from lp.testing._login import (
345 ANONYMOUS, is_logged_in, login, login_person, logout)
346+# canonical.launchpad.ftests expects test_tales to be imported from here.
347+# XXX: JonathanLange 2010-01-01: Why?!
348 from lp.testing._tales import test_tales
349
350-from twisted.python.util import mergeFunctionMetadata
351-
352 # zope.exception demands more of frame objects than twisted.python.failure
353 # provides in its fake frames. This is enough to make it work with them
354 # as of 2009-09-16. See https://bugs.edge.launchpad.net/bugs/425113.
355@@ -164,72 +190,9 @@
356 debug(False)
357
358
359-class TestCase(unittest.TestCase):
360+class TestCase(testtools.TestCase):
361 """Provide Launchpad-specific test facilities."""
362
363- # Python 2.4 monkeypatch:
364- if getattr(unittest.TestCase, '_exc_info', None) is None:
365- _exc_info = unittest.TestCase._TestCase__exc_info
366- # We would not expect to need to make this property writeable, but
367- # twisted.trial.unittest.TestCase.__init__ chooses to write to it in
368- # the same way that the __init__ of the standard library's
369- # unittest.TestCase.__init__ does, as part of its own method of
370- # arranging for pre-2.5 compatibility.
371- class MonkeyPatchDescriptor:
372- def __get__(self, obj, type):
373- return obj._TestCase__testMethodName
374- def __set__(self, obj, value):
375- obj._TestCase__testMethodName = value
376- _testMethodName = MonkeyPatchDescriptor()
377-
378- def __init__(self, *args, **kwargs):
379- unittest.TestCase.__init__(self, *args, **kwargs)
380- self._cleanups = []
381-
382- def __str__(self):
383- """Return the fully qualified Python name of the test.
384-
385- Zope uses this method to determine how to print the test in the
386- runner. We use the test's id in order to make the test easier to find,
387- and also so that modifications to the id will show up. This is
388- particularly important with bzrlib-style test multiplication.
389- """
390- return self.id()
391-
392- def _runCleanups(self, result):
393- """Run the cleanups that have been added with addCleanup.
394-
395- See the docstring for addCleanup for more information.
396-
397- Returns True if all cleanups ran without error, False otherwise.
398- """
399- ok = True
400- while self._cleanups:
401- function, arguments, keywordArguments = self._cleanups.pop()
402- try:
403- function(*arguments, **keywordArguments)
404- except KeyboardInterrupt:
405- raise
406- except:
407- result.addError(self, self._exc_info())
408- ok = False
409- return ok
410-
411- def addCleanup(self, function, *arguments, **keywordArguments):
412- """Add a cleanup function to be called before tearDown.
413-
414- Functions added with addCleanup will be called in reverse order of
415- adding after the test method and before tearDown.
416-
417- If a function added with addCleanup raises an exception, the error
418- will be recorded as a test error, and the next cleanup will then be
419- run.
420-
421- Cleanup functions are always called before a test finishes running,
422- even if setUp is aborted by an exception.
423- """
424- self._cleanups.append((function, arguments, keywordArguments))
425-
426 def installFixture(self, fixture):
427 """Install 'fixture', an object that has a `setUp` and `tearDown`.
428
429@@ -346,27 +309,12 @@
430 self.assertTrue(zope_isinstance(instance, assert_class),
431 '%r is not an instance of %r' % (instance, assert_class))
432
433- def assertIs(self, expected, observed):
434- """Assert that `expected` is the same object as `observed`."""
435- self.assertTrue(expected is observed,
436- "%r is not %r" % (expected, observed))
437-
438 def assertIsNot(self, expected, observed, msg=None):
439 """Assert that `expected` is not the same object as `observed`."""
440 if msg is None:
441 msg = "%r is %r" % (expected, observed)
442 self.assertTrue(expected is not observed, msg)
443
444- def assertIn(self, needle, haystack):
445- """Assert that 'needle' is in 'haystack'."""
446- self.assertTrue(
447- needle in haystack, '%r not in %r' % (needle, haystack))
448-
449- def assertNotIn(self, needle, haystack):
450- """Assert that 'needle' is not in 'haystack'."""
451- self.assertFalse(
452- needle in haystack, '%r in %r' % (needle, haystack))
453-
454 def assertContentEqual(self, iter1, iter2):
455 """Assert that 'iter1' has the same content as 'iter2'."""
456 list1 = sorted(iter1)
457@@ -374,28 +322,6 @@
458 self.assertEqual(
459 list1, list2, '%s != %s' % (pformat(list1), pformat(list2)))
460
461- def assertRaises(self, excClass, callableObj, *args, **kwargs):
462- """Assert that a callable raises a particular exception.
463-
464- :param excClass: As for the except statement, this may be either an
465- exception class, or a tuple of classes.
466- :param callableObj: A callable, will be passed ``*args`` and
467- ``**kwargs``.
468-
469- Returns the exception so that you can examine it.
470- """
471- try:
472- callableObj(*args, **kwargs)
473- except excClass, e:
474- return e
475- else:
476- if getattr(excClass, '__name__', None) is not None:
477- excName = excClass.__name__
478- else:
479- # probably a tuple
480- excName = str(excClass)
481- raise self.failureException, "%s not raised" % excName
482-
483 def assertRaisesWithContent(self, exception, exception_content,
484 func, *args):
485 """Check if the given exception is raised with given content.
486@@ -403,15 +329,8 @@
487 If the exception isn't raised or the exception_content doesn't
488 match what was raised an AssertionError is raised.
489 """
490- exception_name = str(exception).split('.')[-1]
491-
492- try:
493- func(*args)
494- except exception, err:
495- self.assertEqual(str(err), exception_content)
496- else:
497- raise AssertionError(
498- "'%s' was not raised" % exception_name)
499+ err = self.assertRaises(exception, func, *args)
500+ self.assertEqual(exception_content, str(err))
501
502 def assertBetween(self, lower_bound, variable, upper_bound):
503 """Assert that 'variable' is strictly between two boundaries."""
504@@ -425,51 +344,12 @@
505 The config values will be restored during test tearDown.
506 """
507 name = self.factory.getUniqueString()
508- body = '\n'.join(["%s: %s"%(k, v) for k, v in kwargs.iteritems()])
509+ body = '\n'.join(["%s: %s" % (k, v) for k, v in kwargs.iteritems()])
510 config.push(name, "\n[%s]\n%s\n" % (section, body))
511 self.addCleanup(config.pop, name)
512
513- def run(self, result=None):
514- if result is None:
515- result = self.defaultTestResult()
516- result.startTest(self)
517- testMethod = getattr(self, self._testMethodName)
518- try:
519- try:
520- self.setUp()
521- except KeyboardInterrupt:
522- raise
523- except:
524- result.addError(self, self._exc_info())
525- self._runCleanups(result)
526- return
527-
528- ok = False
529- try:
530- testMethod()
531- ok = True
532- except self.failureException:
533- result.addFailure(self, self._exc_info())
534- except KeyboardInterrupt:
535- raise
536- except:
537- result.addError(self, self._exc_info())
538-
539- cleanupsOk = self._runCleanups(result)
540- try:
541- self.tearDown()
542- except KeyboardInterrupt:
543- raise
544- except:
545- result.addError(self, self._exc_info())
546- ok = False
547- if ok and cleanupsOk:
548- result.addSuccess(self)
549- finally:
550- result.stopTest(self)
551-
552 def setUp(self):
553- unittest.TestCase.setUp(self)
554+ testtools.TestCase.setUp(self)
555 from lp.testing.factory import ObjectFactory
556 self.factory = ObjectFactory()
557
558@@ -673,6 +553,8 @@
559 zope.event.subscribers[:] = old_subscribers
560
561
562+# XXX: This doesn't seem like a generically-useful testing function. Perhaps
563+# it should go in a sub-module or something? -- jml
564 def get_lsb_information():
565 """Returns a dictionary with the LSB host information.
566
567@@ -770,6 +652,8 @@
568 return " ".join(string.split())
569
570
571+# XXX: This doesn't seem to be a generically useful testing function. Perhaps
572+# it should go into a sub-module? -- jml
573 def map_branch_contents(branch_url):
574 """Return all files in branch at `branch_url`.
575
576@@ -794,6 +678,7 @@
577
578 return contents
579
580+
581 def validate_mock_class(mock_class):
582 """Validate method signatures in mock classes derived from real classes.
583
584
585=== removed symlink 'lib/testtools'
586=== target was u'../sourcecode/testtools/testtools/'
587=== modified file 'utilities/sourcedeps.conf'
588--- utilities/sourcedeps.conf 2010-01-14 03:48:53 +0000
589+++ utilities/sourcedeps.conf 2010-01-14 04:34:14 +0000
590@@ -13,5 +13,5 @@
591 subunit lp:~launchpad-pqm/subunit/trunk;revno=61
592 subvertpy lp:~launchpad-pqm/subvertpy/trunk;revno=2040
593 testresources lp:~launchpad-pqm/testresources/dev;revno=16
594-canonical-identity-provider lp:~launchpad-pqm/canonical-identity-provider/trunk;revno=8900 optional
595+canonical-identity-provider lp:~launchpad-pqm/canonical-identity-provider/trunk;revno=8901 optional
596 shipit lp:~launchpad-pqm/shipit/trunk;revno=8901 optional