Merge lp:~bac/launchpad/lplib-testing into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/lplib-testing
Merge into: lp:launchpad
Prerequisite: lp:~bac/launchpad/bug-569101
Diff against target: 617 lines (+456/-30)
7 files modified
lib/canonical/launchpad/security.py (+5/-0)
lib/canonical/launchpad/testing/systemdocs.py (+8/-3)
lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled (+374/-0)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+18/-0)
lib/lp/registry/tests/test_doc.py (+9/-3)
lib/lp/services/testing/__init__.py (+40/-23)
lib/lp/testing/_webservice.py (+2/-1)
To merge this branch: bzr merge lp:~bac/launchpad/lplib-testing
Reviewer Review Type Date Requested Status
Curtis Hovey (community) Approve
Review via email: mp+24040@code.launchpad.net

Commit message

Changes to support launchpadlib testing, allow milestones to be retrieved by anonymous lplib user.

Description of the change

= Summary =

Attempts to convert a webservice test to a launchpadlib test discovered
many problems that are fixed in this jumbled up branch.

== Proposed fix ==

=== modified file 'lib/canonical/launchpad/security.py'
 * The only production bug of the lot. Allow milestones to be accessed
anonymously via launchpadlib.

=== modified file 'lib/canonical/launchpad/testing/systemdocs.py'
 * Export the new launchpadlib helper methods to doctests instead of
just pagetests.

=== added file
'lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled'
 * New launchpadlib test file. It is disabled because there are
outstanding issues with the test environment and the tests will not pass
as written. I need to file a bug for the issues and put an XXX into the
beginning of this file. The biggest issue is logged in users with
permission are not allowed to see non-public data or make changes.

=== modified file 'lib/lp/services/testing/__init__.py'
 * Refactor to expose the new method 'build_doctest_suite' which is used
to register a subset of all tests. This is used in the registry test
registration.

=== modified file 'lib/lp/testing/_webservice.py'
 * Silly formatting fixes.

=== modified file 'lib/lp/registry/tests/test_doc.py'
 * Allow the discovery of tests in lp/registry/doc/launchpadlib

== Pre-implementation notes ==

Chats with Curtis, Leonard, Gary and Francis.

== Implementation details ==

Nothing interesting.

== Tests ==

Hmm, none really at this time. The test that proves milestones are now
accessible is in the launchpadlib test.

I'll add a web service test to show that milestones are retrievable by
anonymous users.

== Demo and Q/A ==

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled
  lib/lp/registry/tests/test_doc.py
  lib/canonical/launchpad/testing/systemdocs.py
  lib/lp/testing/_webservice.py
  lib/lp/services/testing/__init__.py

== Pylint notices ==

lib/canonical/launchpad/testing/systemdocs.py
    6: [W0105] String statement has no effect

lib/lp/services/testing/__init__.py
    31: [W0102, build_doctest_suite] Dangerous default value {} as argument
    60: [W0102, build_test_suite] Dangerous default value {} as argument

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Here is the missing test: http://pastebin.ubuntu.com/421231/

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thanks for salvaging your branch. The milestone fix and test harness enhancement is much appreciated.

I think you are missing punctuation:
    # Add doctests using default setup/teardown

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

I'm just curious, does this then glue lplib to the test instance of
launchpad ? Could this be adapted to let third party authors test
their code with lplib against a local launchpad [started up by the
test environment] ?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-04-20 08:01:41 +0000
3+++ lib/canonical/launchpad/security.py 2010-04-25 14:29:32 +0000
4@@ -375,6 +375,11 @@
5 usedfor = IDistributionMirror
6
7
8+class ViewMilestone(AnonymousAuthorization):
9+ """Anyone can view an IMilestone."""
10+ usedfor = IMilestone
11+
12+
13 class EditSpecificationBranch(AuthorizationBase):
14
15 usedfor = ISpecificationBranch
16
17=== modified file 'lib/canonical/launchpad/testing/systemdocs.py'
18--- lib/canonical/launchpad/testing/systemdocs.py 2010-03-24 10:15:57 +0000
19+++ lib/canonical/launchpad/testing/systemdocs.py 2010-04-25 14:29:32 +0000
20@@ -1,10 +1,10 @@
21 # Copyright 2009 Canonical Ltd. This software is licensed under the
22 # GNU Affero General Public License version 3 (see the file LICENSE).
23
24+"""Infrastructure for setting up doctests."""
25+
26 from __future__ import with_statement
27
28-"""Infrastructure for setting up doctests."""
29-
30 __metaclass__ = type
31 __all__ = [
32 'default_optionflags',
33@@ -34,7 +34,9 @@
34 from canonical.launchpad.interfaces import ILaunchBag
35 from canonical.launchpad.webapp.testing import verifyObject
36 from canonical.testing import reset_logging
37-from lp.testing import ANONYMOUS, login, login_person, logout
38+from lp.testing import (
39+ ANONYMOUS, launchpadlib_credentials_for, launchpadlib_for, login,
40+ login_person, logout, oauth_access_token_for)
41 from lp.testing.factory import LaunchpadObjectFactory
42 from lp.testing.views import create_view, create_initialized_view
43
44@@ -202,6 +204,9 @@
45 test.globs['pretty'] = pprint.PrettyPrinter(width=1).pformat
46 test.globs['stop'] = stop
47 test.globs['with_statement'] = with_statement
48+ test.globs['launchpadlib_for'] = launchpadlib_for
49+ test.globs['launchpadlib_credentials_for'] = launchpadlib_credentials_for
50+ test.globs['oauth_access_token_for'] = oauth_access_token_for
51
52
53 def setUp(test):
54
55=== added directory 'lib/lp/registry/doc/launchpadlib'
56=== added file 'lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled'
57--- lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled 1970-01-01 00:00:00 +0000
58+++ lib/lp/registry/doc/launchpadlib/project-registry.txt.disabled 2010-04-25 14:29:32 +0000
59@@ -0,0 +1,374 @@
60+# XXX bug=569189, bac 2010-04-23
61+# This test is disabled due to a bug which prevents authenticated
62+# users from writing data or reading any data with a permission
63+# attached.
64+
65+==============
66+Project Groups
67+==============
68+
69+
70+Project group collection
71+------------------------
72+
73+It is possible to get a batched list of all the project groups.
74+
75+ >>> from launchpadlib.launchpad import Launchpad
76+ >>> from lp.testing._login import logout
77+ >>> logout()
78+ >>> lp_anon = Launchpad.login_anonymously('launchpadlib test',
79+ ... 'http://api.launchpad.dev/')
80+ >>> len(lp_anon.project_groups)
81+ 7
82+
83+ >>> project_groups = sorted(lp_anon.project_groups, key=lambda X: X.name)
84+ >>> project_groups[0].self_link
85+ u'http://.../aaa'
86+
87+ >>> for project_group in project_groups:
88+ ... print "%s (%s)" % (project_group.name, project_group.display_name)
89+ aaa (the Test Project)
90+ apache (Apache)
91+ gimp (the GiMP Project)
92+ gnome (GNOME)
93+ iso-codes-project (iso-codes)
94+ launchpad-mirrors (Launchpad SCM Mirrors)
95+ mozilla (the Mozilla Project)
96+
97+It's possible to search the list and get a subset of the project groups.
98+
99+ >>> lp_anon = Launchpad.login_anonymously('launchpadlib test',
100+ ... 'http://api.launchpad.dev/')
101+ >>> project_groups = lp_anon.project_groups.search(text="Apache")
102+ >>> for project_group in project_groups:
103+ ... print project_group.display_name
104+ Apache
105+
106+Searching without providing a search string is the same as getting all
107+the project groups.
108+
109+ >>> project_groups = lp_anon.project_groups.search()
110+ >>> project_groups = sorted(project_groups, key=lambda X: X.name)
111+ >>> for project_group in project_groups:
112+ ... print "%s (%s)" % (project_group.name, project_group.display_name)
113+ aaa (the Test Project)
114+ apache (Apache)
115+ gimp (the GiMP Project)
116+ gnome (GNOME)
117+ iso-codes-project (iso-codes)
118+ launchpad-mirrors (Launchpad SCM Mirrors)
119+ mozilla (the Mozilla Project)
120+
121+Project group object
122+--------------------
123+
124+An individual project group can be accessed using dictionary-like syntax.
125+
126+ >>> mozilla = lp_anon.project_groups['mozilla']
127+
128+A project group supplies many attributes, collections, operations.
129+
130+ >>> from operator import attrgetter
131+ >>> def pprint_object(obj):
132+ ... groups = ['lp_attributes',
133+ ... 'lp_collections',
134+ ... 'lp_entries']
135+ ... items = []
136+ ... for group in groups:
137+ ... items.extend(attrgetter(group)(obj))
138+ ... items.remove('http_etag')
139+ ... for item in sorted(items):
140+ ... value = attrgetter(item)(obj)
141+ ... print "%s: %s" % (item, value)
142+
143+The project group object has a large set of properties that can be
144+accessed directly.
145+
146+ >>> pprint_object(mozilla)
147+ active: True
148+ active_milestones: <lazr.restfulclient.resource.Collection object...>
149+ all_milestones: <lazr.restfulclient.resource.Collection object ...>
150+ bug_reporting_guidelines: None
151+ bug_tracker: None
152+ date_created: 2004-09-24 20:58:02.177698+00:00
153+ description: The Mozilla Project produces several internet applications ...
154+ display_name: the Mozilla Project
155+ driver: None
156+ freshmeat_project: None
157+ homepage_content: None
158+ homepage_url: http://www.mozilla.org/
159+ icon: <lazr.restfulclient.resource.HostedFile object ...>
160+ logo: <lazr.restfulclient.resource.HostedFile object ...>
161+ mugshot: <lazr.restfulclient.resource.HostedFile object ...>
162+ name: mozilla
163+ official_bug_tags: []
164+ owner: http://.../~name12
165+ projects: <lazr.restfulclient.resource.Collection object ...>
166+ registrant: http://.../~name12
167+ resource_type_link: http://.../#project_group
168+ reviewed: False
169+ self_link: http://.../mozilla
170+ sourceforge_project: None
171+ summary: The Mozilla Project is the largest open source web browser...
172+ title: The Mozilla Project
173+ wiki_url: None
174+
175+The milestones can be accessed through the active_milestones
176+collection and the all_milestones collection.
177+
178+ >>> def print_collection(collection, attrs=None):
179+ ... items = [str(item) for item in collection]
180+ ... for item in sorted(items):
181+ ... print item
182+
183+ >>> print_collection(sorted(mozilla.active_milestones))
184+ http://api.launchpad.dev/.../mozilla/+milestone/1.0
185+
186+ >>> print_collection(sorted(mozilla.all_milestones))
187+ http://.../mozilla/+milestone/0.8
188+ http://.../mozilla/+milestone/0.9
189+ http://.../mozilla/+milestone/0.9.1
190+ http://.../mozilla/+milestone/0.9.2
191+ http://.../mozilla/+milestone/1.0
192+ http://.../mozilla/+milestone/1.0.0
193+
194+
195+An individual milestone can be retrieved. None is returned if it
196+doesn't exist.
197+
198+ >>> print mozilla.getMilestone(name="1.0")
199+ http://.../mozilla/+milestone/1.0
200+
201+ >>> print mozilla.getMilestone(name="fnord")
202+ None
203+
204+
205+Project objects
206+---------------
207+
208+The Launchpad 'projects' collection is used to select an individual
209+project, which has a large number of attributes. Some of the
210+attributes are marked as 'redacted' as they are only visible to
211+project administrators.
212+
213+ >>> firefox = lp_anon.projects['firefox']
214+ >>> pprint_object(firefox)
215+ active: True
216+ active_milestones: <lazr.restfulclient.resource.Collection object...>
217+ all_milestones: <lazr.restfulclient.resource.Collection object...>
218+ brand: <lazr.restfulclient.resource.HostedFile object...>
219+ bug_reporting_guidelines: None
220+ bug_tracker: None
221+ commercial_subscription: None
222+ commercial_subscription_is_due: True
223+ date_created: 2004-09-24 20:58:02.185708+00:00
224+ description: The Mozilla Firefox web browser
225+ development_focus: http://.../firefox/trunk
226+ display_name: Mozilla Firefox
227+ download_url: None
228+ driver: None
229+ freshmeat_project: None
230+ homepage_url: None
231+ icon: <lazr.restfulclient.resource.HostedFile object...>
232+ is_permitted: ...redacted
233+ license_approved: ...redacted
234+ license_info: None
235+ license_reviewed: ...redacted
236+ licenses: []
237+ logo: <lazr.restfulclient.resource.HostedFile object...>
238+ name: firefox
239+ official_bug_tags: []
240+ owner: http://.../~name12
241+ programming_language: None
242+ project_group: http://.../mozilla
243+ qualifies_for_free_hosting: False
244+ registrant: http://.../~name12
245+ releases: <lazr.restfulclient.resource.Collection object...>
246+ remote_product: None
247+ resource_type_link: http://.../#project
248+ reviewer_whiteboard: ...redacted
249+ screenshots_url: None
250+ self_link: http://.../firefox
251+ series: <lazr.restfulclient.resource.Collection object...>
252+ sourceforge_project: None
253+ summary: The Mozilla Firefox web browser
254+ title: Mozilla Firefox
255+ translation_focus: None
256+ wiki_url: None
257+
258+Getting a Launchpad object based on an administrator's credentials
259+allows the previously redacted attributes to be seen.
260+
261+ >>> lp_mark = launchpadlib_for(
262+ ... 'launchpadlib test', 'mark', 'READ_PRIVATE')
263+ >>> print lp_mark.me.name
264+ mark
265+ >>> firefox = lp_mark.projects['firefox']
266+ >>> print firefox.license_reviewed
267+ False
268+
269+In Launchpad project names may not have uppercase letters in their
270+name. As a convenience, requests for projects using the wrong case
271+return the correct project.
272+
273+ >>> firefox = lp_anon.projects['Firefox']
274+ >>> print firefox.title
275+ Mozilla Firefox
276+
277+The milestones can be accessed through the active_milestones
278+collection and the all_milestones collection.
279+
280+ >>> # This should not be needed but using the object fetched above causes a
281+ >>> # 301-Moved Permanently exception.
282+ >>> firefox = lp_anon.projects['firefox']
283+ >>> print_collection(sorted(firefox.active_milestones))
284+ http://api.launchpad.dev/.../firefox/+milestone/1.0
285+
286+ >>> print_collection(sorted(firefox.all_milestones))
287+ http://.../firefox/+milestone/0.9
288+ http://.../firefox/+milestone/0.9.1
289+ http://.../firefox/+milestone/0.9.2
290+ http://.../firefox/+milestone/1.0
291+ http://.../firefox/+milestone/1.0.0
292+
293+An individual milestone can be retrieved. None is returned if it
294+doesn't exist.
295+
296+ >>> print firefox.getMilestone(name="1.0")
297+ http://.../firefox/+milestone/1.0
298+
299+ >>> print firefox.getMilestone(name="fnord")
300+ None
301+
302+A list of series can be accessed through the series_collection_link.
303+
304+ >>> print_collection(firefox.series)
305+ http://.../firefox/1.0
306+ http://.../firefox/trunk
307+
308+"getSeries" returns the series for the given name.
309+
310+ >>> series = firefox.getSeries(name="1.0")
311+ >>> print series.self_link
312+ http://.../firefox/1.0
313+
314+A list of releases can be accessed through the releases_collection_link.
315+
316+ >>> print_collection(firefox.releases)
317+ http://.../firefox/1.0/1.0.0
318+ http://.../firefox/trunk/0.9
319+ http://.../firefox/trunk/0.9.1
320+ http://.../firefox/trunk/0.9.2
321+
322+"getRelease" returns the release for the given version.
323+
324+ >>> release = firefox.getRelease(version="0.9.1")
325+ >>> print release.self_link
326+ http://.../firefox/trunk/0.9.1
327+
328+The development focus series can be accessed through the
329+development_focus attribute.
330+
331+ >>> print firefox.development_focus.self_link
332+ http://.../firefox/trunk
333+
334+Attributes can be edited, but not by the anonymous user.
335+
336+ >>> mark = lp_anon.people['mark']
337+ >>> firefox.driver = mark
338+ >>> firefox.lp_save()
339+ Traceback (most recent call last):
340+ ...
341+ HTTPError: HTTP Error 401: Unauthorized...
342+
343+A project administrator can modify attributes on the project.
344+
345+ >>> mark = lp_mark.people['mark']
346+ >>> firefox = lp_mark.projects['firefox']
347+ >>> firefox.driver = mark
348+ >>> firefox.homepage_url = 'http://sf.net/firefox'
349+ >>> firefox.lp_save()
350+
351+ >>> print firefox.driver.self_link
352+ http://.../~mark
353+ >>> print firefox.homepage_url
354+ http://sf.net/firefox
355+
356+Changing the owner of a project can change other attributes as well.
357+
358+ >>> # Create a product with a series and release.
359+ >>> login('test@canonical.com')
360+ >>> test_project_owner = factory.makePerson(name='test-project-owner')
361+ >>> test_project = factory.makeProduct(
362+ ... name='test-project', owner=test_project_owner)
363+ >>> test_series = factory.makeProductSeries(
364+ ... product=test_project, name='test-series',
365+ ... owner=test_project_owner)
366+ >>> test_milestone = factory.makeMilestone(
367+ ... product=test_project, name='test-milestone',
368+ ... productseries=test_series)
369+ >>> test_project_release = factory.makeProductRelease(
370+ ... product=test_project, milestone=test_milestone)
371+ >>> logout()
372+
373+ >>> nopriv = lp_mark.people['no-priv']
374+ >>> test_project = lp_mark.projects['test-project']
375+ >>> test_project.owner = nopriv
376+ >>> test_project.lp_save()
377+ >>> print test_project.owner.self_link
378+ http://.../~name12
379+ >>> test_series = test_project.getSeries(name="test-series")
380+ >>> print test_series.owner.self_link
381+ http://.../~name12
382+
383+ >>> release = test_project.getMilestone(name='test-milestone')
384+ >>> print release.owner.self_link
385+ http://.../~name12
386+
387+Read-only attributes cannot be changed.
388+
389+ >>> firefox.registrant = nopriv
390+ >>> firefox.lp_save()
391+ Traceback (most recent call last):
392+ ...
393+ HTTPError: HTTP Error 400: Bad Request
394+ ...
395+ registrant_link: You tried to modify a read-only attribute...
396+
397+"get_timeline" returns a list of dictionaries, corresponding to each
398+milestone and release.
399+
400+ >>> print pretty(firefox.get_timeline())
401+ [{u'is_development_focus': False,
402+ u'landmarks': [{u'code_name': u'First Stable Release',
403+ u'date': u'2004-06-28',
404+ u'name': u'1.0.0',
405+ u'type': u'release',
406+ u'uri': u'/firefox/1.0/1.0.0'}],
407+ u'name': u'1.0',
408+ u'status': u'Active Development',
409+ u'uri': u'/firefox/1.0'},
410+ {u'is_development_focus': True,
411+ u'landmarks': [{u'code_name': None,
412+ u'date': u'2056-10-16',
413+ u'name': u'1.0',
414+ u'type': u'milestone',
415+ u'uri': u'/firefox/+milestone/1.0'},
416+ {u'code_name': u'One (secure) Tree Hill',
417+ u'date': u'2004-10-15',
418+ u'name': u'0.9.2',
419+ u'type': u'release',
420+ u'uri': u'/firefox/trunk/0.9.2'},
421+ {u'code_name': u'One Tree Hill (v2)',
422+ u'date': u'2004-10-15',
423+ u'name': u'0.9.1',
424+ u'type': u'release',
425+ u'uri': u'/firefox/trunk/0.9.1'},
426+ {u'code_name': u'One Tree Hill',
427+ u'date': u'2004-10-15',
428+ u'name': u'0.9',
429+ u'type': u'release',
430+ u'uri': u'/firefox/trunk/0.9'}],
431+ u'name': u'trunk',
432+ u'status': u'Active Development',
433+ u'uri': u'/firefox/trunk'}]
434
435=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
436--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-04-23 03:02:31 +0000
437+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-04-25 14:29:32 +0000
438@@ -108,6 +108,24 @@
439 http://.../mozilla/+milestone/0.9.2
440 http://.../mozilla/+milestone/1.0.0
441
442+The milestones can also be accessed anonymously.
443+
444+ >>> response = anon_webservice.get(
445+ ... mozilla['active_milestones_collection_link'])
446+ >>> active_milestones = response.jsonBody()
447+ >>> print_self_link_of_entries(active_milestones)
448+ http://.../mozilla/+milestone/1.0
449+
450+ >>> response = anon_webservice.get(
451+ ... mozilla['all_milestones_collection_link'])
452+ >>> all_milestones = response.jsonBody()
453+ >>> print_self_link_of_entries(all_milestones)
454+ http://.../mozilla/+milestone/0.8
455+ http://.../mozilla/+milestone/0.9
456+ http://.../mozilla/+milestone/0.9.1
457+ http://.../mozilla/+milestone/0.9.2
458+ http://.../mozilla/+milestone/1.0.0
459+
460 "getMilestone" returns a milestone for the given name, or None if there
461 is no milestone for the given name.
462
463
464=== modified file 'lib/lp/registry/tests/test_doc.py'
465--- lib/lp/registry/tests/test_doc.py 2009-10-01 14:48:09 +0000
466+++ lib/lp/registry/tests/test_doc.py 2010-04-25 14:29:32 +0000
467@@ -1,4 +1,4 @@
468-# Copyright 2009 Canonical Ltd. This software is licensed under the
469+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
470 # GNU Affero General Public License version 3 (see the file LICENSE).
471
472 """
473@@ -16,7 +16,7 @@
474 LaunchpadZopelessLayer)
475
476 from lp.registry.tests import mailinglists_helper
477-from lp.services.testing import build_test_suite
478+from lp.services.testing import build_doctest_suite, build_test_suite
479
480
481 here = os.path.dirname(os.path.realpath(__file__))
482@@ -28,6 +28,7 @@
483 DatabaseLayer.force_dirty_database()
484 tearDown(test)
485
486+
487 def mailingListXMLRPCInternalSetUp(test):
488 setUp(test)
489 # Use the direct API view instance, not retrieved through the component
490@@ -182,4 +183,9 @@
491
492
493 def test_suite():
494- return build_test_suite(here, special, layer=DatabaseFunctionalLayer)
495+ suite = build_test_suite(here, special, layer=DatabaseFunctionalLayer)
496+ launchpadlib_path = os.path.join(os.path.pardir, 'doc', 'launchpadlib')
497+ lplib_suite = build_doctest_suite(here, launchpadlib_path,
498+ layer=DatabaseFunctionalLayer)
499+ suite.addTest(lplib_suite)
500+ return suite
501
502=== modified file 'lib/lp/services/testing/__init__.py'
503--- lib/lp/services/testing/__init__.py 2010-01-12 21:03:16 +0000
504+++ lib/lp/services/testing/__init__.py 2010-04-25 14:29:32 +0000
505@@ -1,4 +1,4 @@
506-# Copyright 2009 Canonical Ltd. This software is licensed under the
507+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
508 # GNU Affero General Public License version 3 (see the file LICENSE).
509
510 """
511@@ -10,6 +10,12 @@
512 doc/ - Contains doctests
513 """
514
515+__metaclass__ = type
516+__all__ = [
517+ 'build_doctest_suite',
518+ 'build_test_suite',
519+ ]
520+
521 import logging
522 import os
523 import unittest
524@@ -20,14 +26,43 @@
525 from canonical.launchpad.testing.systemdocs import (
526 LayeredDocFileSuite, setUp, tearDown)
527 from canonical.testing import DatabaseFunctionalLayer
528-from canonical.launchpad.testing.systemdocs import strip_prefix
529+
530+
531+def build_doctest_suite(base_dir, tests_path, special_tests={},
532+ layer=DatabaseFunctionalLayer,
533+ setUp=setUp, tearDown=tearDown,
534+ package=None):
535+ """Build the doc test suite."""
536+ suite = unittest.TestSuite()
537+ # Tests are run relative to the calling module, not this module.
538+ if package is None:
539+ package = doctest._normalize_module(None)
540+ testsdir = os.path.abspath(
541+ os.path.normpath(os.path.join(base_dir, tests_path)))
542+
543+ if os.path.exists(testsdir):
544+ # Add doctests using default setup/teardown.
545+ filenames = [filename
546+ for filename in os.listdir(testsdir)
547+ if (filename.endswith('.txt')
548+ and filename not in special_tests)]
549+ # Sort the list to give a predictable order.
550+ filenames.sort()
551+ for filename in filenames:
552+ path = os.path.join(tests_path, filename)
553+ one_test = LayeredDocFileSuite(
554+ path, package=package, setUp=setUp, tearDown=tearDown,
555+ layer=layer, stdout_logging_level=logging.WARNING)
556+ suite.addTest(one_test)
557+ return suite
558+
559
560 def build_test_suite(base_dir, special_tests={},
561 layer=DatabaseFunctionalLayer,
562 setUp=setUp, tearDown=tearDown):
563 """Build a test suite from a directory containing test files.
564
565- The parent's 'stories' subdirectory will be checked for pagetests and
566+ The parent's 'stories' subdirectory will be checked for pagetests and
567 the parent's 'doc' subdirectory will be checked for doctests.
568
569 :param base_dir: The tests subdirectory that.
570@@ -63,24 +98,6 @@
571 suite.addTest(special_suite)
572
573 tests_path = os.path.join(os.path.pardir, 'doc')
574- testsdir = os.path.abspath(
575- os.path.normpath(os.path.join(base_dir, tests_path))
576- )
577-
578- if os.path.exists(testsdir):
579- # Add doctests using default setup/teardown
580- filenames = [filename
581- for filename in os.listdir(testsdir)
582- if (filename.endswith('.txt')
583- and filename not in special_tests)]
584- # Sort the list to give a predictable order.
585- filenames.sort()
586- for filename in filenames:
587- path = os.path.join(tests_path, filename)
588- one_test = LayeredDocFileSuite(
589- path, package=package, setUp=setUp, tearDown=tearDown,
590- layer=layer, stdout_logging_level=logging.WARNING
591- )
592- suite.addTest(one_test)
593-
594+ suite.addTest(build_doctest_suite(base_dir, tests_path, special_tests,
595+ layer, setUp, tearDown, package))
596 return suite
597
598=== modified file 'lib/lp/testing/_webservice.py'
599--- lib/lp/testing/_webservice.py 2010-04-15 20:37:59 +0000
600+++ lib/lp/testing/_webservice.py 2010-04-25 14:29:32 +0000
601@@ -8,7 +8,7 @@
602 __all__ = [
603 'launchpadlib_credentials_for',
604 'launchpadlib_for',
605- 'oauth_access_token_for'
606+ 'oauth_access_token_for',
607 ]
608
609 from zope.component import getUtility
610@@ -22,6 +22,7 @@
611
612 from lp.testing._login import login, logout
613
614+
615 def oauth_access_token_for(consumer_name, person, permission, context=None):
616 """Find or create an OAuth access token for the given person.
617 :param consumer_name: An OAuth consumer name.