← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-829074 into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-829074 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-829074/+merge/91302

This branch is a step to fix bug 829074: Show bugs that are
not known to affect "official" upstream.

Bryce suggests in a comment on the bug to optionally limit the
search to bugtasks targeted to the upstream product.

Daniel points out in another comment that bug 232545 describes
a very similar problem.

On the model side, we can fix both bugs if we allow to optionally
specify an upstream bug target; the search should then return bugs
that have (or do not have) a bug task with the "right properties"
for this target.

An oddity of upstream related searches is that a search with the
parameter pending_bugwatch_elsewhere considers distributions
as a possible upstream target, while the other upstream searches
consider only products as upstream targets.

While I don't see much value in using an entire distribtuion
as the new optional upstream target, I added this option
nevertheless for all upstream related searches, just for the sake
of consistency. (To address bug 232545 completely, I'll add the
targets ISourcePackage and IDistributionSourcePackage in a later
branch. This should make work easier if a Debian package is the
the upstream of an Ubuntu package; it might also help for the new
derived distributions.)

Implementation:

A new property BugTaskSearchParams.upstream_target. This property
  is only relevant if at least one of
  BugTaskSearchParams.pending_bugwatch_elsewhere,
  BugTaskSearchParams.has_no_upstream_bugtask,
  BugTaskSearchParams.resolved_usptream or
  BugTaskSearchParams.open_upstream is specified.

If BugTaskSearchParams.upstream_target is specified, the WHERE
clause returned by BugTaskSet.buildUpstreamClause() contains an
expression that limits the search to bugtasks for this target.

The file lp/bugs/tests/test_bugtask_search.py allows to write a
single test that is run for all possible bug targets.

A proper setup of an upstream related test can be a bit convoluted
-- see for example the already existing method
test_has_no_upstream_bugtask(). To make life a bit easier, I
limited the new tests to the targets SourcePackage and
DistributionSourcePackage: Searching for bugs on other targets
with an upstream filter together with a specific upstream target
is probably pointless, and I intend to add option "limit the
upstream search to upstream target X" to source packages and
DSPs as the main search target.

tests:

./bin/test bugs -vvt lp.bugs.tests.test_bugtask_search.*test_resolved_upstream
./bin/test bugs -vvt lp.bugs.tests.test_bugtask_search.*test_open_upstream
./bin/test bugs -vvt lp.bugs.tests.test_bugtask_search.*test_has_no_upstream_bugtask
./bin/test bugs -vvt lp.bugs.tests.test_bugtask_search.*test_pending_bugwatch_elsewhere

-- 
https://code.launchpad.net/~adeuring/launchpad/bug-829074/+merge/91302
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-829074 into lp:launchpad.
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2012-01-10 14:24:19 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2012-02-02 16:31:40 +0000
@@ -1204,7 +1204,8 @@
                  hardware_is_linked_to_bug=False,
                  linked_branches=None, linked_blueprints=None,
                  structural_subscriber=None, modified_since=None,
-                 created_since=None, exclude_conjoined_tasks=False, cve=None):
+                 created_since=None, exclude_conjoined_tasks=False, cve=None,
+                 upstream_target=None):
 
         self.bug = bug
         self.searchtext = searchtext
@@ -1254,6 +1255,7 @@
         self.created_since = created_since
         self.exclude_conjoined_tasks = exclude_conjoined_tasks
         self.cve = cve
+        self.upstream_target = upstream_target
 
     def setProduct(self, product):
         """Set the upstream context on which to filter the search."""

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2012-01-09 13:25:03 +0000
+++ lib/lp/bugs/model/bugtask.py	2012-02-02 16:31:40 +0000
@@ -1742,19 +1742,35 @@
     _ORDERBY_COLUMN = None
 
     _open_resolved_upstream = """
-                EXISTS (
-                    SELECT TRUE FROM BugTask AS RelatedBugTask
-                    WHERE RelatedBugTask.bug = BugTask.bug
-                        AND RelatedBugTask.id != BugTask.id
-                        AND ((
-                            RelatedBugTask.bugwatch IS NOT NULL AND
-                            RelatedBugTask.status %s)
-                            OR (
-                            RelatedBugTask.product IS NOT NULL AND
-                            RelatedBugTask.bugwatch IS NULL AND
-                            RelatedBugTask.status %s))
-                    )
-                """
+        EXISTS (
+            SELECT TRUE FROM BugTask AS RelatedBugTask
+            WHERE RelatedBugTask.bug = BugTask.bug
+                AND RelatedBugTask.id != BugTask.id
+                AND ((
+                    RelatedBugTask.bugwatch IS NOT NULL AND
+                    RelatedBugTask.status %s)
+                    OR (
+                    RelatedBugTask.product IS NOT NULL AND
+                    RelatedBugTask.bugwatch IS NULL AND
+                    RelatedBugTask.status %s))
+            )
+        """
+
+    _open_resolved_upstream_with_target = """
+        EXISTS (
+            SELECT TRUE FROM BugTask AS RelatedBugTask
+            WHERE RelatedBugTask.bug = BugTask.bug
+                AND RelatedBugTask.id != BugTask.id
+                AND ((
+                    RelatedBugTask.%(target_column)s = %(target_id)s AND
+                    RelatedBugTask.bugwatch IS NOT NULL AND
+                    RelatedBugTask.status %(status_with_watch)s)
+                    OR (
+                    RelatedBugTask.%(target_column)s = %(target_id)s AND
+                    RelatedBugTask.bugwatch IS NULL AND
+                    RelatedBugTask.status %(status_without_watch)s))
+            )
+        """
 
     title = "A set of bug tasks"
 
@@ -2411,66 +2427,147 @@
             query, clauseTables, decorator, join_tables,
             has_duplicate_results, with_clause)
 
-    def buildUpstreamClause(self, params):
-        """Return an clause for returning upstream data if the data exists.
-
-        This method will handles BugTasks that do not have upstream BugTasks
-        as well as thoses that do.
+    def buildPendingBugwatchElsewhereClause(self, params):
+        """Return a clause for BugTaskSearchParams.pending_bugwatch_elsewhere
         """
-        params = self._require_params(params)
-        upstream_clauses = []
-        if params.pending_bugwatch_elsewhere:
-            if params.product:
-                # Include only bugtasks that do no have bug watches that
-                # belong to a product that does not use Malone.
-                pending_bugwatch_elsewhere_clause = """
-                    EXISTS (
-                        SELECT TRUE
-                        FROM BugTask AS RelatedBugTask
-                            LEFT OUTER JOIN Product AS OtherProduct
-                                ON RelatedBugTask.product = OtherProduct.id
-                        WHERE RelatedBugTask.bug = BugTask.bug
-                            AND RelatedBugTask.id = BugTask.id
-                            AND RelatedBugTask.bugwatch IS NULL
-                            AND OtherProduct.official_malone IS FALSE
-                            AND RelatedBugTask.status != %s)
-                    """ % sqlvalues(BugTaskStatus.INVALID)
+        if params.product:
+            # Include only bugtasks that do no have bug watches that
+            # belong to a product that does not use Malone.
+            return """
+                EXISTS (
+                    SELECT TRUE
+                    FROM BugTask AS RelatedBugTask
+                        LEFT OUTER JOIN Product AS OtherProduct
+                            ON RelatedBugTask.product = OtherProduct.id
+                    WHERE RelatedBugTask.bug = BugTask.bug
+                        AND RelatedBugTask.id = BugTask.id
+                        AND RelatedBugTask.bugwatch IS NULL
+                        AND OtherProduct.official_malone IS FALSE
+                        AND RelatedBugTask.status != %s)
+                """ % sqlvalues(BugTaskStatus.INVALID)
+        elif params.upstream_target is None:
+            # Include only bugtasks that have other bugtasks on targets
+            # not using Malone, which are not Invalid, and have no bug
+            # watch.
+            return """
+                EXISTS (
+                    SELECT TRUE
+                    FROM BugTask AS RelatedBugTask
+                        LEFT OUTER JOIN Distribution AS OtherDistribution
+                            ON RelatedBugTask.distribution =
+                                OtherDistribution.id
+                        LEFT OUTER JOIN Product AS OtherProduct
+                            ON RelatedBugTask.product = OtherProduct.id
+                    WHERE RelatedBugTask.bug = BugTask.bug
+                        AND RelatedBugTask.id != BugTask.id
+                        AND RelatedBugTask.bugwatch IS NULL
+                        AND (
+                            OtherDistribution.official_malone IS FALSE
+                            OR OtherProduct.official_malone IS FALSE)
+                        AND RelatedBugTask.status != %s)
+                """ % sqlvalues(BugTaskStatus.INVALID)
+        else:
+            # Include only bugtasks that have other bugtasks on
+            # params.upstream_target, but only if this this product
+            # does not use Malone and if the bugtasks are not Invalid,
+            # and have no bug watch.
+            if IProduct.providedBy(params.upstream_target):
+                target_clause = 'RelatedBugTask.product = %s'
+            elif IDistribution.providedBy(params.upstream_target):
+                target_clause = 'RelatedBugTask.distribution = %s'
             else:
-                # Include only bugtasks that have other bugtasks on targets
-                # not using Malone, which are not Invalid, and have no bug
-                # watch.
-                pending_bugwatch_elsewhere_clause = """
-                    EXISTS (
-                        SELECT TRUE
-                        FROM BugTask AS RelatedBugTask
-                            LEFT OUTER JOIN Distribution AS OtherDistribution
-                                ON RelatedBugTask.distribution =
-                                    OtherDistribution.id
-                            LEFT OUTER JOIN Product AS OtherProduct
-                                ON RelatedBugTask.product = OtherProduct.id
-                        WHERE RelatedBugTask.bug = BugTask.bug
-                            AND RelatedBugTask.id != BugTask.id
-                            AND RelatedBugTask.bugwatch IS NULL
-                            AND (
-                                OtherDistribution.official_malone IS FALSE
-                                OR OtherProduct.official_malone IS FALSE)
-                            AND RelatedBugTask.status != %s)
-                    """ % sqlvalues(BugTaskStatus.INVALID)
-
-            upstream_clauses.append(pending_bugwatch_elsewhere_clause)
-
-        if params.has_no_upstream_bugtask:
+                raise AssertionError(
+                    'params.upstream_target must be a Distribution or '
+                    'a Product')
+            # There is no point to construct a real sub-select if we
+            # already know that the result will be empty.
+            if params.upstream_target.official_malone:
+                return 'false'
+            target_clause = target_clause % sqlvalues(
+                params.upstream_target.id)
+            return """
+                EXISTS (
+                    SELECT TRUE
+                    FROM BugTask AS RelatedBugTask
+                    WHERE RelatedBugTask.bug = BugTask.bug
+                        AND RelatedBugTask.id != BugTask.id
+                        AND RelatedBugTask.bugwatch IS NULL
+                        AND %s
+                        AND RelatedBugTask.status != %s)
+                """ % (target_clause, sqlvalues(BugTaskStatus.INVALID)[0])
+
+    def buildNoUpstreamBugtaskClause(self, params):
+        """Return a clause for BugTaskSearchParams.has_no_upstream_bugtask."""
+        if params.upstream_target is None:
             # Find all bugs that has no product bugtask. We limit the
             # SELECT by matching against BugTask.bug to make the query
             # faster.
-            has_no_upstream_bugtask_clause = """
+            return """
                 NOT EXISTS (SELECT TRUE
                             FROM BugTask AS OtherBugTask
                             WHERE OtherBugTask.bug = BugTask.bug
                                 AND OtherBugTask.product IS NOT NULL)
             """
-            upstream_clauses.append(has_no_upstream_bugtask_clause)
-
+        elif IProduct.providedBy(params.upstream_target):
+            return """
+                NOT EXISTS (SELECT TRUE
+                            FROM BugTask AS OtherBugTask
+                            WHERE OtherBugTask.bug = BugTask.bug
+                                AND OtherBugTask.product=%s)
+            """ % sqlvalues(params.upstream_target.id)
+        elif IDistribution.providedBy(params.upstream_target):
+            return """
+                NOT EXISTS (SELECT TRUE
+                            FROM BugTask AS OtherBugTask
+                            WHERE OtherBugTask.bug = BugTask.bug
+                                AND OtherBugTask.distribution=%s)
+            """ % sqlvalues(params.upstream_target.id)
+        else:
+            raise AssertionError(
+                'params.upstream_target must be a Distribution or '
+                'a Product')
+
+    def buildOpenOrResolvedUpstreamClause(self, params,
+                                          statuses_for_watch_tasks,
+                                          statuses_for_upstream_tasks):
+        """Return a clause for BugTaskSearchParams.open_upstream or
+        BugTaskSearchParams.resolved_upstream."""
+        if params.upstream_target is None:
+            return self._open_resolved_upstream % (
+                    search_value_to_where_condition(
+                        any(*statuses_for_watch_tasks)),
+                    search_value_to_where_condition(
+                        any(*statuses_for_upstream_tasks)))
+        elif IProduct.providedBy(params.upstream_target):
+            query_values = {'target_column': 'product'}
+        elif IDistribution.providedBy(params.upstream_target):
+            query_values = {'target_column': 'distribution'}
+        else:
+            raise AssertionError(
+                'params.upstream_target must be a Distribution or '
+                'a Product')
+        query_values['target_id'] = sqlvalues(params.upstream_target.id)[0]
+        query_values['status_with_watch'] = search_value_to_where_condition(
+            any(*statuses_for_watch_tasks))
+        query_values['status_without_watch'] = search_value_to_where_condition(
+            any(*statuses_for_upstream_tasks))
+        return self._open_resolved_upstream_with_target % query_values
+
+    def buildOpenUpstreamClause(self, params):
+        """Return a clause for BugTaskSearchParams.open_upstream."""
+        statuses_for_open_tasks = [
+            BugTaskStatus.NEW,
+            BugTaskStatus.INCOMPLETE,
+            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
+            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
+            BugTaskStatus.CONFIRMED,
+            BugTaskStatus.INPROGRESS,
+            BugTaskStatus.UNKNOWN]
+        return self.buildOpenOrResolvedUpstreamClause(
+            params, statuses_for_open_tasks, statuses_for_open_tasks)
+
+    def buildResolvedUpstreamClause(self, params):
+        """Return a clause for BugTaskSearchParams.open_upstream."""
         # Our definition of "resolved upstream" means:
         #
         # * bugs with bugtasks linked to watches that are invalid,
@@ -2481,36 +2578,34 @@
         # This definition of "resolved upstream" should address the use
         # cases we gathered at UDS Paris (and followup discussions with
         # seb128, sfllaw, et al.)
+        statuses_for_watch_tasks = [
+            BugTaskStatus.INVALID,
+            BugTaskStatus.FIXCOMMITTED,
+            BugTaskStatus.FIXRELEASED]
+        statuses_for_upstream_tasks = [
+            BugTaskStatus.FIXCOMMITTED,
+            BugTaskStatus.FIXRELEASED]
+        return self.buildOpenOrResolvedUpstreamClause(
+            params, statuses_for_watch_tasks, statuses_for_upstream_tasks)
+
+    def buildUpstreamClause(self, params):
+        """Return an clause for returning upstream data if the data exists.
+
+        This method will handles BugTasks that do not have upstream BugTasks
+        as well as thoses that do.
+        """
+        params = self._require_params(params)
+        upstream_clauses = []
+        if params.pending_bugwatch_elsewhere:
+            upstream_clauses.append(
+                self.buildPendingBugwatchElsewhereClause(params))
+        if params.has_no_upstream_bugtask:
+            upstream_clauses.append(
+                self.buildNoUpstreamBugtaskClause(params))
         if params.resolved_upstream:
-            statuses_for_watch_tasks = [
-                BugTaskStatus.INVALID,
-                BugTaskStatus.FIXCOMMITTED,
-                BugTaskStatus.FIXRELEASED]
-            statuses_for_upstream_tasks = [
-                BugTaskStatus.FIXCOMMITTED,
-                BugTaskStatus.FIXRELEASED]
-
-            only_resolved_upstream_clause = self._open_resolved_upstream % (
-                    search_value_to_where_condition(
-                        any(*statuses_for_watch_tasks)),
-                    search_value_to_where_condition(
-                        any(*statuses_for_upstream_tasks)))
-            upstream_clauses.append(only_resolved_upstream_clause)
+            upstream_clauses.append(self.buildResolvedUpstreamClause(params))
         if params.open_upstream:
-            statuses_for_open_tasks = [
-                BugTaskStatus.NEW,
-                BugTaskStatus.INCOMPLETE,
-                BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
-                BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
-                BugTaskStatus.CONFIRMED,
-                BugTaskStatus.INPROGRESS,
-                BugTaskStatus.UNKNOWN]
-            only_open_upstream_clause = self._open_resolved_upstream % (
-                    search_value_to_where_condition(
-                        any(*statuses_for_open_tasks)),
-                    search_value_to_where_condition(
-                        any(*statuses_for_open_tasks)))
-            upstream_clauses.append(only_open_upstream_clause)
+            upstream_clauses.append(self.buildOpenUpstreamClause(params))
 
         if upstream_clauses:
             upstream_clause = " OR ".join(upstream_clauses)

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2012-01-03 10:57:01 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2012-02-02 16:31:40 +0000
@@ -1228,7 +1228,221 @@
         self.assertSearchFinds(params, [self.bugtasks[-1]])
 
 
-class SourcePackageTarget(BugTargetTestBase):
+class UpstreamFilterTests:
+    """Tests related to restircted upstream filtering.
+
+    These tests make sense only for the targets SourcePackage
+    DistributionSourcePackage.
+    """
+
+    def setUpUpstreamTests(self, upstream_target):
+        # The default test bugs have two tasks for DistributionSourcePackage
+        # tests: one task for the DSP and another task for a product;
+        # they have three tasks for SourcePackage tests: for a product,
+        # for a DSP and for a sourcepackage.
+        # Tests in this class are about searching bug tasks, where the
+        # bug has a task for any upstream target or for a given upstream
+        # target and where the bug task for the upstream target has certain
+        # properties.
+        with person_logged_in(self.searchtarget.distribution.owner):
+            self.searchtarget.distribution.official_malone = True
+        for existing_task in self.bugtasks:
+            bug = existing_task.bug
+            self.factory.makeBugTask(bug, target=upstream_target)
+
+    def addWatch(self, bug, target=None):
+        # Add a bug watch to the bugtask for the given target. If no
+        # target is specified, the bug watch is added to the default
+        # bugtask, which is a different product for each bug.
+        if target is None:
+            task = bug.bugtasks[0]
+        else:
+            for task in bug.bugtasks:
+                if task.target == target:
+                    break
+        with person_logged_in(task.target.owner):
+            watch = self.factory.makeBugWatch(bug=bug)
+            task.bugwatch = watch
+
+    def test_pending_bugwatch_elsewhere__no_upstream_specified(self):
+        # By default, those bugs are returned where
+        #   - an upstream task exists
+        #   - the upstream product does not use LP for bug tracking
+        #   - the bug task has no bug watch.
+        # All test bugs fulfill this condition.
+        upstream_target = self.factory.makeProduct()
+        self.setUpUpstreamTests(upstream_target)
+        params = self.getBugTaskSearchParams(
+            user=None, pending_bugwatch_elsewhere=True)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If a bug watch is added to only one of the product related
+        # bug tasks, the bug is still returned.
+        self.addWatch(self.bugtasks[0].bug)
+        self.addWatch(self.bugtasks[1].bug, target=upstream_target)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If bugwatches are added to the other product related bug task
+        # too, the bugs are not included in the search result.
+        self.addWatch(self.bugtasks[0].bug, target=upstream_target)
+        self.addWatch(self.bugtasks[1].bug)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+    def test_pending_bugwatch_elsewhere__upstream_product(self):
+        # If an upstream target using Malone is specified, a search
+        # returns all bugs with a bug task for this target, if the
+        # task does not have a bug watch.
+        upstream_target = self.factory.makeProduct()
+        self.setUpUpstreamTests(upstream_target)
+        # The first bug task of all test bugs is targeted to its
+        # own Product instance.
+        bug = self.bugtasks[0].bug
+        single_bugtask_product = bug.bugtasks[0].target
+        params = self.getBugTaskSearchParams(
+            user=None, pending_bugwatch_elsewhere=True,
+            upstream_target=single_bugtask_product)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        # If a bug watch is added to this task, the search returns an
+        # empty result set.
+        self.addWatch(self.bugtasks[0].bug)
+        self.assertSearchFinds(params, [])
+
+    def test_pending_bugwatch_elsewhere__upstream_product_uses_lp(self):
+        # If an upstream target not using Malone is specified, a search
+        # alsways returns an empty result set.
+        upstream_target = self.factory.makeProduct()
+        self.setUpUpstreamTests(upstream_target)
+        with person_logged_in(upstream_target.owner):
+            upstream_target.official_malone = True
+        params = self.getBugTaskSearchParams(
+            user=None, pending_bugwatch_elsewhere=True,
+            upstream_target=upstream_target)
+        self.assertSearchFinds(params, [])
+
+    def test_pending_bugwatch_elsewhere__upstream_distribution(self):
+        # If an upstream target not using Malone is specified, a search
+        # alsways returns an empty result set.
+        upstream_target = self.factory.makeDistribution()
+        self.setUpUpstreamTests(upstream_target)
+        params = self.getBugTaskSearchParams(
+            user=None, pending_bugwatch_elsewhere=True,
+            upstream_target=upstream_target)
+        self.assertSearchFinds(params, self.bugtasks)
+
+    def test_has_no_upstream_bugtask__target_specified(self):
+        # The target of the default bugtask of the first test bug
+        # (a product) does not appear in other bugs, thus a search
+        # returns all other bugtasks if we specify the search parameters
+        # has_no_upstream_bugtask and use the target described above
+        # as the upstream_target.
+        bug = self.bugtasks[0].bug
+        upstream_target = bug.bugtasks[0].target
+        params = self.getBugTaskSearchParams(
+            user=None, has_no_upstream_bugtask=True,
+            upstream_target=upstream_target)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+        # If a new distribution is specified as the upstream target,
+        # all bugs are returned, since there are no tasks for this
+        # distribution.
+        upstream_target = self.factory.makeDistribution()
+        params = self.getBugTaskSearchParams(
+            user=None, has_no_upstream_bugtask=True,
+            upstream_target=upstream_target)
+        self.assertSearchFinds(params, self.bugtasks)
+        # When we add bugtasks for this distribution, the search returns
+        # an empty result.
+        self.setUpUpstreamTests(upstream_target)
+        self.assertSearchFinds(params, [])
+
+    def test_open_upstream(self):
+        # It is possible to search for bugs with open upstream bugtasks.
+        bug = self.bugtasks[2].bug
+        upstream_task = bug.bugtasks[0]
+        upstream_owner = upstream_task.target.owner
+        with person_logged_in(upstream_owner):
+            upstream_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_owner)
+        params = self.getBugTaskSearchParams(user=None, open_upstream=True)
+        self.assertSearchFinds(params, self.bugtasks[:2])
+
+    def test_open_upstream__upstream_product_specified(self):
+        # A search for bugs having an open upstream bugtask can be
+        # limited to a specific upstream product.
+        bug = self.bugtasks[2].bug
+        upstream_task = bug.bugtasks[0]
+        upstream_product = upstream_task.target
+        params = self.getBugTaskSearchParams(
+            user=None, open_upstream=True, upstream_target=upstream_product)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+        upstream_owner = upstream_product.owner
+        with person_logged_in(upstream_owner):
+            upstream_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_owner)
+        self.assertSearchFinds(params, [])
+
+    def test_open_upstream__upstream_distribution_specified(self):
+        # A search for bugs having an open upstream bugtask can be
+        # limited to a specific upstream distribution.
+        upstream_distro = self.factory.makeDistribution()
+        params = self.getBugTaskSearchParams(
+            user=None, open_upstream=True, upstream_target=upstream_distro)
+        self.assertSearchFinds(params, [])
+        bug = self.bugtasks[0].bug
+        distro_task = self.factory.makeBugTask(
+            bug=bug, target=upstream_distro)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        with person_logged_in(upstream_distro.owner):
+            distro_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_distro.owner)
+        self.assertSearchFinds(params, [])
+
+    def test_resolved_upstream(self):
+        # It is possible to search for bugs with resolved upstream bugtasks.
+        bug = self.bugtasks[2].bug
+        upstream_task = bug.bugtasks[0]
+        upstream_owner = upstream_task.target.owner
+        with person_logged_in(upstream_owner):
+            upstream_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_owner)
+        params = self.getBugTaskSearchParams(user=None, resolved_upstream=True)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+    def test_resolved_upstream__upstream_product_specified(self):
+        # A search for bugs having a resolved upstream bugtask can be
+        # limited to a specific upstream product.
+        bug = self.bugtasks[2].bug
+        upstream_task = bug.bugtasks[0]
+        upstream_product = upstream_task.target
+        params = self.getBugTaskSearchParams(
+            user=None, resolved_upstream=True,
+            upstream_target=upstream_product)
+        self.assertSearchFinds(params, [])
+        upstream_owner = upstream_product.owner
+        for bug in [task.bug for task in self.bugtasks]:
+            upstream_task = bug.bugtasks[0]
+            upstream_owner = upstream_task.owner
+            with person_logged_in(upstream_owner):
+                upstream_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_owner)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+    def test_resolved_upstream__upstream_distribution_specified(self):
+        # A search for bugs having an open upstream bugtask can be
+        # limited to a specific upstream distribution.
+        upstream_distro = self.factory.makeDistribution()
+        params = self.getBugTaskSearchParams(
+            user=None, resolved_upstream=True,
+            upstream_target=upstream_distro)
+        self.assertSearchFinds(params, [])
+        bug = self.bugtasks[0].bug
+        distro_task = self.factory.makeBugTask(
+            bug=bug, target=upstream_distro)
+        self.assertSearchFinds(params, [])
+        with person_logged_in(upstream_distro.owner):
+            distro_task.transitionToStatus(
+                BugTaskStatus.FIXRELEASED, upstream_distro.owner)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+
+
+class SourcePackageTarget(BugTargetTestBase, UpstreamFilterTests):
     """Use a source package as the bug target."""
 
     def setUp(self):
@@ -1275,7 +1489,8 @@
 
 
 class DistributionSourcePackageTarget(BugTargetTestBase,
-                                      BugTargetWithBugSuperVisor):
+                                      BugTargetWithBugSuperVisor,
+                                      UpstreamFilterTests):
     """Use a distribution source package as the bug target."""
 
     def setUp(self):