← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~thumper/launchpad/different-cloud into lp:launchpad/devel

 

Tim Penhey has proposed merging lp:~thumper/launchpad/different-cloud into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


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



-- 
https://code.launchpad.net/~thumper/launchpad/different-cloud/+merge/34146
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/different-cloud into lp:launchpad/devel.
=== modified file 'lib/lp/code/browser/bazaar.py'
--- lib/lp/code/browser/bazaar.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/browser/bazaar.py	2010-08-31 00:35:58 +0000
@@ -93,17 +93,17 @@
 
 class ProductInfo:
 
-    def __init__(
-        self, product_name, num_branches, branch_size, elapsed):
-        self.name = product_name
-        self.url = '/' + product_name
-        self.num_branches = num_branches
-        self.branch_size = branch_size
+    def __init__(self, name, commits, author_count, size, elapsed):
+        self.name = name
+        self.url = '/' + name
+        self.commits = commits
+        self.author_count = author_count
+        self.size = size
         self.elapsed_since_commit = elapsed
 
     @property
-    def branch_class(self):
-        return "cloud-size-%s" % self.branch_size
+    def tag_class(self):
+        return "cloud-size-%s" % self.size
 
     @property
     def time_darkness(self):
@@ -111,30 +111,32 @@
             return "light"
         if self.elapsed_since_commit.days < 7:
             return "dark"
-        if self.elapsed_since_commit.days < 31:
+        if self.elapsed_since_commit.days < 14:
             return "medium"
         return "light"
 
     @property
     def html_class(self):
-        return "%s cloud-%s" % (self.branch_class, self.time_darkness)
+        return "%s cloud-%s" % (self.tag_class, self.time_darkness)
 
     @property
     def html_title(self):
-        if self.num_branches == 1:
-            size = "1 branch"
-        else:
-            size = "%d branches" % self.num_branches
-        if self.elapsed_since_commit is None:
-            commit = "no commits yet"
-        elif self.elapsed_since_commit.days == 0:
+        if self.commits == 1:
+            size = "1 commit"
+        else:
+            size = "%d commits" % self.commits
+        if self.author_count == 1:
+            who = "1 person"
+        else:
+            who = "%s people" % self.author_count
+        if self.elapsed_since_commit.days == 0:
             commit = "last commit less than a day old"
         elif self.elapsed_since_commit.days == 1:
             commit = "last commit one day old"
         else:
             commit = (
                 "last commit %d days old" % self.elapsed_since_commit.days)
-        return "%s, %s" % (size, commit)
+        return "%s by %s, %s" % (size, who, commit)
 
 
 class BazaarProjectsRedirect(LaunchpadView):
@@ -178,6 +180,8 @@
         # unique by the sql query.
         product_info = sorted(
             list(getUtility(IBranchCloud).getProductsWithInfo(num_products)))
+        if len(product_info) == 0:
+            return
         now = datetime.today()
         counts = sorted(zip(*product_info)[1])
         size_mapping = {
@@ -187,14 +191,10 @@
             0.8: 'large',
             1.0: 'largest',
             }
-        num_branches_to_size = self._make_distribution_map(
+        num_commits_to_size = self._make_distribution_map(
             counts, size_mapping)
 
-        for product_name, num_branches, last_revision_date in product_info:
-            # Projects with no branches are not interesting.
-            if num_branches == 0:
-                continue
-            branch_size = num_branches_to_size[num_branches]
+        for name, commits, author_count, last_revision_date in product_info:
+            size = num_commits_to_size[commits]
             elapsed = now - last_revision_date
-            yield ProductInfo(
-                product_name, num_branches, branch_size, elapsed)
+            yield ProductInfo(name, commits, author_count, size, elapsed)

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-08-19 03:01:51 +0000
+++ lib/lp/code/configure.zcml	2010-08-31 00:35:58 +0000
@@ -568,7 +568,7 @@
     <allow interface="lp.code.interfaces.branch.IBranchDelta"/>
   </class>
   <securedutility
-      class="lp.code.model.branch.BranchCloud"
+      class="lp.code.model.branchcloud.BranchCloud"
       provides="lp.code.interfaces.branch.IBranchCloud">
     <allow interface="lp.code.interfaces.branch.IBranchCloud"/>
   </securedutility>

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/branch.py	2010-08-31 00:35:58 +0000
@@ -1277,9 +1277,12 @@
     """
 
     def getProductsWithInfo(num_products=None):
-        """Get products with their branch activity information.
-
-        :return: a `ResultSet` of (product, num_branches, last_revision_date).
+        """Get products with their recent activity information.
+
+        The counts are for the last 30 days.
+
+        :return: a `ResultSet` of (product, num_commits, num_authors,
+            last_revision_date).
         """
 
 

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/branch.py	2010-08-31 00:35:58 +0000
@@ -1306,37 +1306,6 @@
         return branches
 
 
-class BranchCloud:
-    """See `IBranchCloud`."""
-
-    def getProductsWithInfo(self, num_products=None, store_flavor=None):
-        """See `IBranchCloud`."""
-        # Circular imports are fun.
-        from lp.registry.model.product import Product
-        # It doesn't matter if this query is even a whole day out of date, so
-        # use the slave store by default.
-        if store_flavor is None:
-            store_flavor = SLAVE_FLAVOR
-        store = getUtility(IStoreSelector).get(MAIN_STORE, store_flavor)
-        # Get all products, the count of all hosted & mirrored branches and
-        # the last revision date.
-        result = store.find(
-            (Product.name, Count(Branch.id), Max(Revision.revision_date)),
-            Branch.private == False,
-            Branch.product == Product.id,
-            Or(Branch.branch_type == BranchType.HOSTED,
-               Branch.branch_type == BranchType.MIRRORED),
-            Branch.last_scanned_id == Revision.revision_id)
-        result = result.group_by(Product.name)
-        result = result.order_by(Desc(Count(Branch.id)))
-        if num_products:
-            result.config(limit=num_products)
-        # XXX: JonathanLange 2009-02-10: The revision date in the result set
-        # isn't timezone-aware. Not sure why this is. Doesn't matter too much
-        # for the purposes of cloud calculation though.
-        return result
-
-
 def update_trigger_modified_fields(branch):
     """Make the trigger updated fields reload when next accessed."""
     # Not all the fields are exposed through the interface, and some are read

=== added file 'lib/lp/code/model/branchcloud.py'
--- lib/lp/code/model/branchcloud.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/branchcloud.py	2010-08-31 00:35:58 +0000
@@ -0,0 +1,52 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""The implementation of the branch cloud."""
+
+__metaclass__ = type
+__all__ = [
+    'BranchCloud',
+    ]
+
+
+from datetime import datetime, timedelta
+
+import pytz
+from storm.expr import Alias, Func
+from storm.locals import Count, Desc, Max, Not
+from zope.interface import classProvides
+
+from canonical.launchpad.interfaces.lpstorm import ISlaveStore
+
+from lp.code.interfaces.branch import IBranchCloud
+from lp.code.model.revision import RevisionCache
+from lp.registry.model.product import Product
+
+
+class BranchCloud:
+    """See `IBranchCloud`."""
+
+    classProvides(IBranchCloud)
+
+    @staticmethod
+    def getProductsWithInfo(num_products=None):
+        """See `IBranchCloud`."""
+        distinct_revision_author = Func(
+            "distinct", RevisionCache.revision_author_id)
+        commits = Alias(Count(RevisionCache.revision_id))
+        epoch = datetime.now(pytz.UTC) - timedelta(days=30)
+        # It doesn't matter if this query is even a whole day out of date, so
+        # use the slave store by default.
+        result = ISlaveStore(RevisionCache).find(
+            (Product.name,
+             commits,
+             Count(distinct_revision_author),
+             Max(RevisionCache.revision_date)),
+            RevisionCache.product == Product.id,
+            Not(RevisionCache.private),
+            RevisionCache.revision_date >= epoch)
+        result = result.group_by(Product.name)
+        result = result.order_by(Desc(commits))
+        if num_products:
+            result.config(limit=num_products)
+        return result

=== modified file 'lib/lp/code/model/revision.py'
--- lib/lp/code/model/revision.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/revision.py	2010-08-31 00:35:58 +0000
@@ -609,8 +609,7 @@
     revision_author_id = Int(name='revision_author', allow_none=False)
     revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
 
-    revision_date = DateTime(
-        name='revision_date', allow_none=False, tzinfo=pytz.UTC)
+    revision_date = UtcDateTimeCol(notNull=True)
 
     product_id = Int(name='product', allow_none=True)
     product = Reference(product_id, 'Product.id')

=== modified file 'lib/lp/code/model/tests/test_branchcloud.py'
--- lib/lp/code/model/tests/test_branchcloud.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_branchcloud.py	2010-08-31 00:35:58 +0000
@@ -9,19 +9,21 @@
     datetime,
     timedelta,
     )
+import transaction
 import unittest
 
 import pytz
+from storm.locals import Store
 from zope.component import getUtility
-from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.testing.databasehelpers import (
     remove_all_sample_data_branches,
     )
-from canonical.launchpad.webapp.interfaces import MASTER_FLAVOR
 from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.code.enums import BranchType
+
 from lp.code.interfaces.branch import IBranchCloud
+from lp.code.model.revision import RevisionCache
+from lp.code.tests.helpers import make_project_branch_with_revisions
 from lp.testing import (
     TestCaseWithFactory,
     time_counter,
@@ -39,40 +41,40 @@
 
     def getProductsWithInfo(self, num_products=None):
         """Get product cloud information."""
-        # We use the MASTER_FLAVOR so that data changes made in these tests
-        # are visible to the query in getProductsWithInfo. The default
-        # implementation uses the SLAVE_FLAVOR.
-        return self._branch_cloud.getProductsWithInfo(
-            num_products, store_flavor=MASTER_FLAVOR)
+        # Since we use the slave store to get the information, we need to
+        # commit the transaction to make the information visible to the slave.
+        transaction.commit()
+        cloud_info = self._branch_cloud.getProductsWithInfo(num_products)
+        # The last commit time is timezone unaware as the storm Max function
+        # doesn't take into account the type that it is aggregating, so whack
+        # the UTC tz on it here for easier comparing in the tests.
+        def add_utc(value):
+            return value.replace(tzinfo=pytz.UTC)
+        return [
+            (name, commits, authors, add_utc(last_commit))
+            for name, commits, authors, last_commit in cloud_info]
 
-    def makeBranch(self, product=None, branch_type=None,
-                   last_commit_date=None, private=False):
+    def makeBranch(self, product=None, last_commit_date=None, private=False,
+                   revision_count=None):
         """Make a product branch with a particular last commit date"""
-        revision_count = 5
+        if revision_count is None:
+            revision_count = 5
         delta = timedelta(days=1)
         if last_commit_date is None:
-            date_generator = None
+            # By default we create revisions that are within the last 30 days.
+            date_generator = time_counter(
+                datetime.now(pytz.UTC) - timedelta(days=25), delta)
         else:
             start_date = last_commit_date - delta * (revision_count - 1)
-            # The output of getProductsWithInfo doesn't include timezone
-            # information -- not sure why. To make the tests a little clearer,
-            # this method expects last_commit_date to be a naive datetime that
-            # can be compared directly with the output of getProductsWithInfo.
-            start_date = start_date.replace(tzinfo=pytz.UTC)
             date_generator = time_counter(start_date, delta)
-        branch = self.factory.makeProductBranch(
-            product=product, branch_type=branch_type, private=private)
-        if branch_type != BranchType.REMOTE:
-            self.factory.makeRevisionsForBranch(
-                removeSecurityProxy(branch), count=revision_count,
-                date_generator=date_generator)
+        branch = make_project_branch_with_revisions(
+            self.factory, date_generator, product, private, revision_count)
         return branch
 
     def test_empty_with_no_branches(self):
         # getProductsWithInfo returns an empty result set if there are no
         # branches in the database.
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
+        self.assertEqual([], self.getProductsWithInfo())
 
     def test_empty_products_not_counted(self):
         # getProductsWithInfo doesn't include products that don't have any
@@ -80,76 +82,51 @@
         #
         # Note that this is tested implicitly by test_empty_with_no_branches,
         # since there are such products in the sample data.
-        product = self.factory.makeProduct()
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
+        self.factory.makeProduct()
+        self.assertEqual([], self.getProductsWithInfo())
 
     def test_empty_branches_not_counted(self):
         # getProductsWithInfo doesn't consider branches that lack revision
         # data, 'empty branches', to contribute to the count of branches on a
         # product.
-        branch = self.factory.makeProductBranch()
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
-
-    def test_import_branches_not_counted(self):
-        # getProductsWithInfo doesn't consider imported branches to contribute
-        # to the count of branches on a product.
-        branch = self.makeBranch(branch_type=BranchType.IMPORTED)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
-
-    def test_remote_branches_not_counted(self):
-        # getProductsWithInfo doesn't consider remote branches to contribute
-        # to the count of branches on a product.
-        branch = self.makeBranch(branch_type=BranchType.REMOTE)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
+        self.factory.makeProductBranch()
+        self.assertEqual([], self.getProductsWithInfo())
 
     def test_private_branches_not_counted(self):
         # getProductsWithInfo doesn't count private branches.
-        branch = self.makeBranch(private=True)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual([], list(products_with_info))
-
-    def test_hosted_and_mirrored_counted(self):
-        # getProductsWithInfo includes products that have hosted or mirrored
-        # branches with revisions.
-        product = self.factory.makeProduct()
-        self.makeBranch(product=product, branch_type=BranchType.HOSTED)
-        last_commit_date = datetime(2007, 1, 5)
-        self.makeBranch(
-            product=product, branch_type=BranchType.MIRRORED,
-            last_commit_date=last_commit_date)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual(
-            [(product.name, 2, last_commit_date)], list(products_with_info))
-
-    def test_includes_products_with_branches_with_revisions(self):
-        # getProductsWithInfo includes all products that have branches with
-        # revisions.
-        last_commit_date = datetime(2008, 12, 25)
-        branch = self.makeBranch(last_commit_date=last_commit_date)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual(
-            [(branch.product.name, 1, last_commit_date)],
-            list(products_with_info))
-
-    def test_uses_latest_revision_date(self):
-        # getProductsWithInfo uses the most recent revision date from all the
-        # branches in that product.
-        product = self.factory.makeProduct()
-        self.makeBranch(
-            product=product, last_commit_date=datetime(2008, 12, 25))
-        last_commit_date = datetime(2009, 01, 01)
+        self.makeBranch(private=True)
+        self.assertEqual([], self.getProductsWithInfo())
+
+    def test_revisions_counted(self):
+        # getProductsWithInfo includes products that public revisions.
+        last_commit_date = datetime.now(pytz.UTC) - timedelta(days=5)
+        product = self.factory.makeProduct()
         self.makeBranch(product=product, last_commit_date=last_commit_date)
-        products_with_info = self.getProductsWithInfo()
-        self.assertEqual(
-            [(product.name, 2, last_commit_date)], list(products_with_info))
-
-    def test_sorted_by_branch_count(self):
+        self.assertEqual(
+            [(product.name, 5, 1, last_commit_date)],
+            self.getProductsWithInfo())
+
+    def test_only_recent_revisions_counted(self):
+        # If the revision cache has revisions for the project, but they are
+        # over 30 days old, we don't count them.
+        product = self.factory.makeProduct()
+        date_generator = time_counter(
+            datetime.now(pytz.UTC) - timedelta(days=33),
+            delta=timedelta(days=2))
+        store = Store.of(product)
+        for i in range(4):
+            revision = self.factory.makeRevision(
+                revision_date=date_generator.next())
+            cache = RevisionCache(revision)
+            cache.product = product
+            store.add(cache)
+        self.assertEqual(
+            [(product.name, 2, 2, revision.revision_date)],
+            self.getProductsWithInfo())
+
+    def test_sorted_by_commit_count(self):
         # getProductsWithInfo returns a result set sorted so that the products
-        # with the most branches come first.
+        # with the most commits come first.
         product1 = self.factory.makeProduct()
         for i in range(3):
             self.makeBranch(product=product1)
@@ -158,7 +135,7 @@
             self.makeBranch(product=product2)
         self.assertEqual(
             [product2.name, product1.name],
-            [name for name, count, last_commit
+            [name for name, commits, count, last_commit
              in self.getProductsWithInfo()])
 
     def test_limit(self):
@@ -176,7 +153,7 @@
             self.makeBranch(product=product3)
         self.assertEqual(
             [product3.name, product2.name],
-            [name for name, count, last_commit
+            [name for name, commits, count, last_commit
              in self.getProductsWithInfo(num_products=2)])
 
 

=== modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt'
--- lib/lp/code/stories/branches/xx-bazaar-home.txt	2010-07-22 12:02:30 +0000
+++ lib/lp/code/stories/branches/xx-bazaar-home.txt	2010-08-31 00:35:58 +0000
@@ -31,7 +31,6 @@
     >>> preview = find_tag_by_id(browser.contents, 'project-cloud-preview')
     >>> print extract_text(preview)
     Projects with active branches
-    firefox
     see all projects&#8230;
 
     >>> print preview.fetch('a')[-1]['href']

=== modified file 'lib/lp/code/stories/branches/xx-branch-tag-cloud.txt'
--- lib/lp/code/stories/branches/xx-branch-tag-cloud.txt	2010-04-01 13:31:28 +0000
+++ lib/lp/code/stories/branches/xx-branch-tag-cloud.txt	2010-08-31 00:35:58 +0000
@@ -1,8 +1,20 @@
-= Projects with active branches =
+Projects with active branches
+=============================
 
 The tag cloud of projects is one way in which the number and scope of available
 bazaar branches is shown to the user.
 
+    >>> login(ANONYMOUS)
+    >>> from lp.code.tests.helpers import make_project_cloud_data
+    >>> from datetime import datetime, timedelta
+    >>> import pytz
+    >>> now = datetime.now(pytz.UTC)
+    >>> make_project_cloud_data(factory, [
+    ...     ('wibble', 35, 2, now - timedelta(days=2)),
+    ...     ('linux', 110, 1, now - timedelta(days=8)),
+    ...     ])
+    >>> logout()
+
     >>> anon_browser.open("http://code.launchpad.dev/projects";)
     >>> print anon_browser.title
     Projects with active branches
@@ -14,28 +26,5 @@
     >>> tags = find_tag_by_id(anon_browser.contents, 'project-tags')
     >>> for anchor in tags.fetch('a'):
     ...     print anchor.renderContents(), anchor['class']
-    firefox cloud-size-largest cloud-light
-
-If the project is using bzr as its main branch repository then the class is
-different from the other projects.  This allows the CSS to show these projects
-in a different colour.
-
-Update firefox to officially use codehosting:
-
-    >>> from lp.registry.interfaces.product import IProductSet
-    >>> from zope.component import getUtility
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> firefox.development_focus.branch = factory.makeBranch(
-    ...     product=firefox)
-    >>> logout()
-
-The class for firefox in the project tag cloud will now show 'highlight' rather
-than shade.
-
-    >>> anon_browser.open("http://code.launchpad.dev/projects";)
-    >>> tags = find_tag_by_id(anon_browser.contents, 'project-tags')
-    >>> for anchor in tags.fetch('a'):
-    ...     if anchor.renderContents() == 'firefox':
-    ...         print anchor['class']
-    cloud-size-largest cloud-light
+    linux cloud-size-largest cloud-medium
+    wibble cloud-size-smallest cloud-dark

=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/tests/helpers.py	2010-08-31 00:35:58 +0000
@@ -9,12 +9,15 @@
     'make_erics_fooix_project',
     'make_linked_package_branch',
     'make_official_package_branch',
+    'make_project_branch_with_revisions',
+    'make_project_cloud_data',
     ]
 
 
 from datetime import timedelta
 from difflib import unified_diff
 from itertools import count
+import transaction
 
 from zope.component import getUtility
 from zope.security.proxy import (
@@ -27,6 +30,7 @@
     IBranchMergeProposalJobSource,
     )
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
+from lp.code.interfaces.revision import IRevisionSet
 from lp.code.interfaces.seriessourcepackagebranch import (
     IMakeOfficialBranchLinks,
     )
@@ -256,3 +260,39 @@
         ICanHasLinkedBranch(suite_sourcepackage).setBranch,
         branch, registrant)
     return branch
+
+
+def make_project_branch_with_revisions(factory, date_generator, product=None,
+                                       private=None, revision_count=None):
+    """Make a new branch with revisions."""
+    if revision_count is None:
+        revision_count = 5
+    branch = factory.makeProductBranch(product=product, private=private)
+    naked_branch = removeSecurityProxy(branch)
+    factory.makeRevisionsForBranch(
+        naked_branch, count=revision_count, date_generator=date_generator)
+    # The code that updates the revision cache doesn't need to care about
+    # the privacy of the branch.
+    getUtility(IRevisionSet).updateRevisionCacheForBranch(naked_branch)
+    return branch
+
+
+def make_project_cloud_data(factory, details):
+    """Make test data to populate the project cloud.
+
+    Details is a list of tuples containing:
+      (project-name, num_commits, num_authors, last_commit)
+    """
+    delta = timedelta(seconds=1)
+    for project_name, num_commits, num_authors, last_commit in details:
+        project = factory.makeProduct(name=project_name)
+        start_date = last_commit - delta * (num_commits - 1)
+        gen = time_counter(start_date, delta)
+        commits_each = num_commits / num_authors
+        for committer in range(num_authors - 1):
+            make_project_branch_with_revisions(
+                factory, gen, project, commits_each)
+            num_commits -= commits_each
+        make_project_branch_with_revisions(
+            factory, gen, project, revision_count=num_commits)
+    transaction.commit()

=== added file 'lib/lp/code/tests/test_helpers.py'
--- lib/lp/code/tests/test_helpers.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/tests/test_helpers.py	2010-08-31 00:35:58 +0000
@@ -0,0 +1,45 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the code test helpers found in helpers.py."""
+
+__metaclass__ = type
+
+from datetime import datetime, timedelta
+import pytz
+import unittest
+
+from zope.component import getUtility
+
+from canonical.testing import DatabaseFunctionalLayer
+
+from lp.code.interfaces.branchcollection import IAllBranches
+from lp.code.tests.helpers import make_project_cloud_data
+from lp.registry.interfaces.product import IProductSet
+from lp.testing import TestCaseWithFactory
+
+
+class TestMakeProjectCloudData(TestCaseWithFactory):
+    # Make sure that make_project_cloud_data works.
+
+    layer = DatabaseFunctionalLayer
+
+    def test_single_project(self):
+        # Make a single project with one commit from one person.
+        now = datetime.now(pytz.UTC)
+        commit_time = now - timedelta(days=2)
+        make_project_cloud_data(self.factory, [
+                ('fooix', 1, 1, commit_time),
+                ])
+        # Make sure we have a new project called fooix.
+        fooix = getUtility(IProductSet).getByName('fooix')
+        self.assertIsNot(None, fooix)
+        # There should be one branch with one commit.
+        [branch] = list(
+            getUtility(IAllBranches).inProduct(fooix).getBranches())
+        self.assertEqual(1, branch.revision_count)
+        self.assertEqual(commit_time, branch.getTipRevision().revision_date)
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)