Merge lp:~thumper/launchpad/different-cloud into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 11475
Proposed branch: lp:~thumper/launchpad/different-cloud
Merge into: lp:launchpad
Diff against target: 685 lines (+248/-193)
12 files modified
lib/lp/code/browser/bazaar.py (+27/-27)
lib/lp/code/configure.zcml (+1/-1)
lib/lp/code/interfaces/branch.py (+6/-3)
lib/lp/code/model/branch.py (+0/-31)
lib/lp/code/model/branchcloud.py (+52/-0)
lib/lp/code/model/revision.py (+1/-2)
lib/lp/code/model/tests/test_branchcloud.py (+65/-95)
lib/lp/code/stories/branches/xx-bazaar-home.txt (+0/-1)
lib/lp/code/stories/branches/xx-branch-tag-cloud.txt (+15/-26)
lib/lp/code/tests/helpers.py (+40/-0)
lib/lp/code/tests/test_helpers.py (+40/-0)
lib/lp/testing/tests/test_standard_test_template.py (+1/-7)
To merge this branch: bzr merge lp:~thumper/launchpad/different-cloud
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+34146@code.launchpad.net

Commit message

Fix the branch cloud timeouts.

Description of the change

Change what we show on the tag cloud for branches.

Instead of showing general total branch counts (hosted and mirrored), we instead show projects that have recent commits (recent being in the last 30 days and using the revision cache table).

By using the revision cache, we get *much* faster queries, and this should completely kill any time-outs we are getting for this page (crosses-fingers).

Tests:
  xx-branch-tag-cloud
  code.tests.test_helpers
  code.model.tests.test_branchcloud

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

doit.

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

We may need to adjust this to normalise in some fashion but this is certainly an improvement.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/bazaar.py'
2--- lib/lp/code/browser/bazaar.py 2010-08-20 20:31:18 +0000
3+++ lib/lp/code/browser/bazaar.py 2010-08-31 01:22:44 +0000
4@@ -93,17 +93,17 @@
5
6 class ProductInfo:
7
8- def __init__(
9- self, product_name, num_branches, branch_size, elapsed):
10- self.name = product_name
11- self.url = '/' + product_name
12- self.num_branches = num_branches
13- self.branch_size = branch_size
14+ def __init__(self, name, commits, author_count, size, elapsed):
15+ self.name = name
16+ self.url = '/' + name
17+ self.commits = commits
18+ self.author_count = author_count
19+ self.size = size
20 self.elapsed_since_commit = elapsed
21
22 @property
23- def branch_class(self):
24- return "cloud-size-%s" % self.branch_size
25+ def tag_class(self):
26+ return "cloud-size-%s" % self.size
27
28 @property
29 def time_darkness(self):
30@@ -111,30 +111,32 @@
31 return "light"
32 if self.elapsed_since_commit.days < 7:
33 return "dark"
34- if self.elapsed_since_commit.days < 31:
35+ if self.elapsed_since_commit.days < 14:
36 return "medium"
37 return "light"
38
39 @property
40 def html_class(self):
41- return "%s cloud-%s" % (self.branch_class, self.time_darkness)
42+ return "%s cloud-%s" % (self.tag_class, self.time_darkness)
43
44 @property
45 def html_title(self):
46- if self.num_branches == 1:
47- size = "1 branch"
48- else:
49- size = "%d branches" % self.num_branches
50- if self.elapsed_since_commit is None:
51- commit = "no commits yet"
52- elif self.elapsed_since_commit.days == 0:
53+ if self.commits == 1:
54+ size = "1 commit"
55+ else:
56+ size = "%d commits" % self.commits
57+ if self.author_count == 1:
58+ who = "1 person"
59+ else:
60+ who = "%s people" % self.author_count
61+ if self.elapsed_since_commit.days == 0:
62 commit = "last commit less than a day old"
63 elif self.elapsed_since_commit.days == 1:
64 commit = "last commit one day old"
65 else:
66 commit = (
67 "last commit %d days old" % self.elapsed_since_commit.days)
68- return "%s, %s" % (size, commit)
69+ return "%s by %s, %s" % (size, who, commit)
70
71
72 class BazaarProjectsRedirect(LaunchpadView):
73@@ -177,7 +179,9 @@
74 # is the first item of the tuple returned, and is guaranteed to be
75 # unique by the sql query.
76 product_info = sorted(
77- list(getUtility(IBranchCloud).getProductsWithInfo(num_products)))
78+ getUtility(IBranchCloud).getProductsWithInfo(num_products))
79+ if len(product_info) == 0:
80+ return
81 now = datetime.today()
82 counts = sorted(zip(*product_info)[1])
83 size_mapping = {
84@@ -187,14 +191,10 @@
85 0.8: 'large',
86 1.0: 'largest',
87 }
88- num_branches_to_size = self._make_distribution_map(
89+ num_commits_to_size = self._make_distribution_map(
90 counts, size_mapping)
91
92- for product_name, num_branches, last_revision_date in product_info:
93- # Projects with no branches are not interesting.
94- if num_branches == 0:
95- continue
96- branch_size = num_branches_to_size[num_branches]
97+ for name, commits, author_count, last_revision_date in product_info:
98+ size = num_commits_to_size[commits]
99 elapsed = now - last_revision_date
100- yield ProductInfo(
101- product_name, num_branches, branch_size, elapsed)
102+ yield ProductInfo(name, commits, author_count, size, elapsed)
103
104=== modified file 'lib/lp/code/configure.zcml'
105--- lib/lp/code/configure.zcml 2010-08-19 03:01:51 +0000
106+++ lib/lp/code/configure.zcml 2010-08-31 01:22:44 +0000
107@@ -568,7 +568,7 @@
108 <allow interface="lp.code.interfaces.branch.IBranchDelta"/>
109 </class>
110 <securedutility
111- class="lp.code.model.branch.BranchCloud"
112+ class="lp.code.model.branchcloud.BranchCloud"
113 provides="lp.code.interfaces.branch.IBranchCloud">
114 <allow interface="lp.code.interfaces.branch.IBranchCloud"/>
115 </securedutility>
116
117=== modified file 'lib/lp/code/interfaces/branch.py'
118--- lib/lp/code/interfaces/branch.py 2010-08-20 20:31:18 +0000
119+++ lib/lp/code/interfaces/branch.py 2010-08-31 01:22:44 +0000
120@@ -1277,9 +1277,12 @@
121 """
122
123 def getProductsWithInfo(num_products=None):
124- """Get products with their branch activity information.
125-
126- :return: a `ResultSet` of (product, num_branches, last_revision_date).
127+ """Get products with their recent activity information.
128+
129+ The counts are for the last 30 days.
130+
131+ :return: a `ResultSet` of (product, num_commits, num_authors,
132+ last_revision_date).
133 """
134
135
136
137=== modified file 'lib/lp/code/model/branch.py'
138--- lib/lp/code/model/branch.py 2010-08-20 20:31:18 +0000
139+++ lib/lp/code/model/branch.py 2010-08-31 01:22:44 +0000
140@@ -1306,37 +1306,6 @@
141 return branches
142
143
144-class BranchCloud:
145- """See `IBranchCloud`."""
146-
147- def getProductsWithInfo(self, num_products=None, store_flavor=None):
148- """See `IBranchCloud`."""
149- # Circular imports are fun.
150- from lp.registry.model.product import Product
151- # It doesn't matter if this query is even a whole day out of date, so
152- # use the slave store by default.
153- if store_flavor is None:
154- store_flavor = SLAVE_FLAVOR
155- store = getUtility(IStoreSelector).get(MAIN_STORE, store_flavor)
156- # Get all products, the count of all hosted & mirrored branches and
157- # the last revision date.
158- result = store.find(
159- (Product.name, Count(Branch.id), Max(Revision.revision_date)),
160- Branch.private == False,
161- Branch.product == Product.id,
162- Or(Branch.branch_type == BranchType.HOSTED,
163- Branch.branch_type == BranchType.MIRRORED),
164- Branch.last_scanned_id == Revision.revision_id)
165- result = result.group_by(Product.name)
166- result = result.order_by(Desc(Count(Branch.id)))
167- if num_products:
168- result.config(limit=num_products)
169- # XXX: JonathanLange 2009-02-10: The revision date in the result set
170- # isn't timezone-aware. Not sure why this is. Doesn't matter too much
171- # for the purposes of cloud calculation though.
172- return result
173-
174-
175 def update_trigger_modified_fields(branch):
176 """Make the trigger updated fields reload when next accessed."""
177 # Not all the fields are exposed through the interface, and some are read
178
179=== added file 'lib/lp/code/model/branchcloud.py'
180--- lib/lp/code/model/branchcloud.py 1970-01-01 00:00:00 +0000
181+++ lib/lp/code/model/branchcloud.py 2010-08-31 01:22:44 +0000
182@@ -0,0 +1,52 @@
183+# Copyright 2010 Canonical Ltd. This software is licensed under the
184+# GNU Affero General Public License version 3 (see the file LICENSE).
185+
186+"""The implementation of the branch cloud."""
187+
188+__metaclass__ = type
189+__all__ = [
190+ 'BranchCloud',
191+ ]
192+
193+
194+from datetime import datetime, timedelta
195+
196+import pytz
197+from storm.expr import Alias, Func
198+from storm.locals import Count, Desc, Max, Not
199+from zope.interface import classProvides
200+
201+from canonical.launchpad.interfaces.lpstorm import ISlaveStore
202+
203+from lp.code.interfaces.branch import IBranchCloud
204+from lp.code.model.revision import RevisionCache
205+from lp.registry.model.product import Product
206+
207+
208+class BranchCloud:
209+ """See `IBranchCloud`."""
210+
211+ classProvides(IBranchCloud)
212+
213+ @staticmethod
214+ def getProductsWithInfo(num_products=None):
215+ """See `IBranchCloud`."""
216+ distinct_revision_author = Func(
217+ "distinct", RevisionCache.revision_author_id)
218+ commits = Alias(Count(RevisionCache.revision_id))
219+ epoch = datetime.now(pytz.UTC) - timedelta(days=30)
220+ # It doesn't matter if this query is even a whole day out of date, so
221+ # use the slave store.
222+ result = ISlaveStore(RevisionCache).find(
223+ (Product.name,
224+ commits,
225+ Count(distinct_revision_author),
226+ Max(RevisionCache.revision_date)),
227+ RevisionCache.product == Product.id,
228+ Not(RevisionCache.private),
229+ RevisionCache.revision_date >= epoch)
230+ result = result.group_by(Product.name)
231+ result = result.order_by(Desc(commits))
232+ if num_products:
233+ result.config(limit=num_products)
234+ return result
235
236=== modified file 'lib/lp/code/model/revision.py'
237--- lib/lp/code/model/revision.py 2010-08-20 20:31:18 +0000
238+++ lib/lp/code/model/revision.py 2010-08-31 01:22:44 +0000
239@@ -609,8 +609,7 @@
240 revision_author_id = Int(name='revision_author', allow_none=False)
241 revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
242
243- revision_date = DateTime(
244- name='revision_date', allow_none=False, tzinfo=pytz.UTC)
245+ revision_date = UtcDateTimeCol(notNull=True)
246
247 product_id = Int(name='product', allow_none=True)
248 product = Reference(product_id, 'Product.id')
249
250=== modified file 'lib/lp/code/model/tests/test_branchcloud.py'
251--- lib/lp/code/model/tests/test_branchcloud.py 2010-08-20 20:31:18 +0000
252+++ lib/lp/code/model/tests/test_branchcloud.py 2010-08-31 01:22:44 +0000
253@@ -5,27 +5,22 @@
254
255 __metaclass__ = type
256
257-from datetime import (
258- datetime,
259- timedelta,
260- )
261+from datetime import datetime, timedelta
262+import transaction
263 import unittest
264
265 import pytz
266+from storm.locals import Store
267 from zope.component import getUtility
268-from zope.security.proxy import removeSecurityProxy
269
270 from canonical.launchpad.testing.databasehelpers import (
271 remove_all_sample_data_branches,
272 )
273-from canonical.launchpad.webapp.interfaces import MASTER_FLAVOR
274 from canonical.testing.layers import DatabaseFunctionalLayer
275-from lp.code.enums import BranchType
276 from lp.code.interfaces.branch import IBranchCloud
277-from lp.testing import (
278- TestCaseWithFactory,
279- time_counter,
280- )
281+from lp.code.model.revision import RevisionCache
282+from lp.code.tests.helpers import make_project_branch_with_revisions
283+from lp.testing import TestCaseWithFactory, time_counter
284
285
286 class TestBranchCloud(TestCaseWithFactory):
287@@ -39,40 +34,40 @@
288
289 def getProductsWithInfo(self, num_products=None):
290 """Get product cloud information."""
291- # We use the MASTER_FLAVOR so that data changes made in these tests
292- # are visible to the query in getProductsWithInfo. The default
293- # implementation uses the SLAVE_FLAVOR.
294- return self._branch_cloud.getProductsWithInfo(
295- num_products, store_flavor=MASTER_FLAVOR)
296+ # Since we use the slave store to get the information, we need to
297+ # commit the transaction to make the information visible to the slave.
298+ transaction.commit()
299+ cloud_info = self._branch_cloud.getProductsWithInfo(num_products)
300+ # The last commit time is timezone unaware as the storm Max function
301+ # doesn't take into account the type that it is aggregating, so whack
302+ # the UTC tz on it here for easier comparing in the tests.
303+ def add_utc(value):
304+ return value.replace(tzinfo=pytz.UTC)
305+ return [
306+ (name, commits, authors, add_utc(last_commit))
307+ for name, commits, authors, last_commit in cloud_info]
308
309- def makeBranch(self, product=None, branch_type=None,
310- last_commit_date=None, private=False):
311+ def makeBranch(self, product=None, last_commit_date=None, private=False,
312+ revision_count=None):
313 """Make a product branch with a particular last commit date"""
314- revision_count = 5
315+ if revision_count is None:
316+ revision_count = 5
317 delta = timedelta(days=1)
318 if last_commit_date is None:
319- date_generator = None
320+ # By default we create revisions that are within the last 30 days.
321+ date_generator = time_counter(
322+ datetime.now(pytz.UTC) - timedelta(days=25), delta)
323 else:
324 start_date = last_commit_date - delta * (revision_count - 1)
325- # The output of getProductsWithInfo doesn't include timezone
326- # information -- not sure why. To make the tests a little clearer,
327- # this method expects last_commit_date to be a naive datetime that
328- # can be compared directly with the output of getProductsWithInfo.
329- start_date = start_date.replace(tzinfo=pytz.UTC)
330 date_generator = time_counter(start_date, delta)
331- branch = self.factory.makeProductBranch(
332- product=product, branch_type=branch_type, private=private)
333- if branch_type != BranchType.REMOTE:
334- self.factory.makeRevisionsForBranch(
335- removeSecurityProxy(branch), count=revision_count,
336- date_generator=date_generator)
337+ branch = make_project_branch_with_revisions(
338+ self.factory, date_generator, product, private, revision_count)
339 return branch
340
341 def test_empty_with_no_branches(self):
342 # getProductsWithInfo returns an empty result set if there are no
343 # branches in the database.
344- products_with_info = self.getProductsWithInfo()
345- self.assertEqual([], list(products_with_info))
346+ self.assertEqual([], self.getProductsWithInfo())
347
348 def test_empty_products_not_counted(self):
349 # getProductsWithInfo doesn't include products that don't have any
350@@ -80,76 +75,51 @@
351 #
352 # Note that this is tested implicitly by test_empty_with_no_branches,
353 # since there are such products in the sample data.
354- product = self.factory.makeProduct()
355- products_with_info = self.getProductsWithInfo()
356- self.assertEqual([], list(products_with_info))
357+ self.factory.makeProduct()
358+ self.assertEqual([], self.getProductsWithInfo())
359
360 def test_empty_branches_not_counted(self):
361 # getProductsWithInfo doesn't consider branches that lack revision
362 # data, 'empty branches', to contribute to the count of branches on a
363 # product.
364- branch = self.factory.makeProductBranch()
365- products_with_info = self.getProductsWithInfo()
366- self.assertEqual([], list(products_with_info))
367-
368- def test_import_branches_not_counted(self):
369- # getProductsWithInfo doesn't consider imported branches to contribute
370- # to the count of branches on a product.
371- branch = self.makeBranch(branch_type=BranchType.IMPORTED)
372- products_with_info = self.getProductsWithInfo()
373- self.assertEqual([], list(products_with_info))
374-
375- def test_remote_branches_not_counted(self):
376- # getProductsWithInfo doesn't consider remote branches to contribute
377- # to the count of branches on a product.
378- branch = self.makeBranch(branch_type=BranchType.REMOTE)
379- products_with_info = self.getProductsWithInfo()
380- self.assertEqual([], list(products_with_info))
381+ self.factory.makeProductBranch()
382+ self.assertEqual([], self.getProductsWithInfo())
383
384 def test_private_branches_not_counted(self):
385 # getProductsWithInfo doesn't count private branches.
386- branch = self.makeBranch(private=True)
387- products_with_info = self.getProductsWithInfo()
388- self.assertEqual([], list(products_with_info))
389-
390- def test_hosted_and_mirrored_counted(self):
391- # getProductsWithInfo includes products that have hosted or mirrored
392- # branches with revisions.
393- product = self.factory.makeProduct()
394- self.makeBranch(product=product, branch_type=BranchType.HOSTED)
395- last_commit_date = datetime(2007, 1, 5)
396- self.makeBranch(
397- product=product, branch_type=BranchType.MIRRORED,
398- last_commit_date=last_commit_date)
399- products_with_info = self.getProductsWithInfo()
400- self.assertEqual(
401- [(product.name, 2, last_commit_date)], list(products_with_info))
402-
403- def test_includes_products_with_branches_with_revisions(self):
404- # getProductsWithInfo includes all products that have branches with
405- # revisions.
406- last_commit_date = datetime(2008, 12, 25)
407- branch = self.makeBranch(last_commit_date=last_commit_date)
408- products_with_info = self.getProductsWithInfo()
409- self.assertEqual(
410- [(branch.product.name, 1, last_commit_date)],
411- list(products_with_info))
412-
413- def test_uses_latest_revision_date(self):
414- # getProductsWithInfo uses the most recent revision date from all the
415- # branches in that product.
416- product = self.factory.makeProduct()
417- self.makeBranch(
418- product=product, last_commit_date=datetime(2008, 12, 25))
419- last_commit_date = datetime(2009, 01, 01)
420+ self.makeBranch(private=True)
421+ self.assertEqual([], self.getProductsWithInfo())
422+
423+ def test_revisions_counted(self):
424+ # getProductsWithInfo includes products that public revisions.
425+ last_commit_date = datetime.now(pytz.UTC) - timedelta(days=5)
426+ product = self.factory.makeProduct()
427 self.makeBranch(product=product, last_commit_date=last_commit_date)
428- products_with_info = self.getProductsWithInfo()
429- self.assertEqual(
430- [(product.name, 2, last_commit_date)], list(products_with_info))
431-
432- def test_sorted_by_branch_count(self):
433+ self.assertEqual(
434+ [(product.name, 5, 1, last_commit_date)],
435+ self.getProductsWithInfo())
436+
437+ def test_only_recent_revisions_counted(self):
438+ # If the revision cache has revisions for the project, but they are
439+ # over 30 days old, we don't count them.
440+ product = self.factory.makeProduct()
441+ date_generator = time_counter(
442+ datetime.now(pytz.UTC) - timedelta(days=33),
443+ delta=timedelta(days=2))
444+ store = Store.of(product)
445+ for i in range(4):
446+ revision = self.factory.makeRevision(
447+ revision_date=date_generator.next())
448+ cache = RevisionCache(revision)
449+ cache.product = product
450+ store.add(cache)
451+ self.assertEqual(
452+ [(product.name, 2, 2, revision.revision_date)],
453+ self.getProductsWithInfo())
454+
455+ def test_sorted_by_commit_count(self):
456 # getProductsWithInfo returns a result set sorted so that the products
457- # with the most branches come first.
458+ # with the most commits come first.
459 product1 = self.factory.makeProduct()
460 for i in range(3):
461 self.makeBranch(product=product1)
462@@ -158,7 +128,7 @@
463 self.makeBranch(product=product2)
464 self.assertEqual(
465 [product2.name, product1.name],
466- [name for name, count, last_commit
467+ [name for name, commits, count, last_commit
468 in self.getProductsWithInfo()])
469
470 def test_limit(self):
471@@ -176,7 +146,7 @@
472 self.makeBranch(product=product3)
473 self.assertEqual(
474 [product3.name, product2.name],
475- [name for name, count, last_commit
476+ [name for name, commits, count, last_commit
477 in self.getProductsWithInfo(num_products=2)])
478
479
480
481=== modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt'
482--- lib/lp/code/stories/branches/xx-bazaar-home.txt 2010-07-22 12:02:30 +0000
483+++ lib/lp/code/stories/branches/xx-bazaar-home.txt 2010-08-31 01:22:44 +0000
484@@ -31,7 +31,6 @@
485 >>> preview = find_tag_by_id(browser.contents, 'project-cloud-preview')
486 >>> print extract_text(preview)
487 Projects with active branches
488- firefox
489 see all projects&#8230;
490
491 >>> print preview.fetch('a')[-1]['href']
492
493=== modified file 'lib/lp/code/stories/branches/xx-branch-tag-cloud.txt'
494--- lib/lp/code/stories/branches/xx-branch-tag-cloud.txt 2010-04-01 13:31:28 +0000
495+++ lib/lp/code/stories/branches/xx-branch-tag-cloud.txt 2010-08-31 01:22:44 +0000
496@@ -1,8 +1,20 @@
497-= Projects with active branches =
498+Projects with active branches
499+=============================
500
501 The tag cloud of projects is one way in which the number and scope of available
502 bazaar branches is shown to the user.
503
504+ >>> login(ANONYMOUS)
505+ >>> from lp.code.tests.helpers import make_project_cloud_data
506+ >>> from datetime import datetime, timedelta
507+ >>> import pytz
508+ >>> now = datetime.now(pytz.UTC)
509+ >>> make_project_cloud_data(factory, [
510+ ... ('wibble', 35, 2, now - timedelta(days=2)),
511+ ... ('linux', 110, 1, now - timedelta(days=8)),
512+ ... ])
513+ >>> logout()
514+
515 >>> anon_browser.open("http://code.launchpad.dev/projects")
516 >>> print anon_browser.title
517 Projects with active branches
518@@ -14,28 +26,5 @@
519 >>> tags = find_tag_by_id(anon_browser.contents, 'project-tags')
520 >>> for anchor in tags.fetch('a'):
521 ... print anchor.renderContents(), anchor['class']
522- firefox cloud-size-largest cloud-light
523-
524-If the project is using bzr as its main branch repository then the class is
525-different from the other projects. This allows the CSS to show these projects
526-in a different colour.
527-
528-Update firefox to officially use codehosting:
529-
530- >>> from lp.registry.interfaces.product import IProductSet
531- >>> from zope.component import getUtility
532- >>> login('admin@canonical.com')
533- >>> firefox = getUtility(IProductSet).getByName('firefox')
534- >>> firefox.development_focus.branch = factory.makeBranch(
535- ... product=firefox)
536- >>> logout()
537-
538-The class for firefox in the project tag cloud will now show 'highlight' rather
539-than shade.
540-
541- >>> anon_browser.open("http://code.launchpad.dev/projects")
542- >>> tags = find_tag_by_id(anon_browser.contents, 'project-tags')
543- >>> for anchor in tags.fetch('a'):
544- ... if anchor.renderContents() == 'firefox':
545- ... print anchor['class']
546- cloud-size-largest cloud-light
547+ linux cloud-size-largest cloud-medium
548+ wibble cloud-size-smallest cloud-dark
549
550=== modified file 'lib/lp/code/tests/helpers.py'
551--- lib/lp/code/tests/helpers.py 2010-08-20 20:31:18 +0000
552+++ lib/lp/code/tests/helpers.py 2010-08-31 01:22:44 +0000
553@@ -9,12 +9,15 @@
554 'make_erics_fooix_project',
555 'make_linked_package_branch',
556 'make_official_package_branch',
557+ 'make_project_branch_with_revisions',
558+ 'make_project_cloud_data',
559 ]
560
561
562 from datetime import timedelta
563 from difflib import unified_diff
564 from itertools import count
565+import transaction
566
567 from zope.component import getUtility
568 from zope.security.proxy import (
569@@ -27,6 +30,7 @@
570 IBranchMergeProposalJobSource,
571 )
572 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
573+from lp.code.interfaces.revision import IRevisionSet
574 from lp.code.interfaces.seriessourcepackagebranch import (
575 IMakeOfficialBranchLinks,
576 )
577@@ -256,3 +260,39 @@
578 ICanHasLinkedBranch(suite_sourcepackage).setBranch,
579 branch, registrant)
580 return branch
581+
582+
583+def make_project_branch_with_revisions(factory, date_generator, product=None,
584+ private=None, revision_count=None):
585+ """Make a new branch with revisions."""
586+ if revision_count is None:
587+ revision_count = 5
588+ branch = factory.makeProductBranch(product=product, private=private)
589+ naked_branch = removeSecurityProxy(branch)
590+ factory.makeRevisionsForBranch(
591+ naked_branch, count=revision_count, date_generator=date_generator)
592+ # The code that updates the revision cache doesn't need to care about
593+ # the privacy of the branch.
594+ getUtility(IRevisionSet).updateRevisionCacheForBranch(naked_branch)
595+ return branch
596+
597+
598+def make_project_cloud_data(factory, details):
599+ """Make test data to populate the project cloud.
600+
601+ Details is a list of tuples containing:
602+ (project-name, num_commits, num_authors, last_commit)
603+ """
604+ delta = timedelta(seconds=1)
605+ for project_name, num_commits, num_authors, last_commit in details:
606+ project = factory.makeProduct(name=project_name)
607+ start_date = last_commit - delta * (num_commits - 1)
608+ gen = time_counter(start_date, delta)
609+ commits_each = num_commits / num_authors
610+ for committer in range(num_authors - 1):
611+ make_project_branch_with_revisions(
612+ factory, gen, project, commits_each)
613+ num_commits -= commits_each
614+ make_project_branch_with_revisions(
615+ factory, gen, project, revision_count=num_commits)
616+ transaction.commit()
617
618=== added file 'lib/lp/code/tests/test_helpers.py'
619--- lib/lp/code/tests/test_helpers.py 1970-01-01 00:00:00 +0000
620+++ lib/lp/code/tests/test_helpers.py 2010-08-31 01:22:44 +0000
621@@ -0,0 +1,40 @@
622+# Copyright 2010 Canonical Ltd. This software is licensed under the
623+# GNU Affero General Public License version 3 (see the file LICENSE).
624+
625+"""Test the code test helpers found in helpers.py."""
626+
627+__metaclass__ = type
628+
629+from datetime import datetime, timedelta
630+import pytz
631+
632+from zope.component import getUtility
633+
634+from canonical.testing import DatabaseFunctionalLayer
635+
636+from lp.code.interfaces.branchcollection import IAllBranches
637+from lp.code.tests.helpers import make_project_cloud_data
638+from lp.registry.interfaces.product import IProductSet
639+from lp.testing import TestCaseWithFactory
640+
641+
642+class TestMakeProjectCloudData(TestCaseWithFactory):
643+ # Make sure that make_project_cloud_data works.
644+
645+ layer = DatabaseFunctionalLayer
646+
647+ def test_single_project(self):
648+ # Make a single project with one commit from one person.
649+ now = datetime.now(pytz.UTC)
650+ commit_time = now - timedelta(days=2)
651+ make_project_cloud_data(self.factory, [
652+ ('fooix', 1, 1, commit_time),
653+ ])
654+ # Make sure we have a new project called fooix.
655+ fooix = getUtility(IProductSet).getByName('fooix')
656+ self.assertIsNot(None, fooix)
657+ # There should be one branch with one commit.
658+ [branch] = list(
659+ getUtility(IAllBranches).inProduct(fooix).getBranches())
660+ self.assertEqual(1, branch.revision_count)
661+ self.assertEqual(commit_time, branch.getTipRevision().revision_date)
662
663=== modified file 'lib/lp/testing/tests/test_standard_test_template.py'
664--- lib/lp/testing/tests/test_standard_test_template.py 2010-08-05 14:15:56 +0000
665+++ lib/lp/testing/tests/test_standard_test_template.py 2010-08-31 01:22:44 +0000
666@@ -5,10 +5,8 @@
667
668 __metaclass__ = type
669
670-import unittest
671-
672 from canonical.testing import DatabaseFunctionalLayer
673-from lp.testing import TestCase
674+from lp.testing import TestCase # or TestCaseWithFactory
675
676
677 class TestSomething(TestCase):
678@@ -24,7 +22,3 @@
679
680 # XXX: Assertions take expected value first, actual value second.
681 self.assertEqual(4, 2 + 2)
682-
683-
684-def test_suite():
685- return unittest.TestLoader().loadTestsFromName(__name__)