← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/bug-747546 into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-747546 into lp:launchpad with lp:~jtv/launchpad/pre-747546 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #747546 in Launchpad itself: "Derived Distros: Add a button to sync all packages changed in parent but not changed in child"
  https://bugs.launchpad.net/launchpad/+bug/747546

For more details, see:
https://code.launchpad.net/~jtv/launchpad/bug-747546/+merge/59765

Work in progress.  Please update this MP when the time comes.

This branch adds a button to the +localpackagediffs page that syncs all updates into a derived series for packages that have not been changed in the derived series.
-- 
https://code.launchpad.net/~jtv/launchpad/bug-747546/+merge/59765
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-747546 into lp:launchpad.
=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-04-26 14:40:00 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-05-03 12:15:18 +0000
@@ -12,7 +12,6 @@
     'DistroSeriesEditView',
     'DistroSeriesFacets',
     'DistroSeriesInitializeView',
-    'DistroSeriesLocalDifferences',
     'DistroSeriesNavigation',
     'DistroSeriesPackageSearchView',
     'DistroSeriesPackagesView',
@@ -101,12 +100,21 @@
 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 IPackageCopyJobSource
 from lp.soyuz.interfaces.queue import IPackageUploadSet
 from lp.translations.browser.distroseries import (
     check_distroseries_translations_viewable,
     )
 
 
+# DistroSeries statuses that benefit from mass package upgrade support.
+UPGRADEABLE_SERIESSTATUSES = [
+    SeriesStatus.FUTURE,
+    SeriesStatus.EXPERIMENTAL,
+    SeriesStatus.DEVELOPMENT,
+    ]
+
+
 class DistroSeriesNavigation(GetitemNavigation, BugTargetTraversalMixin,
     StructuralSubscriptionTargetTraversalMixin):
 
@@ -925,6 +933,47 @@
     def sync_sources(self, action, data):
         self._sync_sources(action, data)
 
+    def getUpgrades(self):
+        """Find straightforward package upgrades.
+
+        These are updates for packages that this distroseries shares
+        with a parent series, for which there have been updates in the
+        parent, and which do not have any changes in this series that
+        might complicate a sync.
+
+        :return: A result set of `DistroSeriesDifference`s.
+        """
+        return getUtility(IDistroSeriesDifferenceSource).getSimpleUpgrades(
+            self.context)
+
+    @action(_("Upgrade Packages"), name="upgrade",
+            condition='canUpgrade')
+    def upgrade(self, action, data):
+        """Request synchronization of straightforward package upgrades."""
+        self.requestUpgrades()
+
+    def requestUpgrades(self):
+        """Request sync of packages that can be easily upgraded."""
+        job_source = getUtility(IPackageCopyJobSource)
+# XXX: Create jobs.
+        self.request.response.addInfoNotification("""
+            Upgrades of %s packages have been requested.
+            Please give Launchpad some time to complete these.
+            """ % self.context.displayname)
+
+    def canUpgrade(self):
+        """Should the form offer a packages upgrade?"""
+        if self.context.status not in UPGRADEABLE_SERIESSTATUSES:
+            # A feature freeze precludes blanket updates.
+            return False
+# XXX: Check privilege.  We don't know who should be allowed to do this
+# yet.
+        elif self.getUpgrades().is_empty():
+            # There are no simple updates to perform.
+            return False
+        else:
+            return True
+
 
 class DistroSeriesMissingPackagesView(DistroSeriesDifferenceBaseView,
                                       LaunchpadFormView):

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-04-27 07:27:55 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-05-03 12:15:18 +0000
@@ -47,6 +47,7 @@
     DistroSeriesDifferenceStatus,
     DistroSeriesDifferenceType,
     )
+from lp.registry.interfaces.series import SeriesStatus
 from lp.services.features import (
     get_relevant_feature_controller,
     getFeatureFlag,
@@ -57,6 +58,7 @@
     FeatureFlag,
     getFeatureStore,
     )
+from lp.soyuz.interfaces.distributionjob import IPackageCopyJobSource
 from lp.soyuz.enums import (
     PackagePublishingStatus,
     SourcePackageFormat,
@@ -173,11 +175,11 @@
                 derived_series,
                 '+index',
                 principal=self.simple_user)
-            html = view()
+            html_text = view()
 
         self.assertEqual(
             None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
-        self.assertThat(html, Not(portlet_header))
+        self.assertThat(html_text, Not(portlet_header))
 
     def test_differences_portlet_all_differences(self):
         # The difference portlet shows the differences with the parent
@@ -209,9 +211,9 @@
             # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
             # self.features to NullFeatureController.
             view.request.features = get_relevant_feature_controller()
-            html = view()
+            html_text = view()
 
-        self.assertThat(html, portlet_display)
+        self.assertThat(html_text, portlet_display)
 
     def test_differences_portlet_no_differences(self):
         # The difference portlet displays 'No differences' if there is no
@@ -235,9 +237,9 @@
             # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
             # self.features to NullFeatureController.
             view.request.features = get_relevant_feature_controller()
-            html = view()
+            html_text = view()
 
-        self.assertThat(html, portlet_display)
+        self.assertThat(html_text, portlet_display)
 
     def test_differences_portlet_initialising(self):
         # The difference portlet displays 'The series is initialising.' if
@@ -263,10 +265,10 @@
             # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
             # self.features to NullFeatureController.
             view.request.features = get_relevant_feature_controller()
-            html = view()
+            html_text = view()
 
         self.assertTrue(derived_series.is_initialising)
-        self.assertThat(html, portlet_display)
+        self.assertThat(html_text, portlet_display)
 
 
 class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory):
@@ -401,7 +403,7 @@
 class DistroSeriesDifferenceMixin:
     """A helper class for testing differences pages"""
 
-    def _test_packagesets(self, html, packageset_text,
+    def _test_packagesets(self, html_text, packageset_text,
                           packageset_class, msg_text):
         parent_packagesets = soupmatchers.HTMLContains(
             soupmatchers.Tag(
@@ -409,7 +411,7 @@
                 attrs={'class': packageset_class},
                 text=packageset_text))
 
-        self.assertThat(html, parent_packagesets)
+        self.assertThat(html_text, parent_packagesets)
 
 
 class TestDistroSeriesLocalDifferences(
@@ -471,11 +473,12 @@
                 ds_diff.derived_series,
                 '+localpackagediffs',
                 principal=self.simple_user)
-            html = view()
+            html_text = view()
 
         packageset_text = re.compile('\s*' + ps.name)
         self._test_packagesets(
-            html, packageset_text, 'parent-packagesets', 'Parent packagesets')
+            html_text, packageset_text, 'parent-packagesets',
+            'Parent packagesets')
 
     def test_parent_packagesets_localpackagediffs_sorts(self):
         # Multiple packagesets are sorted in a comma separated list.
@@ -493,12 +496,13 @@
                 ds_diff.derived_series,
                 '+localpackagediffs',
                 principal=self.simple_user)
-            html = view()
+            html_text = view()
 
         packageset_text = re.compile(
             '\s*' + ', '.join(sorted(unsorted_names)))
         self._test_packagesets(
-            html, packageset_text, 'parent-packagesets', 'Parent packagesets')
+            html_text, packageset_text, 'parent-packagesets',
+            'Parent packagesets')
 
     def test_queries(self):
         # With no DistroSeriesDifferences the query count should be low and
@@ -616,6 +620,28 @@
 
     layer = LaunchpadZopelessLayer
 
+    def makePackageUpgrade(self):
+        """Create a `DistroSeriesDifference` for a package upgrade."""
+        base_version = '1.%d' % self.factory.getUniqueInteger()
+        versions = {
+            'base': base_version,
+            'parent': base_version + '-' + self.factory.getUniqueString(),
+            'derived': base_version,
+        }
+        return self.factory.makeDistroSeriesDifference(
+            versions=versions, set_base_version=True)
+
+    def makeDerivedSeries(self):
+        """Create a derived `DistroSeries`."""
+        return self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+
+    def makeView(self, distroseries=None):
+        """Create a +localpackagediffs view for `distroseries`."""
+        if distroseries is None:
+            distroseries = self.makeDerivedSeries()
+        return create_initialized_view(distroseries, '+localpackagediffs')
+
     def test_view_redirects_without_feature_flag(self):
         # If the feature flag soyuz.derived-series-ui.enabled is not set the
         # view simply redirects to the derived series.
@@ -625,8 +651,7 @@
 
         self.assertIs(
             None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         response = view.request.response
         self.assertEqual(302, response.getStatus())
@@ -639,8 +664,7 @@
             name='derilucid', parent_series=self.factory.makeDistroSeries(
                 name='lucid'))
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertEqual(
             "Source package differences between 'Derilucid' and "
@@ -659,8 +683,7 @@
             derived_series=derived_series,
             status=DistroSeriesDifferenceStatus.RESOLVED)
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertContentEqual(
             [current_difference], view.cached_differences.batch)
@@ -677,8 +700,7 @@
             difference_type=(
                 DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES))
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertContentEqual(
             [different_versions_diff], view.cached_differences.batch)
@@ -690,8 +712,7 @@
                 name='lucid'))
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         soup = BeautifulSoup(view())
         help_links = soup.findAll(
@@ -709,8 +730,7 @@
         difference.addComment(difference.owner, "Latest comment")
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         # Find all the rows within the body of the table
         # listing the differences.
@@ -731,8 +751,7 @@
             derived_series=derived_series)
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.findAll('tr')[0]
@@ -770,8 +789,7 @@
             version=new_version)
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.tr
@@ -814,8 +832,7 @@
         flush_database_caches()
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.tr
@@ -832,6 +849,89 @@
         self.assertEqual(versions['derived'], derived_span[0].string.strip())
         self.assertEqual(versions['parent'], parent_span[0].string.strip())
 
+    def test_getUpgrades_shows_updates_in_parent(self):
+        # The view's getUpgrades methods lists packages that can be
+        # trivially upgraded: changed in the parent, not changed in the
+        # derived series, but present in both.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        self.assertContentEqual([dsd], view.getUpgrades())
+
+    def test_upgrades_are_offered_if_appropriate(self):
+        # canUpgrade is the condition for the form showing an "Upgrade
+        # Packages" button.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        self.assertTrue(view.canUpgrade())
+
+    def test_upgrades_offered_only_if_available(self):
+        # If there are no upgrades, the "Upgrade Packages" button won't
+        # be shown.
+        view = self.makeView()
+        self.assertFalse(view.canUpgrade())
+
+    def test_upgrades_not_offered_after_feature_freeze(self):
+        # There won't be an "Upgrade Packages" button once feature
+        # freeze has occurred.  Mass updates would not make sense after
+        # that point.
+        upgradeable = {}
+        for status in SeriesStatus.items:
+            dsd = self.makePackageUpgrade()
+            dsd.derived_series.status = status
+            view = self.makeView(dsd.derived_series)
+            upgradeable[status] = view.canUpgrade()
+        expected = {
+            SeriesStatus.FUTURE: True,
+            SeriesStatus.EXPERIMENTAL: True,
+            SeriesStatus.DEVELOPMENT: True,
+            SeriesStatus.FROZEN: False,
+            SeriesStatus.CURRENT: False,
+            SeriesStatus.SUPPORTED: False,
+            SeriesStatus.OBSOLETE: False,
+        }
+        self.assertEqual(expected, upgradeable)
+
+    def test_upgrade_creates_sync_jobs(self):
+        # requestUpgrades generates PackageCopyJobs for the upgrades
+        # that need doing.
+        dsd = self.makePackageUpgrade()
+        series = dsd.derived_series
+        view = self.makeView(series)
+        view.requestUpgrades()
+        job_source = getUtility(IPackageCopyJobSource)
+        jobs = list(
+            job_source.getActiveJobs(series.distribution.main_archive))
+        self.assertEquals(1, len(jobs))
+        job = jobs[0]
+        self.assertEquals(series, job.distroseries)
+        spphs = job.source_packages
+        self.assertEquals(1, len(spphs))
+        self.assertEqual(dsd.source_package_name, spphs[0].sourcepackagename)
+
+    def test_upgrade_gives_feedback(self):
+        # requestUpgrades doesn't instantly perform package upgrades,
+        # but it shows the user a notice that the upgrades have been
+        # requested.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        view.requestUpgrades()
+# XXX: Test.
+        self.assertTrue(False)
+
+    def test_upgrade_is_privileged(self):
+# XXX: Privileged to whom?  For Ubuntu this would be the ubuntu-archive
+# team, ideally, but we don't know of any formal role that that team has
+# w.r.t. Ubuntu in Launchpad.
+# XXX: Test.
+        self.assertTrue(False)
+
+    def test_requestUpgrade_is_efficient(self):
+        # A single web request may need to schedule large numbers of
+        # package upgrades.  It must do so without issuing large numbers
+        # of database queries.
+# XXX: Test.
+        self.assertTrue(False)
+
 
 class TestDistroSeriesLocalDifferencesFunctional(TestCaseWithFactory):
 
@@ -1228,11 +1328,12 @@
                 self.ds_diff.derived_series,
                 '+missingpackages',
                 principal=self.simple_user)
-            html = view()
+            html_text = view()
 
         packageset_text = re.compile('\s*' + ps.name)
         self._test_packagesets(
-            html, packageset_text, 'parent-packagesets', 'Parent packagesets')
+            html_text, packageset_text, 'parent-packagesets',
+            'Parent packagesets')
 
 
 class DistroSerieUniquePackageDiffsTestCase(TestCaseWithFactory):
@@ -1314,8 +1415,8 @@
                 self.ds_diff.derived_series,
                 '+uniquepackages',
                 principal=self.simple_user)
-            html = view()
+            html_text = view()
 
         packageset_text = re.compile('\s*' + ps.name)
         self._test_packagesets(
-            html, packageset_text, 'packagesets', 'Packagesets')
+            html_text, packageset_text, 'packagesets', 'Packagesets')

=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
--- lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-19 02:57:38 +0000
+++ lib/lp/registry/interfaces/distroseriesdifference.py	2011-05-03 12:15:18 +0000
@@ -303,3 +303,12 @@
         :param source_package_name: The name of the package difference.
         :type source_package_name: unicode.
         """
+
+    def getSimpleUpgrades(distro_series):
+        """Find pending upgrades that can be performed mindlessly.
+
+        These are `DistroSeriesDifferences` where the parent has been
+        updated and the child still has the old version, unchanged.
+
+        Blacklisted items are excluded.
+        """

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-04-22 00:58:35 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-05-03 12:15:18 +0000
@@ -277,29 +277,26 @@
             And(*conditions)).order_by(SourcePackageName.name)
 
         def eager_load(dsds):
+            active_statuses = (
+                PackagePublishingStatus.PUBLISHED,
+                PackagePublishingStatus.PENDING,
+                )
             source_pubs = dict(
                 most_recent_publications(
-                    dsds, in_parent=False, statuses=(
-                        PackagePublishingStatus.PUBLISHED,
-                        PackagePublishingStatus.PENDING)))
+                    dsds, statuses=active_statuses,
+                    in_parent=False, match_version=False))
             parent_source_pubs = dict(
                 most_recent_publications(
-                    dsds, in_parent=True, statuses=(
-                        PackagePublishingStatus.PUBLISHED,
-                        PackagePublishingStatus.PENDING)))
-
+                    dsds, statuses=active_statuses,
+                    in_parent=True, match_version=False))
             source_pubs_for_release = dict(
                 most_recent_publications(
-                    dsds, in_parent=False, statuses=(
-                        PackagePublishingStatus.PUBLISHED,
-                        PackagePublishingStatus.PENDING),
-                    match_version=True))
+                    dsds, statuses=active_statuses,
+                    in_parent=False, match_version=True))
             parent_source_pubs_for_release = dict(
                 most_recent_publications(
-                    dsds, in_parent=True, statuses=(
-                        PackagePublishingStatus.PUBLISHED,
-                        PackagePublishingStatus.PENDING),
-                    match_version=True))
+                    dsds, statuses=active_statuses,
+                    in_parent=True, match_version=True))
 
             latest_comment_by_dsd_id = dict(
                 (comment.distro_series_difference_id, comment)
@@ -376,6 +373,23 @@
                 SourcePackageName.id),
             SourcePackageName.name == source_package_name).one()
 
+    @staticmethod
+    def getSimpleUpgrades(distro_series):
+        """See `IDistroSeriesDifferenceSource`."""
+        return IStore(DistroSeriesDifference).find(
+            DistroSeriesDifference,
+            SourcePackageName.id ==
+                DistroSeriesDifference.source_package_name_id,
+            DistroSeriesDifference.derived_series == distro_series,
+            DistroSeriesDifference.difference_type ==
+                DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
+            DistroSeriesDifference.status ==
+                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
+            DistroSeriesDifference.parent_source_version !=
+                DistroSeriesDifference.base_version,
+            DistroSeriesDifference.source_version ==
+                DistroSeriesDifference.base_version)
+
     @cachedproperty
     def source_pub(self):
         """See `IDistroSeriesDifference`."""

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-21 16:11:51 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2011-05-03 12:15:18 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Model tests for the DistroSeriesDifference class."""
@@ -903,6 +903,36 @@
         return self.factory.makeDistroSeries(
             parent_series=self.factory.makeDistroSeries())
 
+    def makeVersionDifference(self, derived_series=None, changed_parent=False,
+                              changed_child=False, status=None):
+        """Create a `DistroSeriesDifference` between package versions.
+
+        The differing package will exist in both the parent series and in the
+        child.
+
+        :param derived_series: Optional `DistroSeries` that the difference is
+            for.  If not given, one will be created.
+        :param changed_parent: Whether the difference should show a change in
+            the parent's version of the package.
+        :param changed_child: Whether the difference should show a change in
+            the child's version of the package.
+        :param status: Optional status for the `DistroSeriesDifference`.  If
+            not given, defaults to `NEEDS_ATTENTION`.
+        """
+        if status is None:
+            status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
+        base_version = "1.%d" % self.factory.getUniqueInteger()
+        versions = dict(
+            (key, base_version)
+            for key in ['base', 'parent', 'derived'])
+        if changed_parent:
+            versions['parent'] += "-%s" % self.factory.getUniqueString()
+        if changed_child:
+            versions['derived'] += "-%s" % self.factory.getUniqueString()
+        return self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series, versions=versions, status=status,
+            set_base_version=True)
+
     def test_getForDistroSeries_default(self):
         # By default all differences needing attention for the given
         # series are returned.
@@ -987,6 +1017,61 @@
 
         self.assertEqual(ds_diff, result)
 
+    def test_getSimpleUpdates_finds_simple_update(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(changed_parent=True)
+        self.assertEqual(dsd.base_version, dsd.source_version)
+        self.assertContentEqual(
+            [dsd], dsd_source.getSimpleUpdates(dsd.derived_series))
+
+    def test_getSimpleUpdates_ignores_hidden_differences(self):
+        invisible_statuses = [
+            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
+            DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
+            DistroSeriesDifferenceStatus.RESOLVED,
+            ]
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        series = self.makeDerivedSeries()
+        for status in invisible_statuses:
+            self.makeVersionDifference(
+                derived_series=series, changed_parent=True, status=status)
+        self.assertContentEqual([], dsd_source.getSimpleUpdates(series))
+
+    def test_getSimpleUpdates_ignores_other_distroseries(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        self.makeVersionDifference(changed_parent=True)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpdates(self.factory.makeDistroSeries()))
+
+    def test_getSimpleUpdates_ignores_packages_changed_in_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(
+            changed_parent=True, changed_child=True)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpdates(dsd.derived_series))
+
+    def test_getSimpleUpdates_ignores_packages_not_updated_in_parent(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(changed_parent=False)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpdates(dsd.derived_series))
+
+    def test_getSimpleUpdates_ignores_packages_unique_to_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        diff_type = DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES
+        dsd = self.factory.makeDistroSeriesDifference(
+            difference_type=diff_type)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpdates(dsd.derived_series))
+
+    def test_getSimpleUpdates_ignores_packages_missing_from_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        diff_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+        dsd = self.factory.makeDistroSeriesDifference(
+            difference_type=diff_type)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpdates(dsd.derived_series))
+
 
 class TestMostRecentComments(TestCaseWithFactory):
 


Follow ups