Merge lp:~stevenk/launchpad/preload-landing_candidates into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Merged at revision: 16507
Proposed branch: lp:~stevenk/launchpad/preload-landing_candidates
Merge into: lp:launchpad
Diff against target: 502 lines (+80/-95)
3 files modified
lib/lp/code/browser/branch.py (+8/-1)
lib/lp/code/browser/tests/test_branch.py (+69/-93)
lib/lp/code/configure.zcml (+3/-1)
To merge this branch: bzr merge lp:~stevenk/launchpad/preload-landing_candidates
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+150255@code.launchpad.net

Commit message

Preload candidate branches in BranchView.landing_candidates to not cause O(n) queries when checking 1-2 branches per MP targeted to the branch being viewed.

Description of the change

Preload candidate branches in BranchView.landing_candidates before checking for launchpad.View. Since APGs and AAGs are denormalized onto Branch, we only need to load them all in one load_related call to pre-cache the access check.

Clean up a bunch of test_branch, having it use create_view or create_initialized_view rather than calling out the views manually.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2013-02-21 05:43:21 +0000
3+++ lib/lp/code/browser/branch.py 2013-02-25 05:26:21 +0000
4@@ -127,11 +127,14 @@
5 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
6 from lp.code.interfaces.branchtarget import IBranchTarget
7 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
8+from lp.code.model.branch import Branch
9+from lp.code.model.branchcollection import GenericBranchCollection
10 from lp.registry.interfaces.person import IPersonSet
11 from lp.registry.interfaces.productseries import IProductSeries
12 from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
13 from lp.services import searchbuilder
14 from lp.services.config import config
15+from lp.services.database.bulk import load_related
16 from lp.services.database.constants import UTC_NOW
17 from lp.services.feeds.browser import (
18 BranchFeedLink,
19@@ -554,7 +557,11 @@
20 @cachedproperty
21 def landing_candidates(self):
22 """Return a decorated list of landing candidates."""
23- candidates = self.context.landing_candidates
24+ candidates = list(self.context.landing_candidates)
25+ branches = load_related(
26+ Branch, candidates, ['source_branchID', 'prerequisite_branchID'])
27+ GenericBranchCollection.preloadVisibleStackedOnBranches(
28+ branches, self.user)
29 return [proposal for proposal in candidates
30 if check_permission('launchpad.View', proposal)]
31
32
33=== modified file 'lib/lp/code/browser/tests/test_branch.py'
34--- lib/lp/code/browser/tests/test_branch.py 2012-11-14 15:45:10 +0000
35+++ lib/lp/code/browser/tests/test_branch.py 2013-02-25 05:26:21 +0000
36@@ -1,4 +1,4 @@
37-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
38+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
39 # GNU Affero General Public License version 3 (see the file LICENSE).
40
41 """Unit tests for BranchView."""
42@@ -10,6 +10,8 @@
43
44 from BeautifulSoup import BeautifulSoup
45 import pytz
46+from storm.store import Store
47+from testtools.matchers import Equals
48 from zope.component import getUtility
49 from zope.publisher.interfaces import NotFound
50 from zope.security.proxy import removeSecurityProxy
51@@ -22,12 +24,7 @@
52 BugTaskStatus,
53 UNRESOLVED_BUGTASK_STATUSES,
54 )
55-from lp.code.browser.branch import (
56- BranchMirrorStatusView,
57- BranchReviewerEditView,
58- BranchView,
59- )
60-from lp.code.browser.branchlisting import PersonOwnedBranchesView
61+from lp.code.browser.branch import BranchMirrorStatusView
62 from lp.code.bzr import (
63 BranchFormat,
64 ControlFormat,
65@@ -48,6 +45,7 @@
66 login_person,
67 logout,
68 person_logged_in,
69+ StormStatementRecorder,
70 TestCaseWithFactory,
71 )
72 from lp.testing.layers import (
73@@ -57,6 +55,7 @@
74 from lp.testing.matchers import (
75 BrowsesWithQueryLimit,
76 Contains,
77+ HasQueryCount,
78 )
79 from lp.testing.pages import (
80 extract_text,
81@@ -76,7 +75,7 @@
82 layer = LaunchpadFunctionalLayer
83
84 def setUp(self):
85- TestCaseWithFactory.setUp(self)
86+ super(TestBranchMirrorHidden, self).setUp()
87 config.push(
88 "test", dedent("""\
89 [codehosting]
90@@ -85,15 +84,14 @@
91
92 def tearDown(self):
93 config.pop("test")
94- TestCaseWithFactory.tearDown(self)
95+ super(TestBranchMirrorHidden, self).tearDown()
96
97 def testNormalBranch(self):
98 # A branch from a normal location is fine.
99 branch = self.factory.makeAnyBranch(
100 branch_type=BranchType.MIRRORED,
101 url="http://example.com/good/mirror")
102- view = BranchView(branch, LaunchpadTestRequest())
103- view.initialize()
104+ view = create_initialized_view(branch, '+index')
105 self.assertTrue(view.user is None)
106 self.assertEqual(
107 "http://example.com/good/mirror", view.mirror_location)
108@@ -101,10 +99,8 @@
109 def testLocationlessRemoteBranch(self):
110 # A branch from a normal location is fine.
111 branch = self.factory.makeAnyBranch(
112- branch_type=BranchType.REMOTE,
113- url=None)
114- view = BranchView(branch, LaunchpadTestRequest())
115- view.initialize()
116+ branch_type=BranchType.REMOTE, url=None)
117+ view = create_initialized_view(branch, '+index')
118 self.assertTrue(view.user is None)
119 self.assertIs(None, view.mirror_location)
120
121@@ -114,11 +110,9 @@
122 branch = self.factory.makeAnyBranch(
123 branch_type=BranchType.MIRRORED,
124 url="http://private.example.com/bzr-mysql/mysql-5.0")
125- view = BranchView(branch, LaunchpadTestRequest())
126- view.initialize()
127+ view = create_initialized_view(branch, '+index')
128 self.assertTrue(view.user is None)
129- self.assertEqual(
130- "<private server>", view.mirror_location)
131+ self.assertEqual("<private server>", view.mirror_location)
132
133 def testHiddenBranchAsBranchOwner(self):
134 # A branch location with a defined private host is visible to the
135@@ -126,12 +120,10 @@
136 owner = self.factory.makePerson(email="eric@example.com")
137 branch = self.factory.makeAnyBranch(
138 branch_type=BranchType.MIRRORED,
139- owner=owner,
140- url="http://private.example.com/bzr-mysql/mysql-5.0")
141+ owner=owner, url="http://private.example.com/bzr-mysql/mysql-5.0")
142 # Now log in the owner.
143 login('eric@example.com')
144- view = BranchView(branch, LaunchpadTestRequest())
145- view.initialize()
146+ view = create_initialized_view(branch, '+index')
147 self.assertEqual(view.user, owner)
148 self.assertEqual(
149 "http://private.example.com/bzr-mysql/mysql-5.0",
150@@ -143,33 +135,26 @@
151 owner = self.factory.makePerson(email="eric@example.com")
152 other = self.factory.makePerson(email="other@example.com")
153 branch = self.factory.makeAnyBranch(
154- branch_type=BranchType.MIRRORED,
155- owner=owner,
156+ branch_type=BranchType.MIRRORED, owner=owner,
157 url="http://private.example.com/bzr-mysql/mysql-5.0")
158 # Now log in the other person.
159 login('other@example.com')
160- view = BranchView(branch, LaunchpadTestRequest())
161- view.initialize()
162+ view = create_initialized_view(branch, '+index')
163 self.assertEqual(view.user, other)
164- self.assertEqual(
165- "<private server>", view.mirror_location)
166+ self.assertEqual("<private server>", view.mirror_location)
167
168
169 class TestBranchView(BrowserTestCase):
170
171 layer = DatabaseFunctionalLayer
172
173- def setUp(self):
174- super(TestBranchView, self).setUp()
175- self.request = LaunchpadTestRequest()
176-
177 def testMirrorStatusMessageIsTruncated(self):
178 """mirror_status_message is truncated if the text is overly long."""
179 branch = self.factory.makeBranch(branch_type=BranchType.MIRRORED)
180 branch.mirrorFailed(
181 "on quick brown fox the dog jumps to" *
182 BranchMirrorStatusView.MAXIMUM_STATUS_MESSAGE_LENGTH)
183- branch_view = BranchMirrorStatusView(branch, self.request)
184+ branch_view = create_view(branch, '+mirror-status')
185 self.assertEqual(
186 truncate_text(branch.mirror_status_message,
187 branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH) + ' ...',
188@@ -179,7 +164,7 @@
189 """mirror_status_message on the view is the same as on the branch."""
190 branch = self.factory.makeBranch(branch_type=BranchType.MIRRORED)
191 branch.mirrorFailed("This is a short error message.")
192- branch_view = BranchMirrorStatusView(branch, self.request)
193+ branch_view = create_view(branch, '+mirror-status')
194 self.assertTrue(
195 len(branch.mirror_status_message)
196 <= branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH,
197@@ -194,18 +179,16 @@
198 def testShowMergeLinksOnManyBranchProject(self):
199 # The merge links are shown on projects that have multiple branches.
200 product = self.factory.makeProduct(name='super-awesome-project')
201- branch1 = self.factory.makeAnyBranch(product=product)
202+ branch = self.factory.makeAnyBranch(product=product)
203 self.factory.makeAnyBranch(product=product)
204- view = BranchView(branch1, self.request)
205- view.initialize()
206+ view = create_initialized_view(branch, '+index')
207 self.assertTrue(view.show_merge_links)
208
209 def testShowMergeLinksOnJunkBranch(self):
210 # The merge links are not shown on junk branches because they do not
211 # support merge proposals.
212 junk_branch = self.factory.makeBranch(product=None)
213- view = BranchView(junk_branch, self.request)
214- view.initialize()
215+ view = create_initialized_view(junk_branch, '+index')
216 self.assertFalse(view.show_merge_links)
217
218 def testShowMergeLinksOnSingleBranchProject(self):
219@@ -213,17 +196,14 @@
220 # only has one branch because it's pointless to propose it for merging
221 # if there's nothing to merge into.
222 branch = self.factory.makeAnyBranch()
223- view = BranchView(branch, self.request)
224- view.initialize()
225+ view = create_initialized_view(branch, '+index')
226 self.assertFalse(view.show_merge_links)
227
228 def testNoProductSeriesPushingTranslations(self):
229 # By default, a branch view shows no product series pushing
230 # translations to the branch.
231 branch = self.factory.makeBranch()
232-
233- view = BranchView(branch, self.request)
234- view.initialize()
235+ view = create_initialized_view(branch, '+index')
236 self.assertEqual(list(view.translations_sources()), [])
237
238 def testProductSeriesPushingTranslations(self):
239@@ -233,16 +213,13 @@
240 trunk = product.getSeries('trunk')
241 branch = self.factory.makeBranch(owner=product.owner)
242 removeSecurityProxy(trunk).translations_branch = branch
243-
244- view = BranchView(branch, self.request)
245- view.initialize()
246+ view = create_initialized_view(branch, '+index')
247 self.assertEqual(list(view.translations_sources()), [trunk])
248
249 def test_is_empty_directory(self):
250 # Branches are considered empty until they get a control format.
251 branch = self.factory.makeBranch()
252- view = BranchView(branch, self.request)
253- view.initialize()
254+ view = create_initialized_view(branch, '+index')
255 self.assertTrue(view.is_empty_directory)
256 with person_logged_in(branch.owner):
257 # Make it look as though the branch has been pushed.
258@@ -541,6 +518,24 @@
259 browser = self.getUserBrowser(url, user=user)
260 self.assertIn(product_name, browser.contents)
261
262+ def test_query_count_landing_candidates(self):
263+ product = self.factory.makeProduct()
264+ branch = self.factory.makeBranch(product=product)
265+ for i in range(10):
266+ self.factory.makeBranchMergeProposal(target_branch=branch)
267+ stacked = self.factory.makeBranch(product=product)
268+ source = self.factory.makeBranch(stacked_on=stacked, product=product)
269+ prereq = self.factory.makeBranch(product=product)
270+ self.factory.makeBranchMergeProposal(
271+ source_branch=source, target_branch=branch,
272+ prerequisite_branch=prereq)
273+ Store.of(branch).flush()
274+ Store.of(branch).invalidate()
275+ view = create_view(branch, '+index')
276+ with StormStatementRecorder() as recorder:
277+ view.landing_candidates
278+ self.assertThat(recorder, HasQueryCount(Equals(5)))
279+
280
281 class TestBranchViewPrivateArtifacts(BrowserTestCase):
282 """ Tests that branches with private team artifacts can be viewed.
283@@ -733,28 +728,23 @@
284 # the branch.
285 branch = self.factory.makeAnyBranch()
286 self.assertIs(None, branch.reviewer)
287- view = BranchReviewerEditView(branch, LaunchpadTestRequest())
288- self.assertEqual(
289- branch.owner,
290- view.initial_values['reviewer'])
291+ view = create_view(branch, '+reviewer')
292+ self.assertEqual(branch.owner, view.initial_values['reviewer'])
293
294 def test_initial_reviewer_set(self):
295 # If the reviewer has been set, it is shown as the initial value.
296 branch = self.factory.makeAnyBranch()
297 login_person(branch.owner)
298 branch.reviewer = self.factory.makePerson()
299- view = BranchReviewerEditView(branch, LaunchpadTestRequest())
300- self.assertEqual(
301- branch.reviewer,
302- view.initial_values['reviewer'])
303+ view = create_view(branch, '+reviewer')
304+ self.assertEqual(branch.reviewer, view.initial_values['reviewer'])
305
306 def test_set_reviewer(self):
307 # Test setting the reviewer.
308 branch = self.factory.makeAnyBranch()
309 reviewer = self.factory.makePerson()
310 login_person(branch.owner)
311- view = BranchReviewerEditView(branch, LaunchpadTestRequest())
312- view.initialize()
313+ view = create_initialized_view(branch, '+reviewer')
314 view.change_action.success({'reviewer': reviewer})
315 self.assertEqual(reviewer, branch.reviewer)
316 # Last modified has been updated.
317@@ -767,8 +757,7 @@
318 branch = self.factory.makeAnyBranch()
319 login_person(branch.owner)
320 branch.reviewer = self.factory.makePerson()
321- view = BranchReviewerEditView(branch, LaunchpadTestRequest())
322- view.initialize()
323+ view = create_initialized_view(branch, '+reviewer')
324 view.change_action.success({'reviewer': branch.owner})
325 self.assertIs(None, branch.reviewer)
326 # Last modified has been updated.
327@@ -781,8 +770,7 @@
328 # modified is not updated.
329 modified_date = datetime(2007, 1, 1, tzinfo=pytz.UTC)
330 branch = self.factory.makeAnyBranch(date_created=modified_date)
331- view = BranchReviewerEditView(branch, LaunchpadTestRequest())
332- view.initialize()
333+ view = create_initialized_view(branch, '+reviewer')
334 view.change_action.success({'reviewer': branch.owner})
335 self.assertIs(None, branch.reviewer)
336 # Last modified has not been updated.
337@@ -803,8 +791,8 @@
338 # the development focus branch.
339 login_person(product.owner)
340 product.development_focus.branch = branch
341- view = PersonOwnedBranchesView(branch.owner, LaunchpadTestRequest())
342- view.initialize()
343+ view = create_initialized_view(
344+ branch.owner, '+ownedbranches', rootsite='code')
345 navigator = view.branches()
346 [decorated_branch] = navigator.branches
347 self.assertEqual("lp://dev/fooix", decorated_branch.bzr_identity)
348@@ -815,15 +803,12 @@
349
350 layer = DatabaseFunctionalLayer
351
352- def setUp(self):
353- TestCaseWithFactory.setUp(self)
354-
355 def test_public_target(self):
356 # If the user can see the target, then there are merges, and the
357 # landing_target is available for the template rendering.
358 bmp = self.factory.makeBranchMergeProposal()
359 branch = bmp.source_branch
360- view = BranchView(branch, LaunchpadTestRequest())
361+ view = create_view(branch, '+index')
362 self.assertFalse(view.no_merges)
363 [target] = view.landing_targets
364 # Check the ids as the target is a DecoratedMergeProposal.
365@@ -835,7 +820,7 @@
366 branch = bmp.source_branch
367 removeSecurityProxy(bmp.target_branch).information_type = (
368 InformationType.USERDATA)
369- view = BranchView(branch, LaunchpadTestRequest())
370+ view = create_view(branch, '+index')
371 self.assertTrue(view.no_merges)
372 self.assertEqual([], view.landing_targets)
373
374@@ -844,7 +829,7 @@
375 # landing_candidate is available for the template rendering.
376 bmp = self.factory.makeBranchMergeProposal()
377 branch = bmp.target_branch
378- view = BranchView(branch, LaunchpadTestRequest())
379+ view = create_view(branch, '+index')
380 self.assertFalse(view.no_merges)
381 [candidate] = view.landing_candidates
382 # Check the ids as the target is a DecoratedMergeProposal.
383@@ -857,7 +842,7 @@
384 branch = bmp.target_branch
385 removeSecurityProxy(bmp.source_branch).information_type = (
386 InformationType.USERDATA)
387- view = BranchView(branch, LaunchpadTestRequest())
388+ view = create_view(branch, '+index')
389 self.assertTrue(view.no_merges)
390 self.assertEqual([], view.landing_candidates)
391
392@@ -866,7 +851,7 @@
393 # there are merges.
394 branch = self.factory.makeProductBranch()
395 bmp = self.factory.makeBranchMergeProposal(prerequisite_branch=branch)
396- view = BranchView(branch, LaunchpadTestRequest())
397+ view = create_view(branch, '+index')
398 self.assertFalse(view.no_merges)
399 [proposal] = view.dependent_branches
400 self.assertEqual(bmp, proposal)
401@@ -878,7 +863,7 @@
402 bmp = self.factory.makeBranchMergeProposal(prerequisite_branch=branch)
403 removeSecurityProxy(bmp.source_branch).information_type = (
404 InformationType.USERDATA)
405- view = BranchView(branch, LaunchpadTestRequest())
406+ view = create_view(branch, '+index')
407 self.assertTrue(view.no_merges)
408 self.assertEqual([], view.dependent_branches)
409
410@@ -1009,8 +994,7 @@
411 browser.getControl("Change Branch").click()
412 self.assertThat(
413 browser.contents,
414- Contains(
415- 'Public branches are not allowed for target Commercial.'))
416+ Contains('Public branches are not allowed for target Commercial.'))
417 with person_logged_in(owner):
418 self.assertEquals(initial_target, branch.target.context)
419
420@@ -1026,8 +1010,7 @@
421 browser.getControl("Private", index=1).click()
422 browser.getControl("Change Branch").click()
423 with person_logged_in(person):
424- self.assertEqual(
425- InformationType.USERDATA, branch.information_type)
426+ self.assertEqual(InformationType.USERDATA, branch.information_type)
427
428 def test_can_not_change_privacy_of_stacked_on_private(self):
429 # The privacy field is not shown if the branch is stacked on a
430@@ -1042,8 +1025,7 @@
431 with person_logged_in(owner):
432 browser = self.getUserBrowser(
433 canonical_url(branch) + '/+edit', user=owner)
434- self.assertRaises(
435- LookupError, browser.getControl, "Information Type")
436+ self.assertRaises(LookupError, browser.getControl, "Information Type")
437
438 def test_edit_view_ajax_render(self):
439 # An information type change request is processed as expected when an
440@@ -1058,11 +1040,10 @@
441 'field.information_type': 'PUBLICSECURITY'},
442 **extra)
443 with person_logged_in(person):
444- view = create_view(
445+ view = create_initialized_view(
446 branch, name='+edit-information-type',
447 request=request, principal=person)
448 request.traversed_objects = [person, branch.product, branch, view]
449- view.initialize()
450 result = view.render()
451 self.assertEqual('', result)
452 self.assertEqual(
453@@ -1088,10 +1069,8 @@
454 branch = self.factory.makeBranch(
455 information_type=InformationType.PUBLIC)
456 self.assertShownTypes(
457- [InformationType.PUBLIC,
458- InformationType.PUBLICSECURITY,
459- InformationType.PRIVATESECURITY,
460- InformationType.USERDATA],
461+ [InformationType.PUBLIC, InformationType.PUBLICSECURITY,
462+ InformationType.PRIVATESECURITY, InformationType.USERDATA],
463 branch)
464
465 def test_branch_with_disallowed_type(self):
466@@ -1103,12 +1082,9 @@
467 branch = self.factory.makeBranch(
468 product=product, information_type=InformationType.PROPRIETARY)
469 self.assertShownTypes(
470- [InformationType.PUBLIC,
471- InformationType.PUBLICSECURITY,
472- InformationType.PRIVATESECURITY,
473- InformationType.USERDATA,
474- InformationType.PROPRIETARY],
475- branch)
476+ [InformationType.PUBLIC, InformationType.PUBLICSECURITY,
477+ InformationType.PRIVATESECURITY, InformationType.USERDATA,
478+ InformationType.PROPRIETARY], branch)
479
480 def test_stacked_on_private(self):
481 # A branch stacked on a private branch has its choices limited
482
483=== modified file 'lib/lp/code/configure.zcml'
484--- lib/lp/code/configure.zcml 2013-01-22 02:06:41 +0000
485+++ lib/lp/code/configure.zcml 2013-02-25 05:26:21 +0000
486@@ -1,4 +1,4 @@
487-<!-- Copyright 2009-2011 Canonical Ltd. This software is licensed under the
488+<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the
489 GNU Affero General Public License version 3 (see the file LICENSE).
490 -->
491
492@@ -223,8 +223,10 @@
493 id
494 registrant
495 source_branch
496+ source_branchID
497 target_branch
498 prerequisite_branch
499+ prerequisite_branchID
500 description
501 whiteboard
502 queue_status