← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #788956 in Launchpad itself: "DSDJob.run assumes single parent"
  https://bugs.launchpad.net/launchpad/+bug/788956

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

= Summary =

DistroSeriesDifferenceJob was designed and implemented before multi-parent derived distroseries came along as a requirement.

This was starting to interfere with new code, basically forcing us to write new code against the old requirements instead of the new ones.


== Proposed fix ==

Make the job at least accept a parent_series.  Have the code accept None as a valid parent for now, so code that is completely oblivious of multi-parent series can continue to work as before.

Code that does support multi-parent needs to pass parent_series consistently.


== Pre-implementation notes ==

A proper solution is to be full multi-parent support, but that's about our lowest-priority sub-feature.


== Implementation details ==

I had to leave the integration tests as they were, because they exercise code that isn't being updated for multi-parent yet.


== Tests ==

{{{
./bin/test -vvc lp.soyuz.tests.test_distroseriesjob
}}}


== Demo and Q/A ==

If this passes tests and does not completely break DistroSeries:+localpackagediffs, it's probably harmless.  Only tests use the new bit of metadata so far.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/model/distroseriesdifferencejob.py
  database/schema/security.cfg
  lib/lp/soyuz/interfaces/distributionjob.py
  lib/lp/soyuz/tests/test_distroseriesdifferencejob.py
  lib/lp/registry/model/distroseriesdifference.py
-- 
https://code.launchpad.net/~jtv/launchpad/db-bug-788956/+merge/62669
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/db-bug-788956 into lp:launchpad/db-devel.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2011-05-25 17:29:39 +0000
+++ database/schema/security.cfg	2011-05-27 13:25:43 +0000
@@ -1373,6 +1373,7 @@
 public.distroarchseries                 = SELECT
 public.distrocomponentuploader          = SELECT
 public.distroseries                     = SELECT
+public.distroseriesdifference           = SELECT
 public.emailaddress                     = SELECT, INSERT, UPDATE
 public.flatpackagesetinclusion          = SELECT
 public.gpgkey                           = SELECT

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-05-24 11:32:34 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-05-27 13:25:43 +0000
@@ -288,6 +288,8 @@
     @staticmethod
     def new(derived_series, source_package_name, parent_series=None):
         """See `IDistroSeriesDifferenceSource`."""
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+        # mandatory as part of multi-parent support.
         if parent_series is None:
             try:
                 dsps = getUtility(IDistroSeriesParentSet)

=== 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 13:25:43 +0000
@@ -94,16 +94,23 @@
 class IDistroSeriesDifferenceJobSource(IJobSource):
     """An `IJob` for creating `DistroSeriesDifference`s."""
 
-    def createForPackagePublication(distroseries, sourcepackagename, pocket):
+    def createForPackagePublication(derivedseries, sourcepackagename, pocket,
+                                    parent_series=None):
         """Create jobs as appropriate for a given status publication.
 
-        :param distroseries: A `DistroSeries` that is assumed to be
-            derived from another one.
+        :param derived_series: A `DistroSeries` that is assumed to be
+            derived from `parent_series`.
         :param sourcepackagename: A `SourcePackageName` that is being
-            published in `distroseries`.
+            published in `derived_series` or `parent_series`.
         :param pocket: The `PackagePublishingPocket` for the publication.
+        :param parent_series: The parent `DistroSeries` whose version of
+            `sourcepackagename` is to be compared with that in
+            `derived_series`.
+        :return: An iterable of `DistroSeriesDifferenceJob`.
         """
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+        # mandatory as part of multi-parent support.
 
 
 class IDistroSeriesDifferenceJob(IRunnableJob):
-        """A Job that performs actions related to DSDs."""
+    """A `Job` that performs actions related to `DistroSeriesDifference`s."""

=== 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 13:25:43 +0000
@@ -14,12 +14,16 @@
     implements,
     )
 
-from canonical.launchpad.interfaces.lpstorm import IMasterStore
+from canonical.launchpad.interfaces.lpstorm import (
+    IMasterStore,
+    IStore,
+    )
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
     )
 from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.distroseriesdifference import DistroSeriesDifference
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.features import getFeatureFlag
@@ -39,50 +43,74 @@
 FEATURE_FLAG_ENABLE_MODULE = u"soyuz.derived_series_jobs.enabled"
 
 
-def make_metadata(sourcepackagename):
+def make_metadata(sourcepackagename, parent_series=None):
     """Return JSON metadata for a job on `sourcepackagename`."""
-    return {'sourcepackagename': sourcepackagename.id}
-
-
-def create_job(distroseries, sourcepackagename):
+    # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+    # mandatory as part of multi-parent support.
+    if parent_series is None:
+        parent_id = None
+    else:
+        parent_id = parent_series.id
+    return {
+        'sourcepackagename': sourcepackagename.id,
+        'parent_series': parent_id,
+    }
+
+
+def create_job(derived_series, sourcepackagename, parent_series=None):
     """Create a `DistroSeriesDifferenceJob` for a given source package.
 
-    :param distroseries: A `DistroSeries` that is assumed to be derived
+    :param derived_series: A `DistroSeries` that is assumed to be derived
         from another one.
     :param sourcepackagename: The `SourcePackageName` whose publication
         history has changed.
+    :param parent_series: A `DistroSeries` that is a parent of
+        `derived_series`.  The difference is between the versions of
+        `sourcepackagename` in `parent_series` and `derived_series`.
     """
+    # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+    # mandatory as part of multi-parent support.
     job = DistributionJob(
-        distribution=distroseries.distribution, distroseries=distroseries,
+        distribution=derived_series.distribution, distroseries=derived_series,
         job_type=DistributionJobType.DISTROSERIESDIFFERENCE,
-        metadata=make_metadata(sourcepackagename))
+        metadata=make_metadata(sourcepackagename, parent_series))
     IMasterStore(DistributionJob).add(job)
     return DistroSeriesDifferenceJob(job)
 
 
-def find_waiting_jobs(distroseries, sourcepackagename):
+def find_waiting_jobs(derived_series, sourcepackagename, parent_series=None):
     """Look for pending `DistroSeriesDifference` jobs on a package."""
     # Look for identical pending jobs.  This compares directly on
     # the metadata string.  It's fragile, but this is only an
     # optimization.  It's not actually disastrous to create
     # redundant jobs occasionally.
     json_metadata = DistributionJob.serializeMetadata(
-        make_metadata(sourcepackagename))
+        make_metadata(sourcepackagename, parent_series))
 
     # Use master store because we don't like outdated information
     # here.
     store = IMasterStore(DistributionJob)
 
-    return store.find(
+    candidates = store.find(
         DistributionJob,
         DistributionJob.job_type ==
             DistributionJobType.DISTROSERIESDIFFERENCE,
-        DistributionJob.distroseries == distroseries,
+        DistributionJob.distroseries == derived_series,
         DistributionJob._json_data == json_metadata,
         DistributionJob.job_id.is_in(Job.ready_jobs))
 
-
-def may_require_job(distroseries, sourcepackagename):
+    # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+    # mandatory as part of multi-parent support.
+    if parent_series is None:
+        return list(candidates)
+
+    return [
+        job
+        for job in candidates
+            if job.metadata["parent_series"] == parent_series.id]
+
+
+def may_require_job(derived_series, sourcepackagename, parent_series=None):
     """Might publishing this package require a new job?
 
     Use this to determine whether to create a new
@@ -91,17 +119,21 @@
     runner some unnecessary work, but we don't expect a bit of
     unnecessary work to be a big problem.
     """
-    if distroseries is None:
+    # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+    # mandatory as part of multi-parent support.
+    if derived_series is None:
         return False
     dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
-        distroseries)
+        derived_series)
     if dsp.count() == 0:
         return False
     for parent in dsp:
-        if parent.parent_series.distribution == distroseries.distribution:
+        if parent.parent_series.distribution == derived_series.distribution:
             # Differences within a distribution are not tracked.
             return False
-    return find_waiting_jobs(distroseries, sourcepackagename).is_empty()
+    existing_jobs = find_waiting_jobs(
+        derived_series, sourcepackagename, parent_series)
+    return len(existing_jobs) == 0
 
 
 def has_package(distroseries, sourcepackagename):
@@ -119,9 +151,11 @@
     class_job_type = DistributionJobType.DISTROSERIESDIFFERENCE
 
     @classmethod
-    def createForPackagePublication(cls, distroseries, sourcepackagename,
-                                    pocket):
+    def createForPackagePublication(cls, derived_series, sourcepackagename,
+                                    pocket, parent_series=None):
         """See `IDistroSeriesDifferenceJobSource`."""
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+        # mandatory as part of multi-parent support.
         if not getFeatureFlag(FEATURE_FLAG_ENABLE_MODULE):
             return
         # -backports and -proposed are not really part of a standard
@@ -133,16 +167,30 @@
             PackagePublishingPocket.PROPOSED):
             return
         jobs = []
-        children = list(distroseries.getDerivedSeries())
-        for relative in children + [distroseries]:
-            if may_require_job(relative, sourcepackagename):
-                jobs.append(create_job(relative, sourcepackagename))
+        children = list(derived_series.getDerivedSeries())
+        for relative in children + [derived_series]:
+            if may_require_job(relative, sourcepackagename, parent_series):
+                jobs.append(create_job(
+                    relative, sourcepackagename, parent_series))
         return jobs
 
     @property
     def sourcepackagename(self):
         return SourcePackageName.get(self.metadata['sourcepackagename'])
 
+    @property
+    def derived_series(self):
+        return self.distroseries
+
+    @property
+    def parent_series(self):
+        parent_id = self.metadata['parent_series']
+        if not parent_id:
+            # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+            # mandatory as part of multi-parent support.
+            return None
+        return IStore(DistroSeries).get(DistroSeries, parent_id)
+
     def passesPackagesetFilter(self):
         """Is this package of interest as far as packagesets are concerned?
 
@@ -150,13 +198,19 @@
         missing in the derived series are only of interest if they are
         in a packageset that the derived series also has.
         """
-        derived_series = self.distroseries
-        dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
-            derived_series)
-        parent_series = dsp[0].parent_series
-        if has_package(derived_series, self.sourcepackagename):
+        derived_series = self.derived_series
+        parent_series = self.parent_series
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+        # mandatory as part of multi-parent support.
+        if parent_series is None:
+            dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+                derived_series)
+            parent_series = dsp[0].parent_series
+
+        sourcepackagename = self.sourcepackagename
+        if has_package(derived_series, sourcepackagename):
             return True
-        if not has_package(parent_series, self.sourcepackagename):
+        if not has_package(parent_series, sourcepackagename):
             return True
         packagesetset = getUtility(IPackagesetSet)
         if packagesetset.getBySeries(parent_series).is_empty():
@@ -164,26 +218,40 @@
             # case for e.g. Debian.  In that case, don't filter.
             return True
         parent_sets = packagesetset.setsIncludingSource(
-            self.sourcepackagename, distroseries=parent_series)
+            sourcepackagename, distroseries=parent_series)
         for parent_set in parent_sets:
             for related_set in parent_set.relatedSets():
                 if related_set.distroseries == derived_series:
                     return True
         return False
 
+    def getMatchingDSD(self):
+        """Find an existing `DistroSeriesDifference` for this difference."""
+        spn_id = self.metadata["sourcepackagename"]
+        parent_id = self.metadata["parent_series"]
+        store = IMasterStore(DistroSeriesDifference)
+        # XXX JeroenVermeulen 2011-05-26 bug=758906: Make parent_series
+        # mandatory as part of multi-parent support.
+        if parent_id is None:
+            match_parent = True
+        else:
+            match_parent = (
+                DistroSeriesDifference.parent_series_id == parent_id)
+        search = store.find(
+            DistroSeriesDifference,
+            DistroSeriesDifference.derived_series == self.derived_series,
+            DistroSeriesDifference.source_package_name_id == spn_id,
+            match_parent)
+        return search.one()
+
     def run(self):
         """See `IRunnableJob`."""
         if not self.passesPackagesetFilter():
             return
 
-        store = IMasterStore(DistroSeriesDifference)
-        ds_diff = store.find(
-            DistroSeriesDifference,
-            DistroSeriesDifference.derived_series == self.distroseries,
-            DistroSeriesDifference.source_package_name ==
-            self.sourcepackagename).one()
+        ds_diff = self.getMatchingDSD()
         if ds_diff is None:
             ds_diff = getUtility(IDistroSeriesDifferenceSource).new(
-                self.distroseries, self.sourcepackagename)
+                self.distroseries, self.sourcepackagename, self.parent_series)
         else:
             ds_diff.update()

=== 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 13:25:43 +0000
@@ -39,6 +39,20 @@
 from lp.testing import TestCaseWithFactory
 
 
+def find_dsd_for(dsp, package):
+    """Find `DistroSeriesDifference`.
+
+    :param dsp: `DistroSeriesParent`.
+    :param package: `SourcePackageName`.
+    """
+    store = IMasterStore(DistroSeriesDifference)
+    return store.find(
+        DistroSeriesDifference,
+        DistroSeriesDifference.derived_series == dsp.derived_series,
+        DistroSeriesDifference.parent_series == dsp.parent_series,
+        DistroSeriesDifference.source_package_name == package)
+
+
 class TestDistroSeriesDifferenceJobSource(TestCaseWithFactory):
     """Tests for `IDistroSeriesDifferenceJobSource`."""
 
@@ -60,91 +74,124 @@
 
     def test_make_metadata_is_consistent(self):
         package = self.factory.makeSourcePackageName()
-        self.assertEqual(make_metadata(package), make_metadata(package))
+        parent_series = self.factory.makeDistroSeries()
+        self.assertEqual(
+            make_metadata(package, parent_series),
+            make_metadata(package, parent_series))
 
     def test_make_metadata_distinguishes_packages(self):
+        parent_series = self.factory.makeDistroSeries()
         one_package = self.factory.makeSourcePackageName()
         another_package = self.factory.makeSourcePackageName()
         self.assertNotEqual(
-            make_metadata(one_package), make_metadata(another_package))
-
-    def test_may_require_job_accepts_none_distroseries(self):
-        package = self.factory.makeSourcePackageName()
-        self.assertFalse(may_require_job(None, package))
+            make_metadata(one_package, parent_series),
+            make_metadata(another_package, parent_series))
+
+    def test_make_metadata_distinguishes_parents(self):
+        package = self.factory.makeSourcePackageName()
+        one_parent = self.factory.makeDistroSeries()
+        another_parent = self.factory.makeDistroSeries()
+        self.assertNotEqual(
+            make_metadata(package, one_parent),
+            make_metadata(package, another_parent))
+
+    def test_may_require_job_accepts_none_derived_series(self):
+        parent_series = self.factory.makeDistroSeriesParent().parent_series
+        package = self.factory.makeSourcePackageName()
+        self.assertFalse(may_require_job(None, package, parent_series))
+
+    def test_may_require_job_accepts_none_parent_series(self):
+        derived_series = self.makeDerivedDistroSeries()
+        package = self.factory.makeSourcePackageName()
+        self.assertTrue(may_require_job(derived_series, package, None))
 
     def test_may_require_job_allows_new_jobs(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
-        self.assertTrue(may_require_job(distroseries, package))
+        self.assertTrue(may_require_job(
+            dsp.derived_series, package, dsp.parent_series))
 
     def test_may_require_job_forbids_redundant_jobs(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
-        create_job(distroseries, package)
-        self.assertFalse(may_require_job(distroseries, package))
+        create_job(dsp.derived_series, package, dsp.parent_series)
+        self.assertFalse(
+            may_require_job(dsp.derived_series, package, dsp.parent_series))
 
     def test_may_require_job_forbids_jobs_on_nonderived_series(self):
         sourcepackage = self.factory.makeSourcePackage()
         self.assertFalse(may_require_job(
-            sourcepackage.distroseries, sourcepackage.sourcepackagename))
+            sourcepackage.distroseries, sourcepackage.sourcepackagename,
+            None))
 
     def test_may_require_job_forbids_jobs_for_intra_distro_derivation(self):
         package = self.factory.makeSourcePackageName()
         parent = self.factory.makeDistroSeries()
         child = self.factory.makeDistroSeries(
             distribution=parent.distribution, previous_series=parent)
-        self.assertFalse(may_require_job(child, package))
+        self.assertFalse(may_require_job(child, package, parent))
 
     def test_may_require_job_only_considers_waiting_jobs_for_redundancy(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
-        existing_job = create_job(distroseries, package)
+        existing_job = create_job(
+            dsp.derived_series, package, dsp.parent_series)
         existing_job.job.start()
-        self.assertTrue(may_require_job(distroseries, package))
+        self.assertTrue(
+            may_require_job(dsp.derived_series, package, dsp.parent_series))
 
     def test_create_job_creates_waiting_job(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
-        dsdjob = create_job(distroseries, package)
+        dsdjob = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertEqual(JobStatus.WAITING, dsdjob.job.status)
 
     def 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))
-
-    def find_waiting_jobs_ignores_other_series(self):
-        sourcepackage = self.factory.makeSourcePackage()
-        distroseries, sourcepackagename = (
-            sourcepackage.distroseries, sourcepackage.distroseries)
-        create_job(distroseries, sourcepackagename)
-        other_series = self.factory.makeDistroSeries()
-        self.assertContentEqual(
-            [], find_waiting_jobs(other_series, sourcepackagename))
+        dsp = self.factory.makeDistroSeriesParent()
+        package = self.factory.makeSourcePackageName()
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
+        self.assertContentEqual(
+            [job],
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series))
+
+    def find_waiting_jobs_ignores_other_derived_series(self):
+        dsp = self.factory.makeDistroSeriesParent()
+        package = self.factory.makeSourcePackageName()
+        create_job(dsp.derived_series, package, dsp.parent_series)
+        other_series = self.factory.makeDistroSeries()
+        self.assertContentEqual(
+            [], find_waiting_jobs(other_series, package, dsp.parent_series))
+
+    def find_waiting_jobs_ignores_other_parent_series(self):
+        dsp = self.factory.makeDistroSeriesParent()
+        package = self.factory.makeSourcePackageName()
+        create_job(dsp.derived_series, package, dsp.parent_series)
+        other_series = self.factory.makeDistroSeries()
+        self.assertContentEqual(
+            [], find_waiting_jobs(dsp.derived_series, package, other_series))
 
     def find_waiting_jobs_ignores_other_packages(self):
-        sourcepackage = self.factory.makeSourcePackage()
-        distroseries, sourcepackagename = (
-            sourcepackage.distroseries, sourcepackage.distroseries)
-        create_job(distroseries, sourcepackagename)
-        other_spn = self.factory.makeSourcePackageName()
+        dsp = self.factory.makeDistroSeriesParent()
+        package = self.factory.makeSourcePackageName()
+        create_job(dsp.derived_series, package, dsp.parent_series)
+        other_package = self.factory.makeSourcePackageName()
         self.assertContentEqual(
-            [], find_waiting_jobs(distroseries, other_spn))
+            [],
+            find_waiting_jobs(
+                dsp.derived_series, other_package, dsp.parent_series))
 
     def find_waiting_jobs_considers_only_waiting_jobs(self):
-        sourcepackage = self.factory.makeSourcePackage()
-        distroseries, sourcepackagename = (
-            sourcepackage.distroseries, sourcepackage.distroseries)
-        job = create_job(distroseries, sourcepackagename)
+        dsp = self.factory.makeDistroSeriesParent()
+        package = self.factory.makeSourcePackageName()
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         job.start()
         self.assertContentEqual(
-            [], find_waiting_jobs(distroseries, sourcepackagename))
+            [],
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series))
         job.complete()
         self.assertContentEqual(
-            [], find_waiting_jobs(distroseries, sourcepackagename))
+            [],
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series))
 
     def test_createForPackagedPublication_creates_jobs_for_its_child(self):
         dsp = self.factory.makeDistroSeriesParent()
@@ -154,50 +201,59 @@
         # Create a job for the derived_series parent, which should create
         # two jobs. One for derived_series, and the other for its child.
         self.getJobSource().createForPackagePublication(
-            dsp.parent_series, package,
-            PackagePublishingPocket.RELEASE)
-        jobs = (list(
-            find_waiting_jobs(dsp.parent_series, package)) +
-            list(find_waiting_jobs(dsp.derived_series, package)))
-        self.assertEqual(2, len(jobs))
-        self.assertEqual(package.id, jobs[0].metadata['sourcepackagename'])
-        self.assertEqual(package.id, jobs[1].metadata['sourcepackagename'])
-        # Lastly, a job was not created for the grandparent.
-        jobs = list(find_waiting_jobs(parent_dsp.parent_series, package))
-        self.assertEqual(0, len(jobs))
+            parent_dsp.derived_series, package,
+            PackagePublishingPocket.RELEASE, parent_dsp.parent_series)
+        jobs = sum([
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series)
+            for dsp in [parent_dsp, parent_dsp]],
+            [])
+        self.assertEqual(
+            [package.id, package.id],
+            [job.metadata["sourcepackagename"] for job in jobs])
 
     def test_createForPackagePublication_creates_job_for_derived_series(self):
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         self.getJobSource().createForPackagePublication(
-            derived_series, package, PackagePublishingPocket.RELEASE)
-        jobs = list(find_waiting_jobs(derived_series, package))
-        self.assertEqual(1, len(jobs))
-        self.assertEqual(package.id, jobs[0].metadata['sourcepackagename'])
+            dsp.derived_series, package, PackagePublishingPocket.RELEASE,
+            dsp.parent_series)
+        jobs = find_waiting_jobs(
+            dsp.derived_series, package, dsp.parent_series)
+        self.assertEqual(
+            [package.id], [job.metadata["sourcepackagename"] for job in jobs])
 
     def test_createForPackagePublication_obeys_feature_flag(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         self.useFixture(FeatureFixture({FEATURE_FLAG_ENABLE_MODULE: ''}))
         self.getJobSource().createForPackagePublication(
-            distroseries, package, PackagePublishingPocket.RELEASE)
-        self.assertContentEqual([], find_waiting_jobs(distroseries, package))
+            dsp.derived_series, package, PackagePublishingPocket.RELEASE,
+            dsp.parent_series)
+        self.assertContentEqual(
+            [],
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series))
 
     def test_createForPackagePublication_ignores_backports_and_proposed(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         self.getJobSource().createForPackagePublication(
-            distroseries, package, PackagePublishingPocket.BACKPORTS)
+            dsp.derived_series, package, PackagePublishingPocket.BACKPORTS,
+            dsp.parent_series)
         self.getJobSource().createForPackagePublication(
-            distroseries, package, PackagePublishingPocket.PROPOSED)
-        self.assertContentEqual([], find_waiting_jobs(distroseries, package))
+            dsp.derived_series, package, PackagePublishingPocket.PROPOSED,
+            dsp.parent_series)
+        self.assertContentEqual(
+            [],
+            find_waiting_jobs(dsp.derived_series, package, dsp.parent_series))
 
     def test_cronscript(self):
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         self.getJobSource().createForPackagePublication(
-            derived_series, package, PackagePublishingPocket.RELEASE)
-        transaction.commit() # The cronscript is a different process.
+            dsp.derived_series, package, PackagePublishingPocket.RELEASE,
+            dsp.parent_series)
+        # Make changes visible to the process we'll be spawning.
+        transaction.commit()
         return_code, stdout, stderr = run_script(
             'cronscripts/distroseriesdifference_job.py', ['-v'])
         # The cronscript ran how we expected it to.
@@ -205,40 +261,30 @@
         self.assertIn(
             'INFO    Ran 1 DistroSeriesDifferenceJob jobs.', stderr)
         # And it did what we expected.
-        jobs = list(find_waiting_jobs(derived_series, package))
-        self.assertEqual(0, len(jobs))
-        store = IMasterStore(DistroSeriesDifference)
-        ds_diff = store.find(
-            DistroSeriesDifference,
-            DistroSeriesDifference.derived_series == derived_series,
-            DistroSeriesDifference.source_package_name == package)
-        self.assertEqual(1, ds_diff.count())
+        jobs = find_waiting_jobs(
+            dsp.derived_series, package, dsp.parent_series)
+        self.assertContentEqual([], jobs)
+        self.assertEqual(1, find_dsd_for(dsp, package).count())
 
     def test_job_runner_does_not_create_multiple_dsds(self):
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         job = self.getJobSource().createForPackagePublication(
-            derived_series, package, PackagePublishingPocket.RELEASE)
+            dsp.derived_series, package, PackagePublishingPocket.RELEASE,
+            dsp.parent_series)
         job[0].start()
         job[0].run()
-        job[0].job.complete() # So we can create another job.
+        # Complete the job so we can create another.
+        job[0].job.complete()
         # The first job would have created a DSD for us.
-        store = IMasterStore(DistroSeriesDifference)
-        ds_diff = store.find(
-            DistroSeriesDifference,
-            DistroSeriesDifference.derived_series == derived_series,
-            DistroSeriesDifference.source_package_name == package)
-        self.assertEqual(1, ds_diff.count())
+        self.assertEqual(1, find_dsd_for(dsp, package).count())
         # If we run the job again, it will not create another DSD.
         job = self.getJobSource().createForPackagePublication(
-            derived_series, package, PackagePublishingPocket.RELEASE)
+            dsp.derived_series, package, PackagePublishingPocket.RELEASE,
+            dsp.parent_series)
         job[0].start()
         job[0].run()
-        ds_diff = store.find(
-            DistroSeriesDifference,
-            DistroSeriesDifference.derived_series == derived_series,
-            DistroSeriesDifference.source_package_name == package)
-        self.assertEqual(1, ds_diff.count())
+        self.assertEqual(1, find_dsd_for(dsp, package).count())
 
     def test_packageset_filter_passes_inherited_packages(self):
         dsp = self.factory.makeDistroSeriesParent()
@@ -251,7 +297,7 @@
             distroseries=dsp.parent_series, sourcepackagename=package)
         self.factory.makeSourcePackagePublishingHistory(
             distroseries=dsp.derived_series, sourcepackagename=package)
-        job = create_job(dsp.derived_series, package)
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_passes_packages_unique_to_derived_series(self):
@@ -263,7 +309,7 @@
         # series.
         self.factory.makeSourcePackagePublishingHistory(
             distroseries=dsp.derived_series, sourcepackagename=package)
-        job = create_job(dsp.derived_series, package)
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_passes_all_if_parent_has_no_packagesets(self):
@@ -273,7 +319,7 @@
         package = self.factory.makeSourcePackageName()
         self.factory.makeSourcePackagePublishingHistory(
             distroseries=dsp.parent_series, sourcepackagename=package)
-        job = create_job(dsp.derived_series, package)
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertTrue(job.passesPackagesetFilter())
 
     def makeInheritedPackageSet(self, distro_series_parent, packages=()):
@@ -285,7 +331,7 @@
         parent_packageset = self.factory.makePackageset(
             distroseries=distro_series_parent.parent_series,
             packages=packages)
-        derived_packageset = self.factory.makePackageset(
+        return self.factory.makePackageset(
             distroseries=distro_series_parent.derived_series,
             packages=packages, name=parent_packageset.name,
             owner=parent_packageset.owner, related_set=parent_packageset)
@@ -300,7 +346,7 @@
         # derived series inherited.
         self.factory.makeSourcePackagePublishingHistory(
             distroseries=dsp.parent_series, sourcepackagename=package)
-        job = create_job(dsp.derived_series, package)
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_blocks_unwanted_parent_package(self):
@@ -311,7 +357,7 @@
         # between the derived series and the parent series.
         self.factory.makeSourcePackagePublishingHistory(
             distroseries=dsp.parent_series, sourcepackagename=package)
-        job = create_job(dsp.derived_series, package)
+        job = create_job(dsp.derived_series, package, dsp.parent_series)
         self.assertFalse(job.passesPackagesetFilter())
 
 
@@ -337,7 +383,8 @@
             archive = distroseries.main_archive
         changelog_lfa = self.factory.makeChangelog(
             source_package_name.name, versions)
-        transaction.commit() # Yay, librarian.
+        # Commit for the Librarian's sake.
+        transaction.commit()
         spr = self.factory.makeSourcePackageRelease(
             sourcepackagename=source_package_name, version=versions[0],
             changelog=changelog_lfa)
@@ -355,30 +402,31 @@
             source_package_name)
 
     def runJob(self, job):
-        transaction.commit() # Switching DB user performs an abort.
+        transaction.commit()
         self.layer.switchDbUser('distroseriesdifferencejob')
         dsdjob = DistroSeriesDifferenceJob(job)
         dsdjob.start()
         dsdjob.run()
         dsdjob.complete()
-        transaction.commit() # Switching DB user performs an abort.
+        transaction.commit()
         self.layer.switchDbUser('launchpad')
 
     def test_parent_gets_newer(self):
         # When a new source package is uploaded to the parent distroseries,
         # a job is created that updates the relevant DSD.
         dsp = self.makeDerivedDistroSeries()
-        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
-            source_package_name, ['1.0-1derived1', '1.0-1'], derived_series)
+            source_package_name, ['1.0-1derived1', '1.0-1'],
+            dsp.derived_series)
         self.createPublication(
             source_package_name, ['1.0-1'], dsp.parent_series)
-        # Creating the SPPHs has created jobs for us, so grab it off the
-        # queue.
-        jobs = find_waiting_jobs(derived_series, source_package_name)
+
+        # Creating the SPPHs has created jobs for us, so grab them off
+        # the queue.
+        jobs = find_waiting_jobs(dsp.derived_series, source_package_name)
         self.runJob(jobs[0])
-        ds_diff = self.findDSD(derived_series, source_package_name)
+        ds_diff = find_dsd_for(dsp, source_package_name)
         self.assertEqual(1, ds_diff.count())
         self.assertEqual('1.0-1', ds_diff[0].parent_source_version)
         self.assertEqual('1.0-1derived1', ds_diff[0].source_version)
@@ -387,7 +435,7 @@
         self.createPublication(
             source_package_name, ['1.0-2', '1.0-1'],
             dsp.parent_series)
-        jobs = find_waiting_jobs(derived_series, source_package_name)
+        jobs = find_waiting_jobs(dsp.derived_series, source_package_name)
         self.runJob(jobs[0])
         # And the DSD we have a hold of will have updated.
         self.assertEqual('1.0-2', ds_diff[0].parent_source_version)
@@ -398,23 +446,24 @@
         # When a new source is uploaded to the child distroseries, the DSD is
         # updated and auto-blacklisted.
         dsp = self.makeDerivedDistroSeries()
-        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
-            source_package_name, ['1.0-1'], derived_series)
+            source_package_name, ['1.0-1'], dsp.derived_series)
         self.createPublication(
             source_package_name, ['1.0-1'], dsp.parent_series)
-        jobs = find_waiting_jobs(derived_series, source_package_name)
+        jobs = find_waiting_jobs(dsp.derived_series, source_package_name)
         self.runJob(jobs[0])
-        ds_diff = self.findDSD(derived_series, source_package_name)
+        ds_diff = find_dsd_for(dsp, source_package_name)
         self.assertEqual(
             DistroSeriesDifferenceStatus.RESOLVED, ds_diff[0].status)
         self.createPublication(
-            source_package_name, ['2.0-0derived1', '1.0-1'], derived_series)
-        jobs = find_waiting_jobs(derived_series, source_package_name)
+            source_package_name, ['2.0-0derived1', '1.0-1'],
+            dsp.derived_series)
+        jobs = find_waiting_jobs(dsp.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):
@@ -515,13 +564,12 @@
     def test_no_job_for_PPA(self):
         # If a source package is uploaded to a PPA, a job is not created.
         dsp = self.makeDerivedDistroSeries()
-        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         ppa = self.factory.makeArchive()
         self.createPublication(
-            source_package_name, ['1.0-1'], derived_series, ppa)
-        jobs = find_waiting_jobs(derived_series, source_package_name)
-        self.assertEqual(0, jobs.count())
+            source_package_name, ['1.0-1'], dsp.derived_series, ppa)
+        self.assertContentEqual(
+            [], find_waiting_jobs(dsp.derived_series, source_package_name))
 
     def test_no_job_for_PPA_with_deleted_source(self):
         # If a source package is deleted from a PPA, no job is created.
@@ -532,8 +580,8 @@
         spph = self.createPublication(
             source_package_name, ['1.0-1'], derived_series, ppa)
         spph.requestDeletion(ppa.owner)
-        jobs = find_waiting_jobs(derived_series, source_package_name)
-        self.assertEqual(0, jobs.count())
+        self.assertContentEqual(
+            [], find_waiting_jobs(derived_series, source_package_name))
 
     def test_update_deletes_diffs(self):
         # When a DSD is updated, the diffs are invalidated.
@@ -580,8 +628,9 @@
             'queued',
             'uploader',
             ]
-        derived_series = self.factory.makeDistroSeries(
-            previous_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        parent = dsp.parent_series
+        derived = dsp.derived_series
         packages = dict(
             (user, self.factory.makeSourcePackageName())
             for user in script_users)
@@ -589,7 +638,7 @@
         for user in script_users:
             self.layer.switchDbUser(user)
             try:
-                create_job(derived_series, packages[user])
+                create_job(derived, packages[user], parent)
             except ProgrammingError, e:
                 self.assertTrue(
                     False,