← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/revert-subscribe-to-tag-bug-151129 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/revert-subscribe-to-tag-bug-151129 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #151129 Can't subscribe to a tag
  https://bugs.launchpad.net/bugs/151129


Subscribing to tags has performance problems. See the linked bug for more information. This branch reverts the change.
-- 
https://code.launchpad.net/~allenap/launchpad/revert-subscribe-to-tag-bug-151129/+merge/41901
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/revert-subscribe-to-tag-bug-151129 into lp:launchpad.
=== modified file 'lib/lp/registry/model/structuralsubscription.py'
--- lib/lp/registry/model/structuralsubscription.py	2010-11-19 14:33:01 +0000
+++ lib/lp/registry/model/structuralsubscription.py	2010-11-25 21:01:30 +0000
@@ -2,26 +2,14 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
-__all__ = [
-    'StructuralSubscription',
-    'StructuralSubscriptionTargetMixin',
-    ]
+__all__ = ['StructuralSubscription',
+           'StructuralSubscriptionTargetMixin']
 
 from sqlobject import ForeignKey
 from storm.expr import (
-    Alias,
     And,
-    CompoundOper,
-    Except,
-    In,
-    Intersect,
     LeftJoin,
-    NamedFunc,
-    Not,
     Or,
-    Select,
-    SQL,
-    Union,
     )
 from storm.store import Store
 from zope.component import (
@@ -46,7 +34,6 @@
 from lp.bugs.model.bugsubscriptionfilterstatus import (
     BugSubscriptionFilterStatus,
     )
-from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag
 from lp.registry.enum import BugNotificationLevel
 from lp.registry.errors import (
     DeleteSubscriptionError,
@@ -484,242 +471,50 @@
 
     def getSubscriptionsForBugTask(self, bugtask, level):
         """See `IStructuralSubscriptionTarget`."""
-        set_builder = BugFilterSetBuilder(
-            bugtask, level, self.__helper.join)
-        return Store.of(self.__helper.pillar).find(
-            StructuralSubscription, In(
-                StructuralSubscription.id,
-                set_builder.subscriptions))
-
-
-class ArrayAgg(NamedFunc):
-    __slots__ = ()
-    name = "ARRAY_AGG"
-
-
-class ArrayContains(CompoundOper):
-    __slots__ = ()
-    oper = "@>"
-
-
-class BugFilterSetBuilder:
-    """A convenience class to build queries for getSubscriptionsForBugTask."""
-
-    def __init__(self, bugtask, level, join_condition):
-        """Initialize a new set builder for bug filters.
-
-        :param bugtask: The `IBugTask` to match against.
-        :param level: A member of `BugNotificationLevel`.
-        :param join_condition: A condition for selecting structural
-            subscriptions. Generally this should limit the subscriptions to a
-            particular target (i.e. project or distribution).
-        """
-        self.status = bugtask.status
-        self.importance = bugtask.importance
-        # The list() gets around some weirdness with security proxies; Storm
-        # does not know how to compile an expression with a proxied list.
-        self.tags = list(bugtask.bug.tags)
-        # Set up common conditions.
-        self.base_conditions = And(
+        origin = [
+            StructuralSubscription,
+            LeftJoin(
+                BugSubscriptionFilter,
+                BugSubscriptionFilter.structural_subscription_id == (
+                    StructuralSubscription.id)),
+            LeftJoin(
+                BugSubscriptionFilterStatus,
+                BugSubscriptionFilterStatus.filter_id == (
+                    BugSubscriptionFilter.id)),
+            LeftJoin(
+                BugSubscriptionFilterImportance,
+                BugSubscriptionFilterImportance.filter_id == (
+                    BugSubscriptionFilter.id)),
+            ]
+
+        if len(bugtask.bug.tags) == 0:
+            tag_conditions = [
+                BugSubscriptionFilter.include_any_tags == False,
+                ]
+        else:
+            tag_conditions = [
+                BugSubscriptionFilter.exclude_any_tags == False,
+                ]
+
+        conditions = [
             StructuralSubscription.bug_notification_level >= level,
-            join_condition)
-        # Set up common filter conditions.
-        if len(self.tags) == 0:
-            self.filter_conditions = And(
-                # When the bug has no tags, filters with include_any_tags set
-                # can never match.
-                Not(BugSubscriptionFilter.include_any_tags),
-                self.base_conditions)
-        else:
-            self.filter_conditions = And(
-                # When the bug has tags, filters with exclude_any_tags set can
-                # never match.
-                Not(BugSubscriptionFilter.exclude_any_tags),
-                self.base_conditions)
-
-    @property
-    def subscriptions_without_filters(self):
-        """Subscriptions without filters."""
-        return Select(
-            StructuralSubscription.id,
-            tables=(
-                StructuralSubscription,
-                LeftJoin(
-                    BugSubscriptionFilter,
-                    BugSubscriptionFilter.structural_subscription_id == (
-                        StructuralSubscription.id))),
-            where=And(
+            Or(
+                # There's no filter or ...
                 BugSubscriptionFilter.id == None,
-                self.base_conditions))
-
-    def _filters_matching_x(self, join, where_condition, **extra):
-        """Return an expression yielding `(subscription_id, filter_id)` rows.
-
-        The expressions returned by this function are used in set (union,
-        intersect, except) operations at the *filter* level. However, the
-        interesting result of these set operations is the structural
-        subscription, hence both columns are included in the expressions
-        generated. Since a structural subscription can have zero or more
-        filters, and a filter can never be associated with more than one
-        subscription, the set operations are unaffected.
-        """
-        return Select(
-            columns=(
-                # Alias this column so it can be selected in
-                # subscriptions_matching.
-                Alias(
-                    BugSubscriptionFilter.structural_subscription_id,
-                    "structural_subscription_id"),
-                BugSubscriptionFilter.id),
-            tables=(
-                StructuralSubscription, BugSubscriptionFilter, join),
-            where=And(
-                BugSubscriptionFilter.structural_subscription_id == (
-                    StructuralSubscription.id),
-                self.filter_conditions,
-                where_condition),
-            **extra)
-
-    @property
-    def filters_matching_status(self):
-        """Filters with the given bugtask's status."""
-        join = LeftJoin(
-            BugSubscriptionFilterStatus,
-            BugSubscriptionFilterStatus.filter_id == (
-                BugSubscriptionFilter.id))
-        condition = Or(
-            BugSubscriptionFilterStatus.id == None,
-            BugSubscriptionFilterStatus.status == self.status)
-        return self._filters_matching_x(join, condition)
-
-    @property
-    def filters_matching_importance(self):
-        """Filters with the given bugtask's importance."""
-        join = LeftJoin(
-            BugSubscriptionFilterImportance,
-            BugSubscriptionFilterImportance.filter_id == (
-                BugSubscriptionFilter.id))
-        condition = Or(
-            BugSubscriptionFilterImportance.id == None,
-            BugSubscriptionFilterImportance.importance == self.importance)
-        return self._filters_matching_x(join, condition)
-
-    @property
-    def filters_without_include_tags(self):
-        """Filters with no tags required."""
-        join = LeftJoin(
-            BugSubscriptionFilterTag,
-            And(BugSubscriptionFilterTag.filter_id == (
-                    BugSubscriptionFilter.id),
-                BugSubscriptionFilterTag.include))
-        return self._filters_matching_x(
-            join, BugSubscriptionFilterTag.id == None)
-
-    @property
-    def filters_matching_any_include_tags(self):
-        """Filters including any of the bug's tags."""
-        condition = And(
-            BugSubscriptionFilterTag.filter_id == (
-                BugSubscriptionFilter.id),
-            BugSubscriptionFilterTag.include,
-            Not(BugSubscriptionFilter.find_all_tags),
-            In(BugSubscriptionFilterTag.tag, self.tags))
-        return self._filters_matching_x(
-            BugSubscriptionFilterTag, condition)
-
-    @property
-    def filters_matching_any_exclude_tags(self):
-        """Filters excluding any of the bug's tags."""
-        condition = And(
-            BugSubscriptionFilterTag.filter_id == (
-                BugSubscriptionFilter.id),
-            Not(BugSubscriptionFilterTag.include),
-            Not(BugSubscriptionFilter.find_all_tags),
-            In(BugSubscriptionFilterTag.tag, self.tags))
-        return self._filters_matching_x(
-            BugSubscriptionFilterTag, condition)
-
-    def _filters_matching_all_x_tags(self, where_condition):
-        """Return an expression yielding `(subscription_id, filter_id)` rows.
-
-        This joins to `BugSubscriptionFilterTag` and calls up to
-        `_filters_matching_x`, and groups by filter. Conditions are added to
-        ensure that all rows in each group are a subset of the bug's tags.
-        """
-        tags_array = "ARRAY[%s]::TEXT[]" % ",".join(
-            quote(tag) for tag in self.tags)
-        return self._filters_matching_x(
-            BugSubscriptionFilterTag,
-            And(
-                BugSubscriptionFilterTag.filter_id == (
-                    BugSubscriptionFilter.id),
-                BugSubscriptionFilter.find_all_tags,
-                self.filter_conditions,
-                where_condition),
-            group_by=(
-                BugSubscriptionFilter.structural_subscription_id,
-                BugSubscriptionFilter.id),
-            having=ArrayContains(
-                SQL(tags_array), ArrayAgg(
-                    BugSubscriptionFilterTag.tag)))
-
-    @property
-    def filters_matching_all_include_tags(self):
-        """Filters including the bug's tags."""
-        return self._filters_matching_all_x_tags(
-            BugSubscriptionFilterTag.include)
-
-    @property
-    def filters_matching_all_exclude_tags(self):
-        """Filters excluding the bug's tags."""
-        return self._filters_matching_all_x_tags(
-            Not(BugSubscriptionFilterTag.include))
-
-    @property
-    def filters_matching_include_tags(self):
-        """Filters with tag filters including the bug."""
-        return Union(
-            self.filters_matching_any_include_tags,
-            self.filters_matching_all_include_tags)
-
-    @property
-    def filters_matching_exclude_tags(self):
-        """Filters with tag filters excluding the bug."""
-        return Union(
-            self.filters_matching_any_exclude_tags,
-            self.filters_matching_all_exclude_tags)
-
-    @property
-    def filters_matching_tags(self):
-        """Filters with tag filters matching the bug."""
-        if len(self.tags) == 0:
-            # The filter's required tags must be an empty set. The filter's
-            # excluded tags can be anything so no condition is needed.
-            return self.filters_without_include_tags
-        else:
-            return Except(
-                Union(self.filters_without_include_tags,
-                      self.filters_matching_include_tags),
-                self.filters_matching_exclude_tags)
-
-    @property
-    def filters_matching(self):
-        """Filters matching the bug."""
-        return Intersect(
-            self.filters_matching_status,
-            self.filters_matching_importance,
-            self.filters_matching_tags)
-
-    @property
-    def subscriptions_with_matching_filters(self):
-        """Subscriptions with one or more filters matching the bug."""
-        return Select(
-            # I don't know of a more Storm-like way of doing this.
-            SQL("filters_matching.structural_subscription_id"),
-            tables=Alias(self.filters_matching, "filters_matching"))
-
-    @property
-    def subscriptions(self):
-        return Union(
-            self.subscriptions_without_filters,
-            self.subscriptions_with_matching_filters)
+                # There is a filter and ...
+                And(
+                    # There's no status filter, or there is a status filter
+                    # and and it matches.
+                    Or(BugSubscriptionFilterStatus.id == None,
+                       BugSubscriptionFilterStatus.status == bugtask.status),
+                    # There's no importance filter, or there is an importance
+                    # filter and it matches.
+                    Or(BugSubscriptionFilterImportance.id == None,
+                       BugSubscriptionFilterImportance.importance == (
+                            bugtask.importance)),
+                    # Any number of conditions relating to tags.
+                    *tag_conditions)),
+            ]
+
+        return Store.of(self.__helper.pillar).using(*origin).find(
+            StructuralSubscription, self.__helper.join, *conditions)

=== modified file 'lib/lp/registry/tests/test_structuralsubscriptiontarget.py'
--- lib/lp/registry/tests/test_structuralsubscriptiontarget.py	2010-11-18 21:29:44 +0000
+++ lib/lp/registry/tests/test_structuralsubscriptiontarget.py	2010-11-25 21:01:30 +0000
@@ -32,6 +32,7 @@
     BugTaskImportance,
     BugTaskStatus,
     )
+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
 from lp.bugs.tests.test_bugtarget import bugtarget_filebug
 from lp.registry.enum import BugNotificationLevel
 from lp.registry.errors import (
@@ -56,11 +57,10 @@
 from lp.testing.matchers import Provides
 
 
-class RestrictedStructuralSubscriptionTestBase:
-    """Tests suitable for a target that restricts structural subscriptions."""
+class StructuralSubscriptionTestBase:
 
     def setUp(self):
-        super(RestrictedStructuralSubscriptionTestBase, self).setUp()
+        super(StructuralSubscriptionTestBase, self).setUp()
         self.ordinary_subscriber = self.factory.makePerson()
         self.bug_supervisor_subscriber = self.factory.makePerson()
         self.team_owner = self.factory.makePerson()
@@ -124,12 +124,7 @@
             self.ordinary_subscriber, self.ordinary_subscriber)
 
 
-class UnrestrictedStructuralSubscriptionTestBase(
-    RestrictedStructuralSubscriptionTestBase):
-    """
-    Tests suitable for a target that does not restrict structural
-    subscriptions.
-    """
+class UnrestrictedStructuralSubscription(StructuralSubscriptionTestBase):
 
     def test_structural_subscription_by_ordinary_user(self):
         # ordinary users can subscribe themselves
@@ -172,276 +167,198 @@
             None)
 
 
-class FilteredStructuralSubscriptionTestBase:
+class FilteredStructuralSubscriptionTestBase(StructuralSubscriptionTestBase):
     """Tests for filtered structural subscriptions."""
 
-    layer = LaunchpadFunctionalLayer
-
-    def makeTarget(self):
-        raise NotImplementedError(self.makeTarget)
-
     def makeBugTask(self):
         return self.factory.makeBugTask(target=self.target)
 
-    def setUp(self):
-        super(FilteredStructuralSubscriptionTestBase, self).setUp()
-        self.ordinary_subscriber = self.factory.makePerson()
-        login_person(self.ordinary_subscriber)
-        self.target = self.makeTarget()
-        self.bugtask = self.makeBugTask()
-        self.bug = self.bugtask.bug
-        self.subscription = self.target.addSubscription(
-            self.ordinary_subscriber, self.ordinary_subscriber)
-        self.subscription.bug_notification_level = (
-            BugNotificationLevel.COMMENTS)
-
-    def assertSubscriptions(
-        self, expected_subscriptions, level=BugNotificationLevel.NOTHING):
-        observed_subscriptions = list(
-            self.target.getSubscriptionsForBugTask(self.bugtask, level))
-        self.assertEqual(expected_subscriptions, observed_subscriptions)
-
     def test_getSubscriptionsForBugTask(self):
         # If no one has a filtered subscription for the given bug, the result
         # of getSubscriptionsForBugTask() is the same as for
         # getSubscriptions().
+        bugtask = self.makeBugTask()
         subscriptions = self.target.getSubscriptions(
             min_bug_notification_level=BugNotificationLevel.NOTHING)
-        self.assertSubscriptions(list(subscriptions))
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual(list(subscriptions), list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_filter_on_status(self):
         # If a status filter exists for a subscription, the result of
         # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
+        bugtask = self.makeBugTask()
+
+        # Create a new subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.COMMENTS
 
         # Without any filters the subscription is found.
-        self.assertSubscriptions([self.subscription])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
         # Filter the subscription to bugs in the CONFIRMED state.
-        subscription_filter = self.subscription.newBugFilter()
+        subscription_filter = BugSubscriptionFilter()
+        subscription_filter.structural_subscription = subscription
         subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
 
         # With the filter the subscription is not found.
-        self.assertSubscriptions([])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
         # If the filter is adjusted, the subscription is found again.
-        subscription_filter.statuses = [self.bugtask.status]
-        self.assertSubscriptions([self.subscription])
+        subscription_filter.statuses = [bugtask.status]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_filter_on_importance(self):
         # If an importance filter exists for a subscription, the result of
         # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
+        bugtask = self.makeBugTask()
+
+        # Create a new subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.COMMENTS
 
         # Without any filters the subscription is found.
-        self.assertSubscriptions([self.subscription])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
         # Filter the subscription to bugs in the CRITICAL state.
-        subscription_filter = self.subscription.newBugFilter()
+        subscription_filter = BugSubscriptionFilter()
+        subscription_filter.structural_subscription = subscription
         subscription_filter.importances = [BugTaskImportance.CRITICAL]
 
         # With the filter the subscription is not found.
-        self.assertSubscriptions([])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
         # If the filter is adjusted, the subscription is found again.
-        subscription_filter.importances = [self.bugtask.importance]
-        self.assertSubscriptions([self.subscription])
+        subscription_filter.importances = [bugtask.importance]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_filter_on_level(self):
         # All structural subscriptions have a level for bug notifications
         # which getSubscriptionsForBugTask() observes.
+        bugtask = self.makeBugTask()
 
-        # Adjust the subscription level to METADATA.
-        self.subscription.bug_notification_level = (
-            BugNotificationLevel.METADATA)
+        # Create a new METADATA level subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.METADATA
 
         # The subscription is found when looking for NOTHING or above.
-        self.assertSubscriptions(
-            [self.subscription], BugNotificationLevel.NOTHING)
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
         # The subscription is found when looking for METADATA or above.
-        self.assertSubscriptions(
-            [self.subscription], BugNotificationLevel.METADATA)
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.METADATA)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
         # The subscription is not found when looking for COMMENTS or above.
-        self.assertSubscriptions(
-            [], BugNotificationLevel.COMMENTS)
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.COMMENTS)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_filter_include_any_tags(self):
         # If a subscription filter has include_any_tags, a bug with one or
         # more tags is matched.
+        bugtask = self.makeBugTask()
 
-        subscription_filter = self.subscription.newBugFilter()
+        # Create a new subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.COMMENTS
+        subscription_filter = subscription.newBugFilter()
         subscription_filter.include_any_tags = True
 
         # Without any tags the subscription is not found.
-        self.assertSubscriptions([])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
         # With any tag the subscription is found.
-        self.bug.tags = ["foo"]
-        self.assertSubscriptions([self.subscription])
+        bugtask.bug.tags = ["foo"]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_filter_exclude_any_tags(self):
         # If a subscription filter has exclude_any_tags, only bugs with no
         # tags are matched.
+        bugtask = self.makeBugTask()
 
-        subscription_filter = self.subscription.newBugFilter()
+        # Create a new subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.COMMENTS
+        subscription_filter = subscription.newBugFilter()
         subscription_filter.exclude_any_tags = True
 
         # Without any tags the subscription is found.
-        self.assertSubscriptions([self.subscription])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
         # With any tag the subscription is not found.
-        self.bug.tags = ["foo"]
-        self.assertSubscriptions([])
-
-    def test_getSubscriptionsForBugTask_with_filter_for_any_tag(self):
-        # If a subscription filter specifies that any of one or more specific
-        # tags must be present, bugs with any of those tags are matched.
-
-        # Looking for either the "foo" or the "bar" tag.
-        subscription_filter = self.subscription.newBugFilter()
-        subscription_filter.tags = [u"foo", u"bar"]
-        subscription_filter.find_all_tags = False
-
-        # Without either tag the subscription is not found.
-        self.assertSubscriptions([])
-
-        # With either tag the subscription is found.
-        self.bug.tags = ["bar", "baz"]
-        self.assertSubscriptions([self.subscription])
-
-    def test_getSubscriptionsForBugTask_with_filter_for_all_tags(self):
-        # If a subscription filter specifies that all of one or more specific
-        # tags must be present, bugs with all of those tags are matched.
-
-        # Looking for both the "foo" and the "bar" tag.
-        subscription_filter = self.subscription.newBugFilter()
-        subscription_filter.tags = [u"foo", u"bar"]
-        subscription_filter.find_all_tags = True
-
-        # Without either tag the subscription is not found.
-        self.assertSubscriptions([])
-
-        # Without only one of the required tags the subscription is not found.
-        self.bug.tags = ["foo"]
-        self.assertSubscriptions([])
-
-        # With both required tags the subscription is found.
-        self.bug.tags = ["foo", "bar"]
-        self.assertSubscriptions([self.subscription])
-
-    def test_getSubscriptionsForBugTask_with_filter_for_not_any_tag(self):
-        # If a subscription filter specifies that any of one or more specific
-        # tags must not be present, bugs without any of those tags are
-        # matched.
-
-        # Looking to exclude the "foo" or "bar" tags.
-        subscription_filter = self.subscription.newBugFilter()
-        subscription_filter.tags = [u"-foo", u"-bar"]
-        subscription_filter.find_all_tags = False
-
-        # Without either tag the subscription is found.
-        self.assertSubscriptions([self.subscription])
-
-        # With either tag the subscription is no longer found.
-        self.bug.tags = ["foo"]
-        self.assertSubscriptions([])
-
-    def test_getSubscriptionsForBugTask_with_filter_for_not_all_tags(self):
-        # If a subscription filter specifies that all of one or more specific
-        # tags must not be present, bugs without all of those tags are
-        # matched.
-
-        # Looking to exclude the "foo" and "bar" tags.
-        subscription_filter = self.subscription.newBugFilter()
-        subscription_filter.tags = [u"-foo", u"-bar"]
-        subscription_filter.find_all_tags = True
-
-        # Without either tag the subscription is found.
-        self.assertSubscriptions([self.subscription])
-
-        # With only one of the excluded tags the subscription is found.
-        self.bug.tags = ["foo"]
-        self.assertSubscriptions([self.subscription])
-
-        # With both tags the subscription is no longer found.
-        self.bug.tags = ["foo", "bar"]
-        self.assertSubscriptions([])
+        bugtask.bug.tags = ["foo"]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
     def test_getSubscriptionsForBugTask_with_multiple_filters(self):
         # If multiple filters exist for a subscription, all filters must
         # match.
+        bugtask = self.makeBugTask()
 
-        # Add the "foo" tag to the bug.
-        self.bug.tags = ["foo"]
+        # Create a new subscription on self.target.
+        login_person(self.ordinary_subscriber)
+        subscription = self.target.addSubscription(
+            self.ordinary_subscriber, self.ordinary_subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.COMMENTS
 
         # Filter the subscription to bugs in the CRITICAL state.
-        subscription_filter = self.subscription.newBugFilter()
+        subscription_filter = BugSubscriptionFilter()
+        subscription_filter.structural_subscription = subscription
         subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
         subscription_filter.importances = [BugTaskImportance.CRITICAL]
 
         # With the filter the subscription is not found.
-        self.assertSubscriptions([])
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
         # If the filter is adjusted to match status but not importance, the
         # subscription is still not found.
-        subscription_filter.statuses = [self.bugtask.status]
-        self.assertSubscriptions([])
+        subscription_filter.statuses = [bugtask.status]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([], list(subscriptions_for_bugtask))
 
         # If the filter is adjusted to also match importance, the subscription
         # is found again.
-        subscription_filter.importances = [self.bugtask.importance]
-        self.assertSubscriptions([self.subscription])
-
-        # If the filter is given some tag criteria, the subscription is not
-        # found.
-        subscription_filter.tags = [u"-foo", u"bar", u"baz"]
-        subscription_filter.find_all_tags = False
-        self.assertSubscriptions([])
-
-        # After removing the "foo" tag and adding the "bar" tag, the
-        # subscription is found.
-        self.bug.tags = ["bar"]
-        self.assertSubscriptions([self.subscription])
-
-        # Requiring that all tag criteria are fulfilled causes the
-        # subscription to no longer be found.
-        subscription_filter.find_all_tags = True
-        self.assertSubscriptions([])
-
-        # After adding the "baz" tag, the subscription is found again.
-        self.bug.tags = ["bar", "baz"]
-        self.assertSubscriptions([self.subscription])
-
-    def test_getSubscriptionsForBugTask_any_filter_is_a_match(self):
-        # If a subscription has multiple filters, the subscription is selected
-        # when any filter is found to match. Put another way, the filters are
-        # ORed together.
-        subscription_filter1 = self.subscription.newBugFilter()
-        subscription_filter1.statuses = [BugTaskStatus.CONFIRMED]
-        subscription_filter2 = self.subscription.newBugFilter()
-        subscription_filter2.tags = [u"foo"]
-
-        # With the filter the subscription is not found.
-        self.assertSubscriptions([])
-
-        # If the bugtask is adjusted to match the criteria of the first filter
-        # but not those of the second, the subscription is found.
-        self.bugtask.transitionToStatus(
-            BugTaskStatus.CONFIRMED, self.ordinary_subscriber)
-        self.assertSubscriptions([self.subscription])
-
-        # If the filter is adjusted to also match the criteria of the second
-        # filter, the subscription is still found.
-        self.bugtask.bug.tags = [u"foo"]
-        self.assertSubscriptions([self.subscription])
-
-        # If the bugtask is adjusted to no longer match the criteria of the
-        # first filter, the subscription is still found.
-        self.bugtask.transitionToStatus(
-            BugTaskStatus.INPROGRESS, self.ordinary_subscriber)
-        self.assertSubscriptions([self.subscription])
+        subscription_filter.importances = [bugtask.importance]
+        subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
+            bugtask, BugNotificationLevel.NOTHING)
+        self.assertEqual([subscription], list(subscriptions_for_bugtask))
 
 
 class TestStructuralSubscriptionForDistro(
-    RestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -504,15 +421,10 @@
             StructuralSubscription)
 
 
-class TestStructuralSubscriptionFiltersForDistro(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeDistribution()
-
-
 class TestStructuralSubscriptionForProduct(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -521,15 +433,10 @@
         self.target = self.factory.makeProduct()
 
 
-class TestStructuralSubscriptionFiltersForProduct(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeProduct()
-
-
 class TestStructuralSubscriptionForDistroSourcePackage(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -539,15 +446,10 @@
         self.target = ProxyFactory(self.target)
 
 
-class TestStructuralSubscriptionFiltersForDistroSourcePackage(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeDistributionSourcePackage()
-
-
 class TestStructuralSubscriptionForMilestone(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -556,19 +458,15 @@
         self.target = self.factory.makeMilestone()
         self.target = ProxyFactory(self.target)
 
-
-class TestStructuralSubscriptionFiltersForMilestone(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeMilestone()
-
     def makeBugTask(self):
+        # XXX Should test with target *and* series_target.
         return self.factory.makeBugTask(target=self.target.series_target)
 
 
 class TestStructuralSubscriptionForDistroSeries(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -578,15 +476,10 @@
         self.target = ProxyFactory(self.target)
 
 
-class TestStructuralSubscriptionFiltersForDistroSeries(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeDistroSeries()
-
-
 class TestStructuralSubscriptionForProjectGroup(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -595,20 +488,15 @@
         self.target = self.factory.makeProject()
         self.target = ProxyFactory(self.target)
 
-
-class TestStructuralSubscriptionFiltersForProjectGroup(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeProject()
-
     def makeBugTask(self):
         return self.factory.makeBugTask(
             target=self.factory.makeProduct(project=self.target))
 
 
 class TestStructuralSubscriptionForProductSeries(
-    UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
+    UnrestrictedStructuralSubscription,
+    FilteredStructuralSubscriptionTestBase,
+    TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
@@ -618,13 +506,6 @@
         self.target = ProxyFactory(self.target)
 
 
-class TestStructuralSubscriptionFiltersForProductSeries(
-    FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
-
-    def makeTarget(self):
-        return self.factory.makeProductSeries()
-
-
 class TestStructuralSubscriptionTargetHelper(TestCaseWithFactory):
     """Tests for implementations of `IStructuralSubscriptionTargetHelper`."""