← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/db-bug-788526 into lp:launchpad/db-devel

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/db-bug-788526 into lp:launchpad/db-devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #788526 in Launchpad itself: "Display in-progress DSDJobs"
  https://bugs.launchpad.net/launchpad/+bug/788526

For more details, see:
https://code.launchpad.net/~jtv/launchpad/db-bug-788526/+merge/62623

= Summary =

When a DistroSeriesDifference on the DistroSeries:+localpackagediffs page is waiting for a sync to be completed, it is marked with "updating..."

We want that for pending DSD updates as well.


== Proposed fix ==

Very similar to what we already do for the package-copy jobs (the package syncs): add a "find jobs relevant to these DSDs" method to the job source.  Call it once from a cachedproperty on the view, for the entire batch.  Have a simple lookup method on the view to see whether a given DSD has jobs pending.

This was also a nice opportunity to decisions from TAL to the view class.  I added a view method that computes the new string (and note that a DSD may be syncing and updating at the same time).


== Pre-implementation notes ==

Discussed with Julian.  Note that a pending DSD update should not stop the user from requesting a sync for that DSD.  In that respect this change differs from the one for pending syncs.


== Implementation details ==

I cleaned up some "if/elif/elif" code in cached_differences, replacing it with a dict.  One of the items in the code was a bit irregular (a mono-tuple instead of an item, which in this case looks like it does the same thing) but I was too lazy to check whether that could be cleaned up as well.  I left it in.

The set_derived_series_difference_jobs_feature_flag in lib/lp/registry/browser/tests/test_distroseries.py conforms to other similar functions right above it in the same module.

In lib/lp/soyuz/tests.test_distroseriesdifference.py I found a few test methods that were wrongly named so that they weren't run as tests.  (Probably my fault; I've caught myself doing that a few times.  I've made it a point to watch tests fail before I start making them succeed.)  One needed a change to the test, but not one with any semantic implications.  Otherwise, no surprises.

Something I kept running into is that DistroSeriesDifferenceJobs are not aware of multi-parent derived series yet.  That change is going to affect this code in several places.


== Tests ==

{{{
./bin/test -vvc lp.registry.browser.tests.test_distroseries
./bin/test -vvc lp.soyuz.tests.test_distroseriesdifferencejob
}}}


== Demo and Q/A ==

Look at a DSDs page such as https://staging.launchpad.net/ubuntu/oneiric/+localpackagediffs

Requesting asynchronous package syncs for DSDs should cause them to be marked as "synchronizing..."

Updating a package and reloading +localpackagediffs should cause the corresponding DSD to be marked as "updating..."

The two messages may also combine.


= Launchpad lint =

I added no lint and I fixed some pre-existing lint, but left some in place as well:

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/templates/distroseries-localdifferences.pt
  lib/lp/soyuz/model/distroseriesdifferencejob.py
  lib/lp/registry/browser/tests/test_distroseries.py
  lib/lp/soyuz/interfaces/distributionjob.py
  lib/lp/soyuz/tests/test_distroseriesdifferencejob.py
  lib/lp/registry/browser/distroseries.py

./lib/lp/soyuz/tests/test_distroseriesdifferencejob.py
     259: E261 at least two spaces before inline comment
     283: E261 at least two spaces before inline comment
     399: E261 at least two spaces before inline comment
     417: E261 at least two spaces before inline comment
     423: E261 at least two spaces before inline comment
-- 
https://code.launchpad.net/~jtv/launchpad/db-bug-788526/+merge/62623
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/db-bug-788526 into lp:launchpad/db-devel.
=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-05-24 10:08:33 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-05-27 08:21:17 +0000
@@ -104,6 +104,9 @@
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.soyuz.browser.archive import PackageCopyingMixin
 from lp.soyuz.browser.packagesearch import PackageSearchViewBase
+from lp.soyuz.interfaces.distributionjob import (
+    IDistroSeriesDifferenceJobSource,
+    )
 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
 from lp.soyuz.interfaces.queue import IPackageUploadSet
 from lp.soyuz.model.packagecopyjob import specify_dsd_package
@@ -863,6 +866,21 @@
         job_source = getUtility(IPlainPackageCopyJobSource)
         return job_source.getPendingJobsPerPackage(self.context)
 
+    @cachedproperty
+    def pending_dsd_updates(self):
+        """Pending `DistroSeriesDifference` update jobs.
+
+        :return: A `set` of `DistroSeriesDifference`s that have pending
+            `DistroSeriesDifferenceJob`s.
+        """
+        job_source = getUtility(IDistroSeriesDifferenceJobSource)
+        return job_source.getPendingJobsForDifferences(
+            self.context, self.cached_differences.batch)
+
+    def hasPendingDSDUpdate(self, dsd):
+        """Have there been changes that `dsd` is still being updated for?"""
+        return dsd in self.pending_dsd_updates
+
     def hasPendingSync(self, dsd):
         """Is there a package-copying job pending to resolve `dsd`?"""
         return self.pending_syncs.get(specify_dsd_package(dsd)) is not None
@@ -898,6 +916,28 @@
         return (
             not self.isNewerThanParent(dsd) and not self.hasPendingSync(dsd))
 
+    def describeJobs(self, dsd):
+        """Describe any jobs that may be pending for `dsd`.
+
+        Shows "synchronizing..." if the entry is being synchronized, and
+        "updating..." if the DSD is being updated with package changes.
+
+        :param dsd: A `DistroSeriesDifference` on the page.
+        :return: An HTML text describing work that is pending or in
+            progress for `dsd`; or None.
+        """
+        has_pending_dsd_update = self.hasPendingDSDUpdate(dsd)
+        has_pending_sync = self.hasPendingSync(dsd)
+        if not has_pending_dsd_update and not has_pending_sync:
+            return None
+
+        description = []
+        if has_pending_dsd_update:
+            description.append("updating")
+        if has_pending_sync:
+            description.append("synchronizing")
+        return " and ".join(description) + "…"
+
     @property
     def specified_name_filter(self):
         """If specified, return the name filter from the GET form data."""
@@ -924,23 +964,18 @@
     @cachedproperty
     def cached_differences(self):
         """Return a batch navigator of filtered results."""
-        if self.specified_package_type == NON_IGNORED:
-            status = (
-                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,)
-            child_version_higher = False
-        elif self.specified_package_type == IGNORED:
-            status = (
-                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
-            child_version_higher = False
-        elif self.specified_package_type == HIGHER_VERSION_THAN_PARENT:
-            status = (
-                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
-            child_version_higher = True
-        elif self.specified_package_type == RESOLVED:
-            status = DistroSeriesDifferenceStatus.RESOLVED
-            child_version_higher = False
-        else:
-            raise AssertionError('specified_package_type unknown')
+        package_type_dsd_status = {
+            NON_IGNORED: (
+                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,),
+            IGNORED: DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
+            HIGHER_VERSION_THAN_PARENT: (
+                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT),
+            RESOLVED: DistroSeriesDifferenceStatus.RESOLVED,
+        }
+
+        status = package_type_dsd_status[self.specified_package_type]
+        child_version_higher = (
+            self.specified_package_type == HIGHER_VERSION_THAN_PARENT)
 
         differences = getUtility(
             IDistroSeriesDifferenceSource).getForDistroSeries(

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-05-24 14:22:31 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-05-27 08:21:17 +0000
@@ -66,6 +66,7 @@
     )
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.distributionjob import (
+    IDistroSeriesDifferenceJobSource,
     IInitialiseDistroSeriesJobSource,
     )
 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
@@ -73,6 +74,7 @@
     ISourcePackageFormatSelectionSet,
     )
 from lp.soyuz.model.archivepermission import ArchivePermission
+from lp.soyuz.model import distroseriesdifferencejob
 from lp.soyuz.model.packagecopyjob import specify_dsd_package
 from lp.testing import (
     anonymous_logged_in,
@@ -85,6 +87,7 @@
     TestCaseWithFactory,
     with_celebrity_logged_in,
     )
+from lp.testing.fakemethod import FakeMethod
 from lp.testing.matchers import HasQueryCount
 from lp.testing.views import create_initialized_view
 
@@ -102,6 +105,12 @@
         }))
 
 
+def set_derived_series_difference_jobs_feature_flag(test_case):
+    test_case.useFixture(FeatureFixture({
+        distroseriesdifferencejob.FEATURE_FLAG_ENABLE_MODULE: u'on',
+        }))
+
+
 class TestDistroSeriesView(TestCaseWithFactory):
     """Test the distroseries +index view."""
 
@@ -1095,6 +1104,14 @@
 
     layer = LaunchpadFunctionalLayer
 
+    def makeDSDJob(self, dsd):
+        """Create a `DistroSeriesDifferenceJob` to update `dsd`."""
+        job_source = getUtility(IDistroSeriesDifferenceJobSource)
+        jobs = job_source.createForPackagePublication(
+            dsd.derived_series, dsd.source_package_name,
+            PackagePublishingPocket.RELEASE)
+        return jobs[0]
+
     def test_higher_radio_mentions_parent(self):
         # The user is shown an option to display only the blacklisted
         # package with a higer version than in the parent.
@@ -1345,6 +1362,20 @@
 
             self.assertFalse(view.canPerformSync())
 
+    def test_hasPendingDSDUpdate_returns_False_if_no_pending_update(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        self.assertFalse(view.hasPendingDSDUpdate(dsd))
+
+    def test_hasPendingDSDUpdate_returns_True_if_pending_update(self):
+        set_derived_series_difference_jobs_feature_flag(self)
+        dsd = self.factory.makeDistroSeriesDifference()
+        self.makeDSDJob(dsd)
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        self.assertTrue(view.hasPendingDSDUpdate(dsd))
+
     def test_hasPendingSync_returns_False_if_no_pending_sync(self):
         dsd = self.factory.makeDistroSeriesDifference()
         view = create_initialized_view(
@@ -1423,6 +1454,44 @@
             dsd.derived_series, '+localpackagediffs')
         self.assertTrue(view.canRequestSync(dsd))
 
+    def test_canRequestSync_ignores_DSDJobs(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        view.hasPendingDSDUpdate = FakeMethod(result=True)
+        self.assertTrue(view.canRequestSync(dsd))
+
+    def test_describeJobs_returns_None_if_no_jobs(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        self.assertIs(None, view.describeJobs(dsd))
+
+    def test_describeJobs_reports_pending_update(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        view.hasPendingDSDUpdate = FakeMethod(result=True)
+        view.hasPendingSync = FakeMethod(result=False)
+        self.assertEqual("updating…", view.describeJobs(dsd))
+
+    def test_describeJobs_reports_pending_sync(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        view.hasPendingDSDUpdate = FakeMethod(result=False)
+        view.hasPendingSync = FakeMethod(result=True)
+        self.assertEqual("synchronizing…", view.describeJobs(dsd))
+
+    def test_describeJobs_reports_pending_sync_and_update(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        view = create_initialized_view(
+            dsd.derived_series, '+localpackagediffs')
+        view.hasPendingDSDUpdate = FakeMethod(result=True)
+        view.hasPendingSync = FakeMethod(result=True)
+        self.assertEqual(
+            "updating and synchronizing…", view.describeJobs(dsd))
+
     def _syncAndGetView(self, derived_series, person, sync_differences,
                         difference_type=None, view_name='+localpackagediffs'):
         # A helper to get the POST'ed sync view.

=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	2011-05-24 11:32:34 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2011-05-27 08:21:17 +0000
@@ -94,11 +94,12 @@
                    class="js-action toggle-extra"
                    tal:content="src_name">Foo</a>
 
-                <span
-                    class="lowlight"
-                    tal:condition="python: view.hasPendingSync(difference)">
-                    &nbsp;&nbsp;synchronizing&hellip;
-                </span>
+                <tal:activity
+                    define="activity python:view.describeJobs(difference)"
+                    condition="activity">
+                  &nbsp;&nbsp;
+                  <span class="lowlight" tal:content="structure activity"></span>
+                </tal:activity>
               </td>
               <td tal:condition="python: not(view.has_unique_parent) and view.show_parent_version">
                 <a tal:attributes="href difference/parent_series/fmt:url"

=== modified file 'lib/lp/soyuz/interfaces/distributionjob.py'
--- lib/lp/soyuz/interfaces/distributionjob.py	2011-05-13 10:00:02 +0000
+++ lib/lp/soyuz/interfaces/distributionjob.py	2011-05-27 08:21:17 +0000
@@ -104,6 +104,17 @@
         :param pocket: The `PackagePublishingPocket` for the publication.
         """
 
+    def getPendingJobsForDifferences(derived_series, distroseriesdifferences):
+        """Find `DistroSeriesDifferenceJob`s for `DistroSeriesDifference`s.
+
+        :param derived_series: The derived `DistroSeries` that the
+            differences (and jobs) must be for.
+        :param distroseriesdifferences:
+            An iterable of `DistroSeriesDifference`s.
+        :return: A dict mapping each of `distroseriesdifferences` that has
+            pending jobs to a list of its jobs.
+        """
+
 
 class IDistroSeriesDifferenceJob(IRunnableJob):
         """A Job that performs actions related to DSDs."""

=== modified file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
--- lib/lp/soyuz/model/distroseriesdifferencejob.py	2011-05-20 14:36:26 +0000
+++ lib/lp/soyuz/model/distroseriesdifferencejob.py	2011-05-27 08:21:17 +0000
@@ -14,7 +14,10 @@
     implements,
     )
 
-from canonical.launchpad.interfaces.lpstorm import IMasterStore
+from canonical.launchpad.interfaces.lpstorm import (
+    IMasterStore,
+    IStore,
+    )
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
     )
@@ -139,6 +142,29 @@
                 jobs.append(create_job(relative, sourcepackagename))
         return jobs
 
+    @classmethod
+    def getPendingJobsForDifferences(cls, derived_series,
+                                     distroseriesdifferences):
+        """See `IDistroSeriesDifferenceJobSource`."""
+        jobs = IStore(DistributionJob).find(
+            DistributionJob,
+            DistributionJob.job_type == cls.class_job_type,
+            Job.id == DistributionJob.job_id,
+            Job._status.is_in(Job.PENDING_STATUSES),
+            DistributionJob.distroseries == derived_series)
+
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Check for parent
+        # series once it becomes available.
+        keyed_dsds = dict(
+            (dsd.source_package_name.id, dsd)
+            for dsd in distroseriesdifferences)
+        jobs_by_dsd = {}
+        for job in jobs:
+            dsd = keyed_dsds.get(job.metadata["sourcepackagename"])
+            if dsd is not None:
+                jobs_by_dsd.setdefault(dsd, []).append(cls(job))
+        return jobs_by_dsd
+
     @property
     def sourcepackagename(self):
         return SourcePackageName.get(self.metadata['sourcepackagename'])

=== modified file 'lib/lp/soyuz/tests/test_distroseriesdifferencejob.py'
--- lib/lp/soyuz/tests/test_distroseriesdifferencejob.py	2011-05-23 10:54:01 +0000
+++ lib/lp/soyuz/tests/test_distroseriesdifferencejob.py	2011-05-27 08:21:17 +0000
@@ -9,6 +9,7 @@
 from psycopg2 import ProgrammingError
 from zope.component import getUtility
 from zope.interface.verify import verifyObject
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from canonical.launchpad.scripts.tests import run_script
@@ -26,8 +27,10 @@
 from lp.services.job.interfaces.job import JobStatus
 from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.distributionjob import (
+    DistributionJobType,
     IDistroSeriesDifferenceJobSource,
     )
+from lp.soyuz.model.distributionjob import DistributionJob
 from lp.soyuz.model.distroseriesdifferencejob import (
     create_job,
     DistroSeriesDifferenceJob,
@@ -108,15 +111,17 @@
         dsdjob = create_job(distroseries, package)
         self.assertEqual(JobStatus.WAITING, dsdjob.job.status)
 
-    def find_waiting_jobs_finds_waiting_jobs(self):
+    def test_find_waiting_jobs_finds_waiting_jobs(self):
         sourcepackage = self.factory.makeSourcePackage()
         distroseries, sourcepackagename = (
             sourcepackage.distroseries, sourcepackage.distroseries)
         job = create_job(distroseries, sourcepackagename)
-        self.assertContentEqual(
-            [job], find_waiting_jobs(distroseries, sourcepackagename))
+        found_jobs = find_waiting_jobs(distroseries, sourcepackagename)
+        self.assertEqual([job], [
+            DistroSeriesDifferenceJob(found_job)
+            for found_job in found_jobs])
 
-    def find_waiting_jobs_ignores_other_series(self):
+    def test_find_waiting_jobs_ignores_other_series(self):
         sourcepackage = self.factory.makeSourcePackage()
         distroseries, sourcepackagename = (
             sourcepackage.distroseries, sourcepackage.distroseries)
@@ -125,7 +130,7 @@
         self.assertContentEqual(
             [], find_waiting_jobs(other_series, sourcepackagename))
 
-    def find_waiting_jobs_ignores_other_packages(self):
+    def test_find_waiting_jobs_ignores_other_packages(self):
         sourcepackage = self.factory.makeSourcePackage()
         distroseries, sourcepackagename = (
             sourcepackage.distroseries, sourcepackage.distroseries)
@@ -134,7 +139,7 @@
         self.assertContentEqual(
             [], find_waiting_jobs(distroseries, other_spn))
 
-    def find_waiting_jobs_considers_only_waiting_jobs(self):
+    def test_find_waiting_jobs_considers_only_waiting_jobs(self):
         sourcepackage = self.factory.makeSourcePackage()
         distroseries, sourcepackagename = (
             sourcepackage.distroseries, sourcepackage.distroseries)
@@ -192,6 +197,60 @@
             distroseries, package, PackagePublishingPocket.PROPOSED)
         self.assertContentEqual([], find_waiting_jobs(distroseries, package))
 
+    def test_getPendingJobsForDifferences_finds_job(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        job = create_job(dsd.derived_series, dsd.source_package_name)
+        self.assertEqual(
+            {dsd: [job]},
+            self.getJobSource().getPendingJobsForDifferences(
+                dsd.derived_series, [dsd]))
+
+    def test_getPendingJobsForDifferences_ignores_other_package(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        create_job(dsd.derived_series, self.factory.makeSourcePackageName())
+        self.assertEqual(
+            {},
+            self.getJobSource().getPendingJobsForDifferences(
+                dsd.derived_series, [dsd]))
+
+    def test_getPendingJobsForDifferences_ignores_other_derived_series(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        create_job(self.makeDerivedDistroSeries(), dsd.source_package_name)
+        self.assertEqual(
+            {},
+            self.getJobSource().getPendingJobsForDifferences(
+                dsd.derived_series, [dsd]))
+
+    def test_getPendingJobsForDifferences_ignores_other_parent_series(self):
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Can't test this
+        # until we can specify the right parent series when creating a
+        # job.
+        return
+
+    def test_getPendingJobsForDifferences_ignores_non_pending_jobs(self):
+        dsd = self.factory.makeDistroSeriesDifference()
+        job = create_job(dsd.derived_series, dsd.source_package_name)
+        removeSecurityProxy(job).job._status = JobStatus.COMPLETED
+        self.assertEqual(
+            {},
+            self.getJobSource().getPendingJobsForDifferences(
+                dsd.derived_series, [dsd]))
+
+    def test_getPendingJobsForDifferences_ignores_other_job_types(self):
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Once parent_series
+        # is incorporated into the job type, set it to dsd.parent_series
+        # on the fake job, or this test will become silently meaningless.
+        dsd = self.factory.makeDistroSeriesDifference()
+        DistributionJob(
+            distribution=dsd.derived_series.distribution,
+            distroseries=dsd.derived_series,
+            job_type=DistributionJobType.INITIALISE_SERIES,
+            metadata={"sourcepackagename": dsd.source_package_name.id})
+        self.assertEqual(
+            {},
+            self.getJobSource().getPendingJobsForDifferences(
+                dsd.derived_series, [dsd]))
+
     def test_cronscript(self):
         derived_series = self.makeDerivedDistroSeries()
         package = self.factory.makeSourcePackageName()
@@ -285,7 +344,7 @@
         parent_packageset = self.factory.makePackageset(
             distroseries=distro_series_parent.parent_series,
             packages=packages)
-        derived_packageset = self.factory.makePackageset(
+        self.factory.makePackageset(
             distroseries=distro_series_parent.derived_series,
             packages=packages, name=parent_packageset.name,
             owner=parent_packageset.owner, related_set=parent_packageset)
@@ -414,7 +473,8 @@
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         self.assertEqual(
-            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT, ds_diff[0].status)
+            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
+            ds_diff[0].status)
         self.assertEqual('1.0-1', ds_diff[0].base_version)
 
     def test_child_is_synced(self):