← Back to team overview

launchpad-reviewers team mailing list archive

lp:~allenap/launchpad/localpackagediffs-performance-bug-751321-queries into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/localpackagediffs-performance-bug-751321-queries into lp:launchpad with lp:~allenap/launchpad/localpackagediffs-performance-bug-751321-refactor as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/launchpad/localpackagediffs-performance-bug-751321-queries/+merge/57778

This attempts to eager-load a ton of useful stuff for the +localpackagediffs page. It does this in several different ways, but the core test to look for is test_queries. This attempts to show that the query count is flat even after adding more differences to render. At the moment that is not true - bug 760733 has been filed to follow up on that - but this branch represents a big move in the right direction.
-- 
https://code.launchpad.net/~allenap/launchpad/localpackagediffs-performance-bug-751321-queries/+merge/57778
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/localpackagediffs-performance-bug-751321-queries into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-04-08 07:43:39 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-04-14 21:12:40 +0000
@@ -487,6 +487,9 @@
 patch_collection_return_type(
     IDistroSeries, 'getDerivedSeries', IDistroSeries)
 
+# IDistroSeriesDifference
+patch_reference_property(
+    IDistroSeriesDifference, 'latest_comment', IDistroSeriesDifferenceComment)
 
 # IDistroSeriesDifferenceComment
 IDistroSeriesDifferenceComment['comment_author'].schema = IPerson

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-04-12 12:50:34 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-04-14 21:12:40 +0000
@@ -5,16 +5,25 @@
 
 __metaclass__ = type
 
+import difflib
 import re
+from textwrap import TextWrapper
 
 from BeautifulSoup import BeautifulSoup
 from lxml import html
 import soupmatchers
 from storm.zope.interfaces import IResultSet
-from testtools.matchers import EndsWith
+from testtools.content import Content
+from testtools.content_type import UTF8_TEXT
+from testtools.matchers import (
+    EndsWith,
+    LessThan,
+    )
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.config import config
+from canonical.database.sqlbase import flush_database_caches
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.testing.pages import find_tag_by_id
 from canonical.launchpad.webapp.batching import BatchNavigator
@@ -56,8 +65,10 @@
     login_person,
     person_logged_in,
     set_feature_flag,
+    StormStatementRecorder,
     TestCaseWithFactory,
     )
+from lp.testing.matchers import HasQueryCount
 from lp.testing.views import create_initialized_view
 
 
@@ -319,6 +330,100 @@
         self._test_packagesets(
             html, packageset_text, 'parent-packagesets', 'Parent packagesets')
 
+    def test_queries(self):
+        # With no DistroSeriesDifferences the query count should be low and
+        # fairly static. However, with some DistroSeriesDifferences the query
+        # count will be higher, but it should remain the same no matter how
+        # many differences there are.
+        login_person(self.simple_user)
+        derived_series = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+
+        def add_differences(num):
+            for index in xrange(num):
+                version = self.factory.getUniqueInteger()
+                versions = {
+                    'base': u'1.%d' % version,
+                    'derived': u'1.%dderived1' % version,
+                    'parent': u'1.%d-1' % version,
+                    }
+                dsd = self.factory.makeDistroSeriesDifference(
+                    derived_series=derived_series,
+                    versions=versions)
+
+                # Push a base_version in... not sure how better to do it.
+                removeSecurityProxy(dsd).base_version = versions["base"]
+
+                # Add a couple of comments.
+                self.factory.makeDistroSeriesDifferenceComment(dsd)
+                self.factory.makeDistroSeriesDifferenceComment(dsd)
+
+                # Update the spr, some with recipes, some with signing keys.
+                # SPR.uploader references both, and the uploader is referenced
+                # in the page.
+                spr = dsd.source_pub.sourcepackagerelease
+                if index % 2 == 0:
+                    removeSecurityProxy(spr).source_package_recipe_build = (
+                        self.factory.makeSourcePackageRecipeBuild(
+                            sourcename=spr.sourcepackagename.name,
+                            distroseries=derived_series))
+                else:
+                    removeSecurityProxy(spr).dscsigningkey = (
+                        self.factory.makeGPGKey(owner=spr.creator))
+
+        def flush_and_render():
+            flush_database_caches()
+            # Pull in the calling user's location so that it isn't recorded in
+            # the query count; it causes the total to be fragile for no
+            # readily apparent reason.
+            self.simple_user.location
+            with StormStatementRecorder() as recorder:
+                create_initialized_view(
+                    derived_series, '+localpackagediffs',
+                    principal=self.simple_user)()
+            return recorder
+
+        def statement_differ(rec1, rec2):
+            wrapper = TextWrapper(break_long_words=False)
+
+            def prepare_statements(rec):
+                for statement in rec.statements:
+                    for line in wrapper.wrap(statement):
+                        yield line
+                    yield "-" * wrapper.width
+
+            def statement_diff():
+                diff = difflib.ndiff(
+                    list(prepare_statements(rec1)),
+                    list(prepare_statements(rec2)))
+                for line in diff:
+                    yield "%s\n" % line
+
+            return statement_diff
+
+        # Render without differences and check the query count isn't silly.
+        recorder1 = flush_and_render()
+        self.assertThat(recorder1, HasQueryCount(LessThan(30)))
+        # Add some differences and render.
+        add_differences(2)
+        recorder2 = flush_and_render()
+        # Add more differences and render again.
+        add_differences(2)
+        recorder3 = flush_and_render()
+        # The last render should not need more queries than the previous.
+        self.addDetail(
+            "statement-diff", Content(
+                UTF8_TEXT, statement_differ(recorder2, recorder3)))
+        # XXX: GavinPanella 2011-04-12 bug=760733: Reducing the query count
+        # further needs work. Ideally this test would be along the lines of
+        # recorder3.count == recorder2.count. 8 queries above the recorder2
+        # count is 4 queries per difference which is not acceptable, but is
+        # *far* better than without the changes introduced by landing this.
+        compromise_statement_count = recorder2.count + 8
+        self.assertThat(
+            recorder3, HasQueryCount(
+                LessThan(compromise_statement_count + 1)))
+
 
 class TestDistroSeriesLocalDifferencesZopeless(TestCaseWithFactory):
     """Test the distroseries +localpackagediffs view."""
@@ -519,6 +624,8 @@
         # Delete the publications.
         difference.source_pub.status = PackagePublishingStatus.DELETED
         difference.parent_source_pub.status = PackagePublishingStatus.DELETED
+        # Flush out the changes and invalidate caches (esp. property caches).
+        flush_database_caches()
 
         set_derived_series_ui_feature_flag(self)
         view = create_initialized_view(

=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
--- lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-14 20:06:33 +0000
+++ lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-14 21:12:40 +0000
@@ -188,6 +188,11 @@
         :return: True if the record was updated, False otherwise.
         """
 
+    latest_comment = Reference(
+        Interface, # IDistroSeriesDifferenceComment
+        title=_("The latest comment"),
+        readonly=True)
+
     def getComments():
         """Return a result set of the comments for this difference."""
 

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-04-13 14:44:31 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-04-14 21:12:40 +0000
@@ -9,18 +9,25 @@
     'DistroSeriesDifference',
     ]
 
+from itertools import chain
+from operator import itemgetter
+
 from debian.changelog import (
     Changelog,
     Version,
     )
 from lazr.enum import DBItem
 from sqlobject import StringCol
-from storm.expr import Desc
+from storm.expr import (
+    And,
+    compile as storm_compile,
+    Desc,
+    SQL,
+    )
+from storm.info import ClassAlias
 from storm.locals import (
-    And,
     Int,
     Reference,
-    Storm,
     )
 from storm.zope.interfaces import IResultSet
 from zope.component import getUtility
@@ -30,10 +37,15 @@
     )
 
 from canonical.database.enumcol import DBEnum
+from canonical.launchpad.components.decoratedresultset import (
+    DecoratedResultSet,
+    )
+from canonical.launchpad.database.message import Message
 from canonical.launchpad.interfaces.lpstorm import (
     IMasterStore,
     IStore,
     )
+from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
 from lp.registry.enum import (
     DistroSeriesDifferenceStatus,
     DistroSeriesDifferenceType,
@@ -49,25 +61,130 @@
 from lp.registry.interfaces.distroseriesdifferencecomment import (
     IDistroSeriesDifferenceCommentSource,
     )
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.distroseriesdifferencecomment import (
     DistroSeriesDifferenceComment,
     )
+from lp.registry.model.gpgkey import GPGKey
 from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.database import bulk
+from lp.services.database.stormbase import StormBase
 from lp.services.propertycache import (
     cachedproperty,
     clear_property_cache,
+    get_property_cache,
     )
 from lp.soyuz.enums import (
+    ArchivePurpose,
     PackageDiffStatus,
     PackagePublishingStatus,
     )
 from lp.soyuz.interfaces.packageset import IPackagesetSet
+from lp.soyuz.model.archive import Archive
 from lp.soyuz.model.distroseriessourcepackagerelease import (
     DistroSeriesSourcePackageRelease,
     )
-
-
-class DistroSeriesDifference(Storm):
+from lp.soyuz.model.publishing import SourcePackagePublishingHistory
+from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+
+
+def most_recent_publications(dsds, in_parent, statuses, match_version=False):
+    """The most recent publications for the given `DistroSeriesDifference`s.
+
+    Returns an `IResultSet` that yields two columns: `SourcePackageName.id`
+    and `SourcePackagePublishingHistory`.
+
+    :param dsds: An iterable of `DistroSeriesDifference` instances.
+    :param in_parent: A boolean indicating if we should look in the parent
+        series' archive instead of the derived series' archive.
+    """
+    distinct_on = "DistroSeriesDifference.source_package_name"
+    columns = (
+        # XXX: GavinPanella 2010-04-06 bug=374777: This SQL(...) is a hack; it
+        # does not seem to be possible to express DISTINCT ON with Storm.
+        SQL("DISTINCT ON (%s) 0 AS ignore" % distinct_on),
+        DistroSeriesDifference.source_package_name_id,
+        SourcePackagePublishingHistory,
+        )
+    conditions = And(
+        DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds),
+        DistroSeries.id == DistroSeriesDifference.derived_series_id,
+        SourcePackagePublishingHistory.archiveID == Archive.id,
+        SourcePackagePublishingHistory.sourcepackagereleaseID == (
+            SourcePackageRelease.id),
+        SourcePackagePublishingHistory.status.is_in(statuses),
+        SourcePackageRelease.sourcepackagenameID == (
+            DistroSeriesDifference.source_package_name_id),
+        )
+    # Check in the parent archive or the child?
+    if in_parent:
+        ParentDistroSeries = ClassAlias(DistroSeries)
+        conditions = And(
+            conditions,
+            ParentDistroSeries.id == DistroSeries.parent_seriesID,
+            Archive.distributionID == ParentDistroSeries.distributionID,
+            Archive.purpose == ArchivePurpose.PRIMARY,
+            )
+    else:
+        conditions = And(
+            conditions,
+            Archive.distributionID == DistroSeries.distributionID,
+            Archive.purpose == ArchivePurpose.PRIMARY,
+            )
+    # Do we match on DistroSeriesDifference.(parent_)source_version?
+    if match_version:
+        if in_parent:
+            version_column = DistroSeriesDifference.parent_source_version
+        else:
+            version_column = DistroSeriesDifference.source_version
+        conditions = And(
+            conditions,
+            SourcePackageRelease.version == version_column,
+            )
+    # The sort order is critical so that the DISTINCT ON clause selects the
+    # most recent publication (i.e. the one with the highest id).
+    order_by = (
+        DistroSeriesDifference.source_package_name_id,
+        Desc(SourcePackagePublishingHistory.id),
+        )
+    store = IStore(SourcePackagePublishingHistory)
+    results = store.find(columns, conditions).order_by(*order_by)
+    return DecoratedResultSet(results, itemgetter(1, 2))
+
+
+def most_recent_comments(dsds):
+    """The most recent comments for the given `DistroSeriesDifference`s.
+
+    Returns an `IResultSet` that yields a single column of
+        `DistroSeriesDifferenceComment`.
+
+    :param dsds: An iterable of `DistroSeriesDifference` instances.
+    """
+    distinct_on = storm_compile(
+        DistroSeriesDifferenceComment.distro_series_difference_id)
+    columns = (
+        # XXX: GavinPanella 2010-04-06 bug=374777: This SQL(...) is a
+        # hack; it does not seem to be possible to express DISTINCT ON
+        # with Storm.
+        SQL("DISTINCT ON (%s) 0 AS ignore" % distinct_on),
+        DistroSeriesDifferenceComment,
+        Message,
+        )
+    conditions = And(
+        DistroSeriesDifferenceComment
+            .distro_series_difference_id.is_in(dsd.id for dsd in dsds),
+        Message.id == DistroSeriesDifferenceComment.message_id)
+    order_by = (
+        DistroSeriesDifferenceComment.distro_series_difference_id,
+        Desc(DistroSeriesDifferenceComment.id),
+        )
+    store = IStore(DistroSeriesDifferenceComment)
+    comments = store.find(columns, conditions).order_by(*order_by)
+    return DecoratedResultSet(comments, itemgetter(1))
+
+
+class DistroSeriesDifference(StormBase):
     """See `DistroSeriesDifference`."""
     implements(IDistroSeriesDifference)
     classProvides(IDistroSeriesDifferenceSource)
@@ -154,10 +271,90 @@
                 DistroSeriesDifference.source_version >
                     DistroSeriesDifference.parent_source_version])
 
-        return IStore(DistroSeriesDifference).find(
+        differences = IStore(DistroSeriesDifference).find(
             DistroSeriesDifference,
             And(*conditions)).order_by(SourcePackageName.name)
 
+        def eager_load(dsds):
+            source_pubs = dict(
+                most_recent_publications(
+                    dsds, in_parent=False, statuses=(
+                        PackagePublishingStatus.PUBLISHED,
+                        PackagePublishingStatus.PENDING)))
+            parent_source_pubs = dict(
+                most_recent_publications(
+                    dsds, in_parent=True, statuses=(
+                        PackagePublishingStatus.PUBLISHED,
+                        PackagePublishingStatus.PENDING)))
+
+            source_pubs_for_release = dict(
+                most_recent_publications(
+                    dsds, in_parent=False, statuses=(
+                        PackagePublishingStatus.PUBLISHED,),
+                    match_version=True))
+            parent_source_pubs_for_release = dict(
+                most_recent_publications(
+                    dsds, in_parent=True, statuses=(
+                        PackagePublishingStatus.PUBLISHED,),
+                    match_version=True))
+
+            latest_comment_by_dsd_id = dict(
+                (comment.distro_series_difference_id, comment)
+                for comment in most_recent_comments(dsds))
+            latest_comments = latest_comment_by_dsd_id.values()
+
+            for dsd in dsds:
+                spn_id = dsd.source_package_name_id
+                cache = get_property_cache(dsd)
+                cache.source_pub = source_pubs.get(spn_id)
+                cache.parent_source_pub = parent_source_pubs.get(spn_id)
+                if dsd.id in source_pubs_for_release:
+                    cache.source_package_release = (
+                        DistroSeriesSourcePackageRelease(
+                            dsd.derived_series,
+                            source_pubs_for_release[dsd.id]))
+                if dsd.id in parent_source_pubs_for_release:
+                    cache.parent_source_package_release = (
+                        DistroSeriesSourcePackageRelease(
+                            dsd.derived_series.parent_series,
+                            parent_source_pubs_for_release[dsd.id]))
+                cache.latest_comment = latest_comment_by_dsd_id.get(dsd.id)
+
+            # SourcePackageReleases of the parent and source pubs are often
+            # referred to.
+            sprs = bulk.load_related(
+                SourcePackageRelease, chain(
+                    source_pubs.itervalues(),
+                    parent_source_pubs.itervalues()),
+                ("sourcepackagereleaseID",))
+
+            # SourcePackageRelease.uploader can end up getting the requester
+            # for a source package recipe build.
+            sprbs = bulk.load_related(
+                SourcePackageRecipeBuild, sprs,
+                ("source_package_recipe_build_id",))
+
+            # SourcePackageRelease.uploader can end up getting the owner of
+            # the DSC signing key.
+            gpgkeys = bulk.load_related(GPGKey, sprs, ("dscsigningkeyID",))
+
+            # Load DistroSeriesDifferenceComment owners,
+            # SourcePackageRecipeBuild requesters and GPGKey owners.
+            person_ids = set().union(
+                (dsdc.message.ownerID for dsdc in latest_comments),
+                (sprb.requester_id for sprb in sprbs),
+                (gpgkey.ownerID for gpgkey in gpgkeys))
+            uploaders = getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+                person_ids, need_validity=True)
+            list(uploaders)
+
+            # Load SourcePackageNames.
+            bulk.load_related(
+                SourcePackageName, dsds, ("source_package_name_id",))
+
+        return DecoratedResultSet(
+            differences, pre_iter_hook=eager_load)
+
     @staticmethod
     def getByDistroSeriesAndName(distro_series, source_package_name):
         """See `IDistroSeriesDifferenceSource`."""
@@ -178,6 +375,21 @@
         """See `IDistroSeriesDifference`."""
         return self._getLatestSourcePub(for_parent=True)
 
+    def _getLatestSourcePub(self, for_parent=False):
+        """Helper to keep source_pub/parent_source_pub DRY."""
+        distro_series = self.derived_series
+        if for_parent:
+            distro_series = self.derived_series.parent_series
+
+        pubs = distro_series.getPublishedSources(
+            self.source_package_name, include_pending=True)
+
+        # The most recent published source is the first one.
+        try:
+            return pubs[0]
+        except IndexError:
+            return None
+
     @cachedproperty
     def base_source_pub(self):
         """See `IDistroSeriesDifference`."""
@@ -284,28 +496,13 @@
         else:
             return self.parent_package_diff.status
 
-    def _getLatestSourcePub(self, for_parent=False):
-        """Helper to keep source_pub/parent_source_pub DRY."""
-        distro_series = self.derived_series
-        if for_parent:
-            distro_series = self.derived_series.parent_series
-
-        pubs = distro_series.getPublishedSources(
-            self.source_package_name, include_pending=True)
-
-        # The most recent published source is the first one.
-        try:
-            return pubs[0]
-        except IndexError:
-            return None
-
-    @property
+    @cachedproperty
     def parent_source_package_release(self):
         return self._package_release(
             self.derived_series.parent_series,
             self.parent_source_version)
 
-    @property
+    @cachedproperty
     def source_package_release(self):
         return self._package_release(
             self.derived_series,
@@ -438,6 +635,11 @@
         return getUtility(IDistroSeriesDifferenceCommentSource).new(
             self, commenter, comment)
 
+    @cachedproperty
+    def latest_comment(self):
+        """See `IDistroSeriesDifference`."""
+        return self.getComments().first()
+
     def getComments(self):
         """See `IDistroSeriesDifference`."""
         DSDComment = DistroSeriesDifferenceComment

=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	2011-04-06 07:52:33 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2011-04-14 21:12:40 +0000
@@ -62,6 +62,12 @@
                             source_pub difference/source_pub;
                             src_name difference/source_package_name/name;"
                 tal:attributes="class src_name">
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 0 QUERIES per row.
+              </tal:comment>
               <td>
                 <input tal:condition="view/canPerformSync"
                   name="field.selected_differences" type="checkbox"
@@ -72,6 +78,13 @@
                 <a tal:attributes="href difference/fmt:url" class="toggle-extra"
                    tal:content="src_name">Foo</a>
               </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 1 QUERY per row:
+                SourcePackagePublishingHistory
+              </tal:comment>
               <td tal:condition="view/show_parent_version">
                 <a tal:condition="difference/parent_source_package_release"
                    tal:attributes="href difference/parent_source_package_release/fmt:url"
@@ -83,6 +96,12 @@
                       tal:content="difference/parent_source_version">
                 </span>
               </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 1 QUERY per row: SourcePackagePublishingHistory
+              </tal:comment>
               <td tal:condition="view/show_derived_version">
                 <a tal:condition="difference/source_package_release"
                    tal:attributes="href difference/source_package_release/fmt:url"
@@ -93,33 +112,67 @@
                     class="derived-version"
                     tal:content="difference/source_version">
                 </span>
-               </td>
+              </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 1 QUERY per row: PackageSet
+              </tal:comment>
               <td tal:condition="view/show_parent_packagesets"
                   class="parent-packagesets">
                 <tal:replace replace="difference/@@/parent_packagesets_names"/>
               </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 0 QUERIES per row.
+              </tal:comment>
               <td tal:condition="view/show_packagesets"
                   class="packagesets">
                 <tal:replace replace="difference/@@/packagesets_names" />
               </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 1 QUERY per row.
+              </tal:comment>
               <td>
                 <tal:parent condition="not: view/show_derived_version">
-                  <span tal:attributes="title difference/parent_source_pub/datepublished/fmt:datetime"
-                        tal:content="difference/parent_source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
-                  <tal:signer condition="parent_source_pub/sourcepackagerelease/uploader">
-                    by <a tal:replace="structure parent_source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
-                  </tal:signer>
+                  <tal:published condition="parent_source_pub">
+                    <span tal:attributes="title parent_source_pub/datepublished/fmt:datetime"
+                          tal:content="parent_source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
+                    <tal:signer condition="parent_source_pub/sourcepackagerelease/uploader">
+                      by <a tal:replace="structure parent_source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
+                    </tal:signer>
+                  </tal:published>
+                  <tal:not-published condition="not:parent_source_pub">
+                    <span tal:content="difference/parent_source_version" />
+                  </tal:not-published>
                 </tal:parent>
                 <tal:derived condition="view/show_derived_version">
-                  <span tal:attributes="title difference/source_pub/datepublished/fmt:datetime"
-                        tal:content="difference/source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
-                  <tal:signer condition="source_pub/sourcepackagerelease/uploader">
-                    by <a tal:replace="structure source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
-                  </tal:signer>
+                  <tal:published condition="parent_source_pub">
+                    <span tal:attributes="title source_pub/datepublished/fmt:datetime"
+                          tal:content="source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
+                    <tal:signer condition="source_pub/sourcepackagerelease/uploader">
+                      by <a tal:replace="structure source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
+                    </tal:signer>
+                  </tal:published>
+                  <tal:not-published condition="not:source_pub">
+                    <span tal:content="difference/source_version" />
+                  </tal:not-published>
                 </tal:derived>
               </td>
+
+              <tal:comment replace="nothing">
+                XXX: GavinPanella 2011-04-14 bug=760733: In
+                TestDistroSeriesLocalDifferences.test_queries, this
+                cell needs 1 QUERY per row: MessageChunk
+              </tal:comment>
               <td>
-                <tal:comment tal:define="comment python:difference.getComments().first();"
+                <tal:comment tal:define="comment difference/latest_comment"
                              tal:condition="comment">
                   <span tal:replace="comment/body_text/fmt:shorten/50">I'm on this.</span>
                   <br /><span class="greyed-out greylink"><span
@@ -128,6 +181,7 @@
                       comment/comment_author/fmt:link">joesmith</a></span>
                 </tal:comment>
               </td>
+
             </tr>
 
             </tal:difference>

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-14 20:06:33 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-14 21:12:40 +0000
@@ -29,6 +29,10 @@
     IDistroSeriesDifference,
     IDistroSeriesDifferenceSource,
     )
+from lp.registry.model.distroseriesdifference import (
+    most_recent_comments,
+    most_recent_publications,
+    )
 from lp.services.propertycache import get_property_cache
 from lp.soyuz.enums import PackageDiffStatus
 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
@@ -322,6 +326,33 @@
         self.assertEqual(
             [dsd_comment_2, dsd_comment], list(ds_diff.getComments()))
 
+<<<<<<< TREE
+=======
+    def test_latest_comment(self):
+        # latest_comment is a property containing the most recent comment.
+        ds_diff = self.factory.makeDistroSeriesDifference()
+
+        with person_logged_in(ds_diff.owner):
+            comments = [
+                ds_diff.addComment(
+                    ds_diff.owner, "Wait until version 2.1"),
+                ds_diff.addComment(
+                    ds_diff.owner, "Wait until version 2.1"),
+                ]
+
+        self.assertEqual(comments[-1], ds_diff.latest_comment)
+
+    def test_addComment_not_public(self):
+        # Comments cannot be added with launchpad.View.
+        ds_diff = self.factory.makeDistroSeriesDifference()
+        person = self.factory.makePerson()
+
+        with person_logged_in(person):
+            self.assertTrue(check_permission('launchpad.View', ds_diff))
+            self.assertFalse(check_permission('launchpad.Edit', ds_diff))
+            self.assertRaises(Unauthorized, getattr, ds_diff, 'addComment')
+
+>>>>>>> MERGE-SOURCE
     def test_addComment_for_owners(self):
         # Comments can be added by any of the owners of the derived
         # series.
@@ -844,3 +875,114 @@
             ds_diff.derived_series, 'fooname')
 
         self.assertEqual(ds_diff, result)
+
+
+class TestMostRecentComments(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_most_recent_comments(self):
+        derived_series = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+        dsds = set(
+            self.factory.makeDistroSeriesDifference(
+                derived_series=derived_series) for index in xrange(5))
+        expected_comments = set()
+        for dsd in dsds:
+            # Add a couple of comments.
+            self.factory.makeDistroSeriesDifferenceComment(dsd)
+            expected_comments.add(
+                self.factory.makeDistroSeriesDifferenceComment(dsd))
+        self.assertContentEqual(
+            expected_comments, most_recent_comments(dsds))
+
+
+class TestMostRecentPublications(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def create_difference(self, derived_series):
+        # Create a new DistroSeriesDifference
+        version = self.factory.getUniqueInteger()
+        versions = {
+            'base': u'1.%d' % version,
+            'derived': u'1.%dderived1' % version,
+            'parent': u'1.%d-1' % version,
+            }
+        dsd = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series,
+            versions=versions)
+        # Push a base_version in... not sure how better to do it.
+        removeSecurityProxy(dsd).base_version = versions["base"]
+        return dsd
+
+    def test_simple(self):
+        derived_series = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+        dsds = [
+            self.create_difference(derived_series),
+            self.create_difference(derived_series),
+            ]
+        # Derived publication.
+        source_pubs_by_spn_id_expected = set(
+            (dsd.source_package_name.id, dsd.source_pub)
+            for dsd in dsds)
+        source_pubs_by_spn_id_found = most_recent_publications(
+            dsds, in_parent=False, statuses=(
+                PackagePublishingStatus.PUBLISHED,
+                PackagePublishingStatus.PENDING))
+        self.assertContentEqual(
+            source_pubs_by_spn_id_expected,
+            source_pubs_by_spn_id_found)
+        # Parent publication
+        parent_source_pubs_by_spn_id_expected = set(
+            (dsd.source_package_name.id, dsd.parent_source_pub)
+            for dsd in dsds)
+        parent_source_pubs_by_spn_id_found = most_recent_publications(
+            dsds, in_parent=True, statuses=(
+                PackagePublishingStatus.PUBLISHED,
+                PackagePublishingStatus.PENDING))
+        self.assertContentEqual(
+            parent_source_pubs_by_spn_id_expected,
+            parent_source_pubs_by_spn_id_found)
+
+    def test_statuses(self):
+        derived_series = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+        dsd = self.create_difference(derived_series)
+        # Change the derived source publication to DELETED.
+        removeSecurityProxy(dsd.source_pub).status = (
+            PackagePublishingStatus.DELETED)
+        # Searching for DELETED will find the source publication.
+        self.assertContentEqual(
+            [(dsd.source_package_name.id, dsd.source_pub)],
+            most_recent_publications(
+                [dsd], in_parent=False, statuses=(
+                    PackagePublishingStatus.DELETED,)))
+        # Searched for DELETED will *not* find the parent publication.
+        self.assertContentEqual(
+            [], most_recent_publications(
+                [dsd], in_parent=True, statuses=(
+                    PackagePublishingStatus.DELETED,)))
+
+    def test_match_version(self):
+        # When match_version is True, the version of the publications (well,
+        # the release) must exactly match those recorded on the
+        # DistroSeriesDifference.
+        derived_series = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+        dsd = self.create_difference(derived_series)
+        # Modify the release version.
+        removeSecurityProxy(
+            dsd.source_package_release.sourcepackagerelease).version += u"2"
+        # Searching with match_version=False finds the publication.
+        self.assertContentEqual(
+            [(dsd.source_package_name.id, dsd.source_pub)],
+            most_recent_publications(
+                [dsd], in_parent=False, match_version=False,
+                statuses=(PackagePublishingStatus.PUBLISHED,)))
+        # Searching with match_version=True does not find the publication.
+        self.assertContentEqual(
+            [], most_recent_publications(
+                [dsd], in_parent=False, match_version=True,
+                statuses=(PackagePublishingStatus.PUBLISHED,)))

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-04-07 01:08:21 +0000
+++ lib/lp/testing/__init__.py	2011-04-14 21:12:40 +0000
@@ -6,19 +6,20 @@
 from __future__ import absolute_import
 from lp.testing.windmill.lpuser import LaunchpadUser
 
+
 __metaclass__ = type
 __all__ = [
     'ANONYMOUS',
     'anonymous_logged_in',
     'api_url',
+    'BrowserTestCase',
     'build_yui_unittest_suite',
-    'BrowserTestCase',
     'celebrity_logged_in',
     'FakeTime',
     'get_lsb_information',
     'is_logged_in',
+    'launchpadlib_credentials_for',
     'launchpadlib_for',
-    'launchpadlib_credentials_for',
     'login',
     'login_as',
     'login_celebrity',
@@ -31,13 +32,13 @@
     'person_logged_in',
     'quote_jquery_expression',
     'record_statements',
+    'run_script',
     'run_with_login',
     'run_with_storm_debug',
-    'run_script',
     'StormStatementRecorder',
+    'test_tales',
     'TestCase',
     'TestCaseWithFactory',
-    'test_tales',
     'time_counter',
     'unlink_source_packages',
     'validate_mock_class',
@@ -166,6 +167,26 @@
     lpuser,
     )
 
+# The following names have been imported for the purpose of being
+# exported. They are referred to here to silence lint warnings.
+anonymous_logged_in
+api_url
+celebrity_logged_in
+is_logged_in
+launchpadlib_credentials_for
+launchpadlib_for
+login_as
+login_celebrity
+login_person
+login_team
+oauth_access_token_for
+person_logged_in
+run_with_login
+test_tales
+with_anonymous_login
+with_celebrity_logged_in
+with_person_logged_in
+
 
 class FakeTime:
     """Provides a controllable implementation of time.time().
@@ -809,7 +830,6 @@
             person.displayname, naked_person.preferredemail.email, password)
         return self.getClientFor(url, user=user)
 
-
     def getClientForAnomymous(self, obj, view_name=None):
         """Return a new client, and the url that it has loaded."""
         client = WindmillTestClient(self.suite_name)


Follow ups