Merge lp:~lifeless/launchpad/bug-722956 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12489
Proposed branch: lp:~lifeless/launchpad/bug-722956
Merge into: lp:launchpad
Diff against target: 606 lines (+159/-55)
20 files modified
lib/lp/app/doc/tales.txt (+2/-0)
lib/lp/app/widgets/suggestion.py (+3/-1)
lib/lp/code/browser/branchlisting.py (+3/-3)
lib/lp/code/feed/branch.py (+1/-1)
lib/lp/code/interfaces/branch.py (+11/-3)
lib/lp/code/interfaces/branchcollection.py (+4/-1)
lib/lp/code/interfaces/branchnamespace.py (+5/-2)
lib/lp/code/interfaces/hasbranches.py (+3/-1)
lib/lp/code/interfaces/seriessourcepackagebranch.py (+7/-0)
lib/lp/code/model/branch.py (+29/-13)
lib/lp/code/model/branchcollection.py (+49/-9)
lib/lp/code/model/branchnamespace.py (+1/-1)
lib/lp/code/model/hasbranches.py (+2/-2)
lib/lp/code/model/seriessourcepackagebranch.py (+6/-1)
lib/lp/code/model/tests/test_branchset.py (+26/-12)
lib/lp/code/model/tests/test_branchtarget.py (+1/-1)
lib/lp/code/tests/test_helpers.py (+2/-1)
lib/lp/code/vocabularies/branch.py (+1/-1)
lib/lp/codehosting/branchdistro.py (+2/-1)
lib/lp/codehosting/scanner/mergedetection.py (+1/-1)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-722956
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Stuart Bishop (community) Needs Information
Review via email: mp+51481@code.launchpad.net

Commit message

[r=lifeless][ui=none] [r=lifeless][bug=722956] Eager load productseries and suite series for the api /branches collection.

Description of the change

The API for /branches is nearly guaranteed to have branches from many different contexts, and we were doing late evaluation on many attributes trying to render the collection. This fixes 2 of those cases and lays the ground work to fix more when the page becomes an issue again.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

The new eager_load parameter is exposed to the webservice. What happens when web service requests use this parameter? This seems a quick hack rather than an API improvement.

I suspect we need a new method (that is not exposed to the webservice) rather than adding an optional parameter to the existing method. Or cast the result set to an eager result set (results = IEagerResultSet(foo.getBranches())). Or just a helper (results = foo.getBranches(); eager_load_branches(results)).

Its annoying that the seemingly arbitrary and undocumented ordering in associatedSuiteSourcePackages needs to be duplicated in the eager load method. We should comment this code dependency for when someone wants to change it.

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

On Mon, Feb 28, 2011 at 9:27 PM, Stuart Bishop
<email address hidden> wrote:
> Review: Needs Information
> The new eager_load parameter is exposed to the webservice. What happens when web service requests use this parameter? This seems a quick hack rather than an API improvement.

Its not exposed to the webservice. The getBranches parameters in the
web service is controlled via the @operation_parameters decorator on
IHasBranches. The variant that I have enabled eager loading for is
only exposed as a collection in the web service: setting a default
parameter lets the method be used in code, and used as a collection in
the webservice, with the default being what the webservice needs.

> I suspect we need a new method (that is not exposed to the webservice) rather than adding an optional parameter to the existing method. Or cast the result set to an eager result set (results = IEagerResultSet(foo.getBranches())). Or just a helper (results = foo.getBranches(); eager_load_branches(results)).

I don't think any of those are needed, because my change isn't exposed
as part of the web service contract; its invisible.

> Its annoying that the seemingly arbitrary and undocumented ordering in associatedSuiteSourcePackages needs to be duplicated in the eager load method. We should comment this code dependency for when someone wants to change it.

I will comment on both sides of the eager load to point this out.

Thanks for the review,
Rob

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

I've addressed Stuarts comments; approving to make ec2land happy.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/doc/tales.txt'
2--- lib/lp/app/doc/tales.txt 2011-02-03 17:59:50 +0000
3+++ lib/lp/app/doc/tales.txt 2011-03-01 04:23:03 +0000
4@@ -498,6 +498,8 @@
5
6 >>> login_person(fooix.owner, LaunchpadTestRequest())
7 >>> fooix.development_focus.branch = branch
8+ >>> from lp.services.propertycache import clear_property_cache
9+ >>> clear_property_cache(branch)
10 >>> print test_tales("branch/fmt:bzr-link", branch=branch)
11 <a href=".../~eric/fooix/bar" class="sprite branch">lp://dev/fooix</a>
12
13
14=== modified file 'lib/lp/app/widgets/suggestion.py'
15--- lib/lp/app/widgets/suggestion.py 2011-02-08 15:43:30 +0000
16+++ lib/lp/app/widgets/suggestion.py 2011-03-01 04:23:03 +0000
17@@ -231,7 +231,9 @@
18 collection = branch.target.collection.targetedBy(logged_in_user,
19 since)
20 collection = collection.visibleByUser(logged_in_user)
21- branches = collection.getBranches().config(distinct=True)
22+ # May actually need some eager loading, but the API isn't fine grained
23+ # yet.
24+ branches = collection.getBranches(eager_load=False).config(distinct=True)
25 target_branches = list(branches.config(limit=5))
26 # If there is a development focus branch, make sure it is always
27 # shown, and as the first item.
28
29=== modified file 'lib/lp/code/browser/branchlisting.py'
30--- lib/lp/code/browser/branchlisting.py 2011-02-11 15:09:53 +0000
31+++ lib/lp/code/browser/branchlisting.py 2011-03-01 04:23:03 +0000
32@@ -650,7 +650,7 @@
33 if lifecycle_status is not None:
34 collection = collection.withLifecycleStatus(*lifecycle_status)
35 collection = collection.visibleByUser(self.user)
36- return collection.getBranches().order_by(
37+ return collection.getBranches(eager_load=False).order_by(
38 self._listingSortToOrderBy(self.sort_by))
39
40 @property
41@@ -809,7 +809,7 @@
42 if lifecycle_status is not None:
43 collection = collection.withLifecycleStatus(*lifecycle_status)
44 collection = collection.visibleByUser(self.user)
45- return collection.getBranches().order_by(
46+ return collection.getBranches(eager_load=False).order_by(
47 self._branch_order)
48
49
50@@ -1501,7 +1501,7 @@
51 # We're only interested in active branches.
52 collection = self.getBranchCollection().withLifecycleStatus(
53 *DEFAULT_BRANCH_STATUS_IN_LISTING)
54- for branch in collection.getBranches():
55+ for branch in collection.getBranches(eager_load=False):
56 branches.setdefault(branch.distroseries, []).append(branch)
57 return branches
58
59
60=== modified file 'lib/lp/code/feed/branch.py'
61--- lib/lp/code/feed/branch.py 2010-08-24 10:45:57 +0000
62+++ lib/lp/code/feed/branch.py 2011-03-01 04:23:03 +0000
63@@ -163,7 +163,7 @@
64 from lp.code.model.branch import Branch
65 collection = self._getCollection().visibleByUser(
66 None).withLifecycleStatus(*DEFAULT_BRANCH_STATUS_IN_LISTING)
67- branches = collection.getBranches()
68+ branches = collection.getBranches(eager_load=False)
69 branches.order_by(
70 Desc(Branch.date_last_modified),
71 Asc(Branch.target_suffix),
72
73=== modified file 'lib/lp/code/interfaces/branch.py'
74--- lib/lp/code/interfaces/branch.py 2011-02-28 14:08:54 +0000
75+++ lib/lp/code/interfaces/branch.py 2011-03-01 04:23:03 +0000
76@@ -708,7 +708,10 @@
77 """
78
79 def associatedSuiteSourcePackages():
80- """Return the suite source packages that this branch is linked to."""
81+ """Return the suite source packages that this branch is linked to.
82+
83+ :return: A list of suite source packages ordered by pocket.
84+ """
85
86 def branchLinks():
87 """Return a sorted list of ICanHasLinkedBranch objects.
88@@ -1286,8 +1289,13 @@
89 """
90
91 @collection_default_content()
92- def getBranches(limit=50):
93- """Return a collection of branches."""
94+ def getBranches(limit=50, eager_load=True):
95+ """Return a collection of branches.
96+
97+ :param eager_load: If True (the default because this is used in the
98+ web service and it needs the related objects to create links) eager
99+ load related objects (products, code imports etc).
100+ """
101
102
103 class IBranchListingQueryOptimiser(Interface):
104
105=== modified file 'lib/lp/code/interfaces/branchcollection.py'
106--- lib/lp/code/interfaces/branchcollection.py 2011-01-20 00:06:17 +0000
107+++ lib/lp/code/interfaces/branchcollection.py 2011-03-01 04:23:03 +0000
108@@ -57,13 +57,16 @@
109 of individuals and teams that own branches in this collection.
110 """
111
112- def getBranches():
113+ def getBranches(eager_load=False):
114 """Return a result set of all branches in this collection.
115
116 The returned result set will also join across the specified tables as
117 defined by the arguments to this function. These extra tables are
118 joined specificly to allow the caller to sort on values not in the
119 Branch table itself.
120+
121+ :param eager_load: If True trigger eager loading of all the related
122+ objects in the collection.
123 """
124
125 def getMergeProposals(statuses=None, for_branches=None,
126
127=== modified file 'lib/lp/code/interfaces/branchnamespace.py'
128--- lib/lp/code/interfaces/branchnamespace.py 2010-08-20 20:31:18 +0000
129+++ lib/lp/code/interfaces/branchnamespace.py 2011-03-01 04:23:03 +0000
130@@ -52,8 +52,11 @@
131 a given prefix, use createBranchWithPrefix.
132 """
133
134- def getBranches():
135- """Return the branches in this namespace."""
136+ def getBranches(eager_load=False):
137+ """Return the branches in this namespace.
138+
139+ :param eager_load: If True eager load related data for the branches.
140+ """
141
142 def getBranchName(name):
143 """Get the potential unique name for a branch called 'name'.
144
145=== modified file 'lib/lp/code/interfaces/hasbranches.py'
146--- lib/lp/code/interfaces/hasbranches.py 2010-10-27 07:15:02 +0000
147+++ lib/lp/code/interfaces/hasbranches.py 2011-03-01 04:23:03 +0000
148@@ -60,13 +60,15 @@
149 @operation_returns_collection_of(Interface) # Really IBranch.
150 @export_read_operation()
151 def getBranches(status=None, visible_by_user=None,
152- modified_since=None):
153+ modified_since=None, eager_load=False):
154 """Returns all branches with the given lifecycle status.
155
156 :param status: A list of statuses to filter with.
157 :param visible_by_user: Normally the user who is asking.
158 :param modified_since: If set, filters the branches being returned
159 to those that have been modified since the specified date/time.
160+ :param eager_load: If True load related objects for the whole
161+ collection.
162 :returns: A list of `IBranch`.
163 """
164
165
166=== modified file 'lib/lp/code/interfaces/seriessourcepackagebranch.py'
167--- lib/lp/code/interfaces/seriessourcepackagebranch.py 2010-08-20 20:31:18 +0000
168+++ lib/lp/code/interfaces/seriessourcepackagebranch.py 2011-03-01 04:23:03 +0000
169@@ -65,6 +65,13 @@
170 :return: An `IResultSet` of `ISeriesSourcePackageBranch` objects.
171 """
172
173+ def findForBranches(branches):
174+ """Get the links to source packages from a branch.
175+
176+ :param branches: A an iterable of `IBranch`.
177+ :return: An `IResultSet` of `ISeriesSourcePackageBranch` objects.
178+ """
179+
180 def findForSourcePackage(sourcepackage):
181 """Get the links to branches from a source package.
182
183
184=== modified file 'lib/lp/code/model/branch.py'
185--- lib/lp/code/model/branch.py 2011-02-27 19:45:44 +0000
186+++ lib/lp/code/model/branch.py 2011-03-01 04:23:03 +0000
187@@ -142,6 +142,7 @@
188 from lp.services.job.interfaces.job import JobStatus
189 from lp.services.job.model.job import Job
190 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
191+from lp.services.propertycache import cachedproperty
192
193
194 class Branch(SQLBase, BzrIdentityMixin):
195@@ -724,13 +725,19 @@
196 DeleteCodeImport(self.code_import)()
197 Store.of(self).flush()
198
199+ @cachedproperty
200+ def _associatedProductSeries(self):
201+ """Helper for eager loading associatedProductSeries."""
202+ # This is eager loaded by BranchCollection.getBranches.
203+ # Imported here to avoid circular import.
204+ from lp.registry.model.productseries import ProductSeries
205+ return Store.of(self).find(
206+ ProductSeries,
207+ ProductSeries.branch == self)
208+
209 def associatedProductSeries(self):
210 """See `IBranch`."""
211- # Imported here to avoid circular import.
212- from lp.registry.model.productseries import ProductSeries
213- return Store.of(self).find(
214- ProductSeries,
215- ProductSeries.branch == self)
216+ return self._associatedProductSeries
217
218 def getProductSeriesPushingTranslations(self):
219 """See `IBranch`."""
220@@ -740,14 +747,21 @@
221 ProductSeries,
222 ProductSeries.translations_branch == self)
223
224- def associatedSuiteSourcePackages(self):
225- """See `IBranch`."""
226+ @cachedproperty
227+ def _associatedSuiteSourcePackages(self):
228+ """Helper for associatedSuiteSourcePackages."""
229+ # This is eager loaded by BranchCollection.getBranches.
230 series_set = getUtility(IFindOfficialBranchLinks)
231- # Order by the pocket to get the release one first.
232+ # Order by the pocket to get the release one first. If changing this be
233+ # sure to also change BranchCollection.getBranches.
234 links = series_set.findForBranch(self).order_by(
235 SeriesSourcePackageBranch.pocket)
236 return [link.suite_sourcepackage for link in links]
237
238+ def associatedSuiteSourcePackages(self):
239+ """See `IBranch`."""
240+ return self._associatedSuiteSourcePackages
241+
242 # subscriptions
243 def subscribe(self, person, notification_level, max_diff_lines,
244 code_review_level, subscribed_by):
245@@ -1311,7 +1325,8 @@
246 branches = all_branches.visibleByUser(
247 visible_by_user).withLifecycleStatus(*lifecycle_statuses)
248 branches = branches.withBranchType(
249- BranchType.HOSTED, BranchType.MIRRORED).scanned().getBranches()
250+ BranchType.HOSTED, BranchType.MIRRORED).scanned().getBranches(
251+ eager_load=False)
252 branches.order_by(
253 Desc(Branch.date_last_modified), Desc(Branch.id))
254 if branch_count is not None:
255@@ -1327,7 +1342,7 @@
256 branches = all_branches.visibleByUser(
257 visible_by_user).withLifecycleStatus(*lifecycle_statuses)
258 branches = branches.withBranchType(
259- BranchType.IMPORTED).scanned().getBranches()
260+ BranchType.IMPORTED).scanned().getBranches(eager_load=False)
261 branches.order_by(
262 Desc(Branch.date_last_modified), Desc(Branch.id))
263 if branch_count is not None:
264@@ -1341,7 +1356,8 @@
265 """See `IBranchSet`."""
266 all_branches = getUtility(IAllBranches)
267 branches = all_branches.withLifecycleStatus(
268- *lifecycle_statuses).visibleByUser(visible_by_user).getBranches()
269+ *lifecycle_statuses).visibleByUser(visible_by_user).getBranches(
270+ eager_load=False)
271 branches.order_by(
272 Desc(Branch.date_created), Desc(Branch.id))
273 if branch_count is not None:
274@@ -1360,10 +1376,10 @@
275 """See `IBranchSet`."""
276 return getUtility(IBranchLookup).getByUrls(urls)
277
278- def getBranches(self, limit=50):
279+ def getBranches(self, limit=50, eager_load=True):
280 """See `IBranchSet`."""
281 anon_branches = getUtility(IAllBranches).visibleByUser(None)
282- branches = anon_branches.scanned().getBranches()
283+ branches = anon_branches.scanned().getBranches(eager_load=eager_load)
284 branches.order_by(
285 Desc(Branch.date_last_modified), Desc(Branch.id))
286 branches.config(limit=limit)
287
288=== modified file 'lib/lp/code/model/branchcollection.py'
289--- lib/lp/code/model/branchcollection.py 2011-01-27 14:02:38 +0000
290+++ lib/lp/code/model/branchcollection.py 2011-03-01 04:23:03 +0000
291@@ -23,20 +23,26 @@
292 from zope.component import getUtility
293 from zope.interface import implements
294
295+from canonical.launchpad.components.decoratedresultset import (
296+ DecoratedResultSet,
297+ )
298 from canonical.launchpad.webapp.interfaces import (
299 DEFAULT_FLAVOR,
300 IStoreSelector,
301 MAIN_STORE,
302 )
303 from canonical.launchpad.webapp.vocabulary import CountableIterator
304+from canonical.lazr.utils import safe_hasattr
305+from lp.bugs.model.bug import Bug
306+from lp.bugs.model.bugbranch import BugBranch
307 from lp.code.interfaces.branch import user_has_special_branch_access
308 from lp.code.interfaces.branchcollection import (
309 IBranchCollection,
310 InvalidFilter,
311 )
312-
313-from lp.bugs.model.bug import Bug
314-from lp.bugs.model.bugbranch import BugBranch
315+from lp.code.interfaces.seriessourcepackagebranch import (
316+ IFindOfficialBranchLinks,
317+ )
318 from lp.code.enums import BranchMergeProposalStatus
319 from lp.code.interfaces.branchlookup import IBranchLookup
320 from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
321@@ -55,6 +61,7 @@
322 from lp.registry.model.product import Product
323 from lp.registry.model.sourcepackagename import SourcePackageName
324 from lp.registry.model.teammembership import TeamParticipation
325+from lp.services.propertycache import get_property_cache
326
327
328 class GenericBranchCollection:
329@@ -89,7 +96,7 @@
330
331 def count(self):
332 """See `IBranchCollection`."""
333- return self.getBranches().count()
334+ return self.getBranches(eager_load=False).count()
335
336 def ownerCounts(self):
337 """See `IBranchCollection`."""
338@@ -136,7 +143,7 @@
339
340 def _getBranchIdQuery(self):
341 """Return a Storm 'Select' for the branch IDs in this collection."""
342- select = self.getBranches()._get_select()
343+ select = self.getBranches(eager_load=False)._get_select()
344 select.columns = (Branch.id,)
345 return select
346
347@@ -144,11 +151,43 @@
348 """Return the where expressions for this collection."""
349 return self._branch_filter_expressions
350
351- def getBranches(self):
352+ def getBranches(self, eager_load=False):
353 """See `IBranchCollection`."""
354 tables = [Branch] + self._tables.values()
355 expressions = self._getBranchExpressions()
356- return self.store.using(*tables).find(Branch, *expressions)
357+ resultset = self.store.using(*tables).find(Branch, *expressions)
358+ if not eager_load:
359+ return resultset
360+ def do_eager_load(rows):
361+ branch_ids = set(branch.id for branch in rows)
362+ if not branch_ids:
363+ return
364+ branches = dict((branch.id, branch) for branch in rows)
365+ caches = dict((branch.id, get_property_cache(branch))
366+ for branch in rows)
367+ for cache in caches.values():
368+ if not safe_hasattr(cache, '_associatedProductSeries'):
369+ cache._associatedProductSeries = []
370+ if not safe_hasattr(cache, '_associatedSuiteSourcePackages'):
371+ cache._associatedSuiteSourcePackages = []
372+ # associatedProductSeries
373+ # Imported here to avoid circular import.
374+ from lp.registry.model.productseries import ProductSeries
375+ for productseries in self.store.find(
376+ ProductSeries,
377+ ProductSeries.branchID.is_in(branch_ids)):
378+ cache = caches[productseries.branchID]
379+ cache._associatedProductSeries.append(productseries)
380+ # associatedSuiteSourcePackages
381+ series_set = getUtility(IFindOfficialBranchLinks)
382+ # Order by the pocket to get the release one first. If changing
383+ # this be sure to also change BranchCollection.getBranches.
384+ links = series_set.findForBranches(rows).order_by(
385+ SeriesSourcePackageBranch.pocket)
386+ for link in links:
387+ cache = caches[link.branchID]
388+ cache._associatedSuiteSourcePackages.append(link)
389+ return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
390
391 def getMergeProposals(self, statuses=None, for_branches=None,
392 target_branch=None, merged_revnos=None):
393@@ -364,7 +403,7 @@
394 # of the unique name and sort based on relevance.
395 branch = self._getExactMatch(search_term)
396 if branch is not None:
397- if branch in self.getBranches():
398+ if branch in self.getBranches(eager_load=False):
399 return CountableIterator(1, [branch])
400 else:
401 return CountableIterator(0, [])
402@@ -397,7 +436,8 @@
403
404 # Get the results.
405 collection = self._filterBy([Branch.id.is_in(Union(*queries))])
406- results = collection.getBranches().order_by(Branch.name, Branch.id)
407+ results = collection.getBranches(eager_load=False).order_by(
408+ Branch.name, Branch.id)
409 return CountableIterator(results.count(), results)
410
411 def scanned(self):
412
413=== modified file 'lib/lp/code/model/branchnamespace.py'
414--- lib/lp/code/model/branchnamespace.py 2010-10-18 02:37:53 +0000
415+++ lib/lp/code/model/branchnamespace.py 2011-03-01 04:23:03 +0000
416@@ -218,7 +218,7 @@
417 name = "%s-%s" % (prefix, count)
418 return name
419
420- def getBranches(self):
421+ def getBranches(self, eager_load=False):
422 """See `IBranchNamespace`."""
423 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
424 return store.find(Branch, self._getBranchesClause())
425
426=== modified file 'lib/lp/code/model/hasbranches.py'
427--- lib/lp/code/model/hasbranches.py 2010-10-27 07:15:02 +0000
428+++ lib/lp/code/model/hasbranches.py 2011-03-01 04:23:03 +0000
429@@ -26,7 +26,7 @@
430 """A mixin implementation for `IHasBranches`."""
431
432 def getBranches(self, status=None, visible_by_user=None,
433- modified_since=None):
434+ modified_since=None, eager_load=False):
435 """See `IHasBranches`."""
436 if status is None:
437 status = DEFAULT_BRANCH_STATUS_IN_LISTING
438@@ -35,7 +35,7 @@
439 collection = collection.withLifecycleStatus(*status)
440 if modified_since is not None:
441 collection = collection.modifiedSince(modified_since)
442- return collection.getBranches()
443+ return collection.getBranches(eager_load=eager_load)
444
445
446 class HasMergeProposalsMixin:
447
448=== modified file 'lib/lp/code/model/seriessourcepackagebranch.py'
449--- lib/lp/code/model/seriessourcepackagebranch.py 2010-08-20 20:31:18 +0000
450+++ lib/lp/code/model/seriessourcepackagebranch.py 2011-03-01 04:23:03 +0000
451@@ -101,10 +101,15 @@
452
453 def findForBranch(self, branch):
454 """See `IFindOfficialBranchLinks`."""
455+ return self.findForBranches([branch])
456+
457+ def findForBranches(self, branches):
458+ """See `IFindOfficialBranchLinks`."""
459+ branch_ids = set(branch.id for branch in branches)
460 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
461 return store.find(
462 SeriesSourcePackageBranch,
463- SeriesSourcePackageBranch.branch == branch.id)
464+ SeriesSourcePackageBranch.branchID.is_in(branch_ids))
465
466 def findForSourcePackage(self, sourcepackage):
467 """See `IFindOfficialBranchLinks`."""
468
469=== modified file 'lib/lp/code/model/tests/test_branchset.py'
470--- lib/lp/code/model/tests/test_branchset.py 2010-10-04 19:50:45 +0000
471+++ lib/lp/code/model/tests/test_branchset.py 2011-03-01 04:23:03 +0000
472@@ -5,32 +5,34 @@
473
474 __metaclass__ = type
475
476-from unittest import TestLoader
477+from testtools.matchers import LessThan
478
479+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
480 from canonical.testing.layers import DatabaseFunctionalLayer
481 from lp.code.interfaces.branch import IBranchSet
482 from lp.code.model.branch import BranchSet
483-from lp.testing import TestCaseWithFactory
484+from lp.testing import (
485+ logout,
486+ TestCaseWithFactory,
487+ )
488+from lp.testing._webservice import QueryCollector
489+from lp.testing.matchers import HasQueryCount
490
491
492 class TestBranchSet(TestCaseWithFactory):
493
494 layer = DatabaseFunctionalLayer
495
496- def setUp(self):
497- TestCaseWithFactory.setUp(self)
498- self.branch_set = BranchSet()
499-
500 def test_provides_IBranchSet(self):
501 # BranchSet instances provide IBranchSet.
502- self.assertProvides(self.branch_set, IBranchSet)
503+ self.assertProvides(BranchSet(), IBranchSet)
504
505 def test_getByUrls(self):
506 # getByUrls returns a list of branches matching the list of URLs that
507 # it's given.
508 a = self.factory.makeAnyBranch()
509 b = self.factory.makeAnyBranch()
510- branches = self.branch_set.getByUrls(
511+ branches = BranchSet().getByUrls(
512 [a.bzr_identity, b.bzr_identity])
513 self.assertEqual({a.bzr_identity: a, b.bzr_identity: b}, branches)
514
515@@ -38,9 +40,21 @@
516 # If a branch cannot be found for a URL, then None appears in the list
517 # in place of the branch.
518 url = 'http://example.com/doesntexist'
519- branches = self.branch_set.getByUrls([url])
520+ branches = BranchSet().getByUrls([url])
521 self.assertEqual({url: None}, branches)
522
523-
524-def test_suite():
525- return TestLoader().loadTestsFromName(__name__)
526+ def test_api_branches_query_count(self):
527+ webservice = LaunchpadWebServiceCaller()
528+ collector = QueryCollector()
529+ collector.register()
530+ self.addCleanup(collector.unregister)
531+ # Get 'all' of the 50 branches this collection is limited to - rather
532+ # than the default in-test-suite pagination size of 5.
533+ url = "/branches?ws.size=50"
534+ logout()
535+ response = webservice.get(url,
536+ headers={'User-Agent': 'AnonNeedsThis'})
537+ self.assertEqual(response.status, 200,
538+ "Got %d for url %r with response %r" % (
539+ response.status, url, response.body))
540+ self.assertThat(collector, HasQueryCount(LessThan(13)))
541
542=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
543--- lib/lp/code/model/tests/test_branchtarget.py 2011-01-31 02:12:30 +0000
544+++ lib/lp/code/model/tests/test_branchtarget.py 2011-03-01 04:23:03 +0000
545@@ -53,7 +53,7 @@
546 # branches related to the branch target.
547 self.assertEqual(self.target.collection.getBranches().count(), 0)
548 branch = self.makeBranchForTarget()
549- branches = self.target.collection.getBranches()
550+ branches = self.target.collection.getBranches(eager_load=False)
551 self.assertEqual([branch], list(branches))
552
553 def test_retargetBranch_packageBranch(self):
554
555=== modified file 'lib/lp/code/tests/test_helpers.py'
556--- lib/lp/code/tests/test_helpers.py 2010-10-04 19:50:45 +0000
557+++ lib/lp/code/tests/test_helpers.py 2011-03-01 04:23:03 +0000
558@@ -35,6 +35,7 @@
559 self.assertIsNot(None, fooix)
560 # There should be one branch with one commit.
561 [branch] = list(
562- getUtility(IAllBranches).inProduct(fooix).getBranches())
563+ getUtility(IAllBranches).inProduct(fooix).getBranches(
564+ eager_load=False))
565 self.assertEqual(1, branch.revision_count)
566 self.assertEqual(commit_time, branch.getTipRevision().revision_date)
567
568=== modified file 'lib/lp/code/vocabularies/branch.py'
569--- lib/lp/code/vocabularies/branch.py 2010-12-01 18:58:44 +0000
570+++ lib/lp/code/vocabularies/branch.py 2011-03-01 04:23:03 +0000
571@@ -69,7 +69,7 @@
572 logged_in_user = getUtility(ILaunchBag).user
573 collection = self._getCollection().visibleByUser(logged_in_user)
574 if query is None:
575- branches = collection.getBranches()
576+ branches = collection.getBranches(eager_load=False)
577 else:
578 branches = collection.search(query)
579 return CountableIterator(branches.count(), branches, self.toTerm)
580
581=== modified file 'lib/lp/codehosting/branchdistro.py'
582--- lib/lp/codehosting/branchdistro.py 2010-11-05 14:17:11 +0000
583+++ lib/lp/codehosting/branchdistro.py 2011-03-01 04:23:03 +0000
584@@ -146,7 +146,8 @@
585 """
586 branches = getUtility(IAllBranches)
587 distroseries_branches = branches.inDistroSeries(self.old_distroseries)
588- return distroseries_branches.officialBranches().getBranches()
589+ return distroseries_branches.officialBranches().getBranches(
590+ eager_load=False)
591
592 def checkConsistentOfficialPackageBranch(self, db_branch):
593 """Check that `db_branch` is a consistent official package branch.
594
595=== modified file 'lib/lp/codehosting/scanner/mergedetection.py'
596--- lib/lp/codehosting/scanner/mergedetection.py 2010-10-08 20:25:27 +0000
597+++ lib/lp/codehosting/scanner/mergedetection.py 2011-03-01 04:23:03 +0000
598@@ -97,7 +97,7 @@
599 BranchLifecycleStatus.DEVELOPMENT,
600 BranchLifecycleStatus.EXPERIMENTAL,
601 BranchLifecycleStatus.MATURE,
602- BranchLifecycleStatus.ABANDONED).getBranches()
603+ BranchLifecycleStatus.ABANDONED).getBranches(eager_load=False)
604 for branch in branches:
605 last_scanned = branch.last_scanned_id
606 # If the branch doesn't have any revisions, not any point setting