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

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 11985
Proposed branch: lp:~allenap/launchpad/revert-subscribe-to-tag-bug-151129
Merge into: lp:launchpad
Diff against target: 888 lines (+176/-500)
2 files modified
lib/lp/registry/model/structuralsubscription.py (+47/-252)
lib/lp/registry/tests/test_structuralsubscriptiontarget.py (+129/-248)
To merge this branch: bzr merge lp:~allenap/launchpad/revert-subscribe-to-tag-bug-151129
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Approve
Gavin Panella (community) Approve
Review via email: mp+41901@code.launchpad.net

Commit message

[r=allenap][ui=none][bug=151129][rollback=11972] Subscribing to tags has performance problems, reverting for now.

Description of the change

Subscribing to tags has performance problems. See the linked bug for more information. This branch reverts the change.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) :
review: Approve
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Trivial.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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:03:11 +0000
@@ -2,26 +2,14 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
5__all__ = [5__all__ = ['StructuralSubscription',
6 'StructuralSubscription',6 'StructuralSubscriptionTargetMixin']
7 'StructuralSubscriptionTargetMixin',
8 ]
97
10from sqlobject import ForeignKey8from sqlobject import ForeignKey
11from storm.expr import (9from storm.expr import (
12 Alias,
13 And,10 And,
14 CompoundOper,
15 Except,
16 In,
17 Intersect,
18 LeftJoin,11 LeftJoin,
19 NamedFunc,
20 Not,
21 Or,12 Or,
22 Select,
23 SQL,
24 Union,
25 )13 )
26from storm.store import Store14from storm.store import Store
27from zope.component import (15from zope.component import (
@@ -46,7 +34,6 @@
46from lp.bugs.model.bugsubscriptionfilterstatus import (34from lp.bugs.model.bugsubscriptionfilterstatus import (
47 BugSubscriptionFilterStatus,35 BugSubscriptionFilterStatus,
48 )36 )
49from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag
50from lp.registry.enum import BugNotificationLevel37from lp.registry.enum import BugNotificationLevel
51from lp.registry.errors import (38from lp.registry.errors import (
52 DeleteSubscriptionError,39 DeleteSubscriptionError,
@@ -484,242 +471,50 @@
484471
485 def getSubscriptionsForBugTask(self, bugtask, level):472 def getSubscriptionsForBugTask(self, bugtask, level):
486 """See `IStructuralSubscriptionTarget`."""473 """See `IStructuralSubscriptionTarget`."""
487 set_builder = BugFilterSetBuilder(474 origin = [
488 bugtask, level, self.__helper.join)475 StructuralSubscription,
489 return Store.of(self.__helper.pillar).find(476 LeftJoin(
490 StructuralSubscription, In(477 BugSubscriptionFilter,
491 StructuralSubscription.id,478 BugSubscriptionFilter.structural_subscription_id == (
492 set_builder.subscriptions))479 StructuralSubscription.id)),
493480 LeftJoin(
494481 BugSubscriptionFilterStatus,
495class ArrayAgg(NamedFunc):482 BugSubscriptionFilterStatus.filter_id == (
496 __slots__ = ()483 BugSubscriptionFilter.id)),
497 name = "ARRAY_AGG"484 LeftJoin(
498485 BugSubscriptionFilterImportance,
499486 BugSubscriptionFilterImportance.filter_id == (
500class ArrayContains(CompoundOper):487 BugSubscriptionFilter.id)),
501 __slots__ = ()488 ]
502 oper = "@>"489
503490 if len(bugtask.bug.tags) == 0:
504491 tag_conditions = [
505class BugFilterSetBuilder:492 BugSubscriptionFilter.include_any_tags == False,
506 """A convenience class to build queries for getSubscriptionsForBugTask."""493 ]
507494 else:
508 def __init__(self, bugtask, level, join_condition):495 tag_conditions = [
509 """Initialize a new set builder for bug filters.496 BugSubscriptionFilter.exclude_any_tags == False,
510497 ]
511 :param bugtask: The `IBugTask` to match against.498
512 :param level: A member of `BugNotificationLevel`.499 conditions = [
513 :param join_condition: A condition for selecting structural
514 subscriptions. Generally this should limit the subscriptions to a
515 particular target (i.e. project or distribution).
516 """
517 self.status = bugtask.status
518 self.importance = bugtask.importance
519 # The list() gets around some weirdness with security proxies; Storm
520 # does not know how to compile an expression with a proxied list.
521 self.tags = list(bugtask.bug.tags)
522 # Set up common conditions.
523 self.base_conditions = And(
524 StructuralSubscription.bug_notification_level >= level,500 StructuralSubscription.bug_notification_level >= level,
525 join_condition)501 Or(
526 # Set up common filter conditions.502 # There's no filter or ...
527 if len(self.tags) == 0:
528 self.filter_conditions = And(
529 # When the bug has no tags, filters with include_any_tags set
530 # can never match.
531 Not(BugSubscriptionFilter.include_any_tags),
532 self.base_conditions)
533 else:
534 self.filter_conditions = And(
535 # When the bug has tags, filters with exclude_any_tags set can
536 # never match.
537 Not(BugSubscriptionFilter.exclude_any_tags),
538 self.base_conditions)
539
540 @property
541 def subscriptions_without_filters(self):
542 """Subscriptions without filters."""
543 return Select(
544 StructuralSubscription.id,
545 tables=(
546 StructuralSubscription,
547 LeftJoin(
548 BugSubscriptionFilter,
549 BugSubscriptionFilter.structural_subscription_id == (
550 StructuralSubscription.id))),
551 where=And(
552 BugSubscriptionFilter.id == None,503 BugSubscriptionFilter.id == None,
553 self.base_conditions))504 # There is a filter and ...
554505 And(
555 def _filters_matching_x(self, join, where_condition, **extra):506 # There's no status filter, or there is a status filter
556 """Return an expression yielding `(subscription_id, filter_id)` rows.507 # and and it matches.
557508 Or(BugSubscriptionFilterStatus.id == None,
558 The expressions returned by this function are used in set (union,509 BugSubscriptionFilterStatus.status == bugtask.status),
559 intersect, except) operations at the *filter* level. However, the510 # There's no importance filter, or there is an importance
560 interesting result of these set operations is the structural511 # filter and it matches.
561 subscription, hence both columns are included in the expressions512 Or(BugSubscriptionFilterImportance.id == None,
562 generated. Since a structural subscription can have zero or more513 BugSubscriptionFilterImportance.importance == (
563 filters, and a filter can never be associated with more than one514 bugtask.importance)),
564 subscription, the set operations are unaffected.515 # Any number of conditions relating to tags.
565 """516 *tag_conditions)),
566 return Select(517 ]
567 columns=(518
568 # Alias this column so it can be selected in519 return Store.of(self.__helper.pillar).using(*origin).find(
569 # subscriptions_matching.520 StructuralSubscription, self.__helper.join, *conditions)
570 Alias(
571 BugSubscriptionFilter.structural_subscription_id,
572 "structural_subscription_id"),
573 BugSubscriptionFilter.id),
574 tables=(
575 StructuralSubscription, BugSubscriptionFilter, join),
576 where=And(
577 BugSubscriptionFilter.structural_subscription_id == (
578 StructuralSubscription.id),
579 self.filter_conditions,
580 where_condition),
581 **extra)
582
583 @property
584 def filters_matching_status(self):
585 """Filters with the given bugtask's status."""
586 join = LeftJoin(
587 BugSubscriptionFilterStatus,
588 BugSubscriptionFilterStatus.filter_id == (
589 BugSubscriptionFilter.id))
590 condition = Or(
591 BugSubscriptionFilterStatus.id == None,
592 BugSubscriptionFilterStatus.status == self.status)
593 return self._filters_matching_x(join, condition)
594
595 @property
596 def filters_matching_importance(self):
597 """Filters with the given bugtask's importance."""
598 join = LeftJoin(
599 BugSubscriptionFilterImportance,
600 BugSubscriptionFilterImportance.filter_id == (
601 BugSubscriptionFilter.id))
602 condition = Or(
603 BugSubscriptionFilterImportance.id == None,
604 BugSubscriptionFilterImportance.importance == self.importance)
605 return self._filters_matching_x(join, condition)
606
607 @property
608 def filters_without_include_tags(self):
609 """Filters with no tags required."""
610 join = LeftJoin(
611 BugSubscriptionFilterTag,
612 And(BugSubscriptionFilterTag.filter_id == (
613 BugSubscriptionFilter.id),
614 BugSubscriptionFilterTag.include))
615 return self._filters_matching_x(
616 join, BugSubscriptionFilterTag.id == None)
617
618 @property
619 def filters_matching_any_include_tags(self):
620 """Filters including any of the bug's tags."""
621 condition = And(
622 BugSubscriptionFilterTag.filter_id == (
623 BugSubscriptionFilter.id),
624 BugSubscriptionFilterTag.include,
625 Not(BugSubscriptionFilter.find_all_tags),
626 In(BugSubscriptionFilterTag.tag, self.tags))
627 return self._filters_matching_x(
628 BugSubscriptionFilterTag, condition)
629
630 @property
631 def filters_matching_any_exclude_tags(self):
632 """Filters excluding any of the bug's tags."""
633 condition = And(
634 BugSubscriptionFilterTag.filter_id == (
635 BugSubscriptionFilter.id),
636 Not(BugSubscriptionFilterTag.include),
637 Not(BugSubscriptionFilter.find_all_tags),
638 In(BugSubscriptionFilterTag.tag, self.tags))
639 return self._filters_matching_x(
640 BugSubscriptionFilterTag, condition)
641
642 def _filters_matching_all_x_tags(self, where_condition):
643 """Return an expression yielding `(subscription_id, filter_id)` rows.
644
645 This joins to `BugSubscriptionFilterTag` and calls up to
646 `_filters_matching_x`, and groups by filter. Conditions are added to
647 ensure that all rows in each group are a subset of the bug's tags.
648 """
649 tags_array = "ARRAY[%s]::TEXT[]" % ",".join(
650 quote(tag) for tag in self.tags)
651 return self._filters_matching_x(
652 BugSubscriptionFilterTag,
653 And(
654 BugSubscriptionFilterTag.filter_id == (
655 BugSubscriptionFilter.id),
656 BugSubscriptionFilter.find_all_tags,
657 self.filter_conditions,
658 where_condition),
659 group_by=(
660 BugSubscriptionFilter.structural_subscription_id,
661 BugSubscriptionFilter.id),
662 having=ArrayContains(
663 SQL(tags_array), ArrayAgg(
664 BugSubscriptionFilterTag.tag)))
665
666 @property
667 def filters_matching_all_include_tags(self):
668 """Filters including the bug's tags."""
669 return self._filters_matching_all_x_tags(
670 BugSubscriptionFilterTag.include)
671
672 @property
673 def filters_matching_all_exclude_tags(self):
674 """Filters excluding the bug's tags."""
675 return self._filters_matching_all_x_tags(
676 Not(BugSubscriptionFilterTag.include))
677
678 @property
679 def filters_matching_include_tags(self):
680 """Filters with tag filters including the bug."""
681 return Union(
682 self.filters_matching_any_include_tags,
683 self.filters_matching_all_include_tags)
684
685 @property
686 def filters_matching_exclude_tags(self):
687 """Filters with tag filters excluding the bug."""
688 return Union(
689 self.filters_matching_any_exclude_tags,
690 self.filters_matching_all_exclude_tags)
691
692 @property
693 def filters_matching_tags(self):
694 """Filters with tag filters matching the bug."""
695 if len(self.tags) == 0:
696 # The filter's required tags must be an empty set. The filter's
697 # excluded tags can be anything so no condition is needed.
698 return self.filters_without_include_tags
699 else:
700 return Except(
701 Union(self.filters_without_include_tags,
702 self.filters_matching_include_tags),
703 self.filters_matching_exclude_tags)
704
705 @property
706 def filters_matching(self):
707 """Filters matching the bug."""
708 return Intersect(
709 self.filters_matching_status,
710 self.filters_matching_importance,
711 self.filters_matching_tags)
712
713 @property
714 def subscriptions_with_matching_filters(self):
715 """Subscriptions with one or more filters matching the bug."""
716 return Select(
717 # I don't know of a more Storm-like way of doing this.
718 SQL("filters_matching.structural_subscription_id"),
719 tables=Alias(self.filters_matching, "filters_matching"))
720
721 @property
722 def subscriptions(self):
723 return Union(
724 self.subscriptions_without_filters,
725 self.subscriptions_with_matching_filters)
726521
=== 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:03:11 +0000
@@ -32,6 +32,7 @@
32 BugTaskImportance,32 BugTaskImportance,
33 BugTaskStatus,33 BugTaskStatus,
34 )34 )
35from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
35from lp.bugs.tests.test_bugtarget import bugtarget_filebug36from lp.bugs.tests.test_bugtarget import bugtarget_filebug
36from lp.registry.enum import BugNotificationLevel37from lp.registry.enum import BugNotificationLevel
37from lp.registry.errors import (38from lp.registry.errors import (
@@ -56,11 +57,10 @@
56from lp.testing.matchers import Provides57from lp.testing.matchers import Provides
5758
5859
59class RestrictedStructuralSubscriptionTestBase:60class StructuralSubscriptionTestBase:
60 """Tests suitable for a target that restricts structural subscriptions."""
6161
62 def setUp(self):62 def setUp(self):
63 super(RestrictedStructuralSubscriptionTestBase, self).setUp()63 super(StructuralSubscriptionTestBase, self).setUp()
64 self.ordinary_subscriber = self.factory.makePerson()64 self.ordinary_subscriber = self.factory.makePerson()
65 self.bug_supervisor_subscriber = self.factory.makePerson()65 self.bug_supervisor_subscriber = self.factory.makePerson()
66 self.team_owner = self.factory.makePerson()66 self.team_owner = self.factory.makePerson()
@@ -124,12 +124,7 @@
124 self.ordinary_subscriber, self.ordinary_subscriber)124 self.ordinary_subscriber, self.ordinary_subscriber)
125125
126126
127class UnrestrictedStructuralSubscriptionTestBase(127class UnrestrictedStructuralSubscription(StructuralSubscriptionTestBase):
128 RestrictedStructuralSubscriptionTestBase):
129 """
130 Tests suitable for a target that does not restrict structural
131 subscriptions.
132 """
133128
134 def test_structural_subscription_by_ordinary_user(self):129 def test_structural_subscription_by_ordinary_user(self):
135 # ordinary users can subscribe themselves130 # ordinary users can subscribe themselves
@@ -172,276 +167,198 @@
172 None)167 None)
173168
174169
175class FilteredStructuralSubscriptionTestBase:170class FilteredStructuralSubscriptionTestBase(StructuralSubscriptionTestBase):
176 """Tests for filtered structural subscriptions."""171 """Tests for filtered structural subscriptions."""
177172
178 layer = LaunchpadFunctionalLayer
179
180 def makeTarget(self):
181 raise NotImplementedError(self.makeTarget)
182
183 def makeBugTask(self):173 def makeBugTask(self):
184 return self.factory.makeBugTask(target=self.target)174 return self.factory.makeBugTask(target=self.target)
185175
186 def setUp(self):
187 super(FilteredStructuralSubscriptionTestBase, self).setUp()
188 self.ordinary_subscriber = self.factory.makePerson()
189 login_person(self.ordinary_subscriber)
190 self.target = self.makeTarget()
191 self.bugtask = self.makeBugTask()
192 self.bug = self.bugtask.bug
193 self.subscription = self.target.addSubscription(
194 self.ordinary_subscriber, self.ordinary_subscriber)
195 self.subscription.bug_notification_level = (
196 BugNotificationLevel.COMMENTS)
197
198 def assertSubscriptions(
199 self, expected_subscriptions, level=BugNotificationLevel.NOTHING):
200 observed_subscriptions = list(
201 self.target.getSubscriptionsForBugTask(self.bugtask, level))
202 self.assertEqual(expected_subscriptions, observed_subscriptions)
203
204 def test_getSubscriptionsForBugTask(self):176 def test_getSubscriptionsForBugTask(self):
205 # If no one has a filtered subscription for the given bug, the result177 # If no one has a filtered subscription for the given bug, the result
206 # of getSubscriptionsForBugTask() is the same as for178 # of getSubscriptionsForBugTask() is the same as for
207 # getSubscriptions().179 # getSubscriptions().
180 bugtask = self.makeBugTask()
208 subscriptions = self.target.getSubscriptions(181 subscriptions = self.target.getSubscriptions(
209 min_bug_notification_level=BugNotificationLevel.NOTHING)182 min_bug_notification_level=BugNotificationLevel.NOTHING)
210 self.assertSubscriptions(list(subscriptions))183 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
184 bugtask, BugNotificationLevel.NOTHING)
185 self.assertEqual(list(subscriptions), list(subscriptions_for_bugtask))
211186
212 def test_getSubscriptionsForBugTask_with_filter_on_status(self):187 def test_getSubscriptionsForBugTask_with_filter_on_status(self):
213 # If a status filter exists for a subscription, the result of188 # If a status filter exists for a subscription, the result of
214 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().189 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
190 bugtask = self.makeBugTask()
191
192 # Create a new subscription on self.target.
193 login_person(self.ordinary_subscriber)
194 subscription = self.target.addSubscription(
195 self.ordinary_subscriber, self.ordinary_subscriber)
196 subscription.bug_notification_level = BugNotificationLevel.COMMENTS
215197
216 # Without any filters the subscription is found.198 # Without any filters the subscription is found.
217 self.assertSubscriptions([self.subscription])199 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
200 bugtask, BugNotificationLevel.NOTHING)
201 self.assertEqual([subscription], list(subscriptions_for_bugtask))
218202
219 # Filter the subscription to bugs in the CONFIRMED state.203 # Filter the subscription to bugs in the CONFIRMED state.
220 subscription_filter = self.subscription.newBugFilter()204 subscription_filter = BugSubscriptionFilter()
205 subscription_filter.structural_subscription = subscription
221 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]206 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
222207
223 # With the filter the subscription is not found.208 # With the filter the subscription is not found.
224 self.assertSubscriptions([])209 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
210 bugtask, BugNotificationLevel.NOTHING)
211 self.assertEqual([], list(subscriptions_for_bugtask))
225212
226 # If the filter is adjusted, the subscription is found again.213 # If the filter is adjusted, the subscription is found again.
227 subscription_filter.statuses = [self.bugtask.status]214 subscription_filter.statuses = [bugtask.status]
228 self.assertSubscriptions([self.subscription])215 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
216 bugtask, BugNotificationLevel.NOTHING)
217 self.assertEqual([subscription], list(subscriptions_for_bugtask))
229218
230 def test_getSubscriptionsForBugTask_with_filter_on_importance(self):219 def test_getSubscriptionsForBugTask_with_filter_on_importance(self):
231 # If an importance filter exists for a subscription, the result of220 # If an importance filter exists for a subscription, the result of
232 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().221 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
222 bugtask = self.makeBugTask()
223
224 # Create a new subscription on self.target.
225 login_person(self.ordinary_subscriber)
226 subscription = self.target.addSubscription(
227 self.ordinary_subscriber, self.ordinary_subscriber)
228 subscription.bug_notification_level = BugNotificationLevel.COMMENTS
233229
234 # Without any filters the subscription is found.230 # Without any filters the subscription is found.
235 self.assertSubscriptions([self.subscription])231 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
232 bugtask, BugNotificationLevel.NOTHING)
233 self.assertEqual([subscription], list(subscriptions_for_bugtask))
236234
237 # Filter the subscription to bugs in the CRITICAL state.235 # Filter the subscription to bugs in the CRITICAL state.
238 subscription_filter = self.subscription.newBugFilter()236 subscription_filter = BugSubscriptionFilter()
237 subscription_filter.structural_subscription = subscription
239 subscription_filter.importances = [BugTaskImportance.CRITICAL]238 subscription_filter.importances = [BugTaskImportance.CRITICAL]
240239
241 # With the filter the subscription is not found.240 # With the filter the subscription is not found.
242 self.assertSubscriptions([])241 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
242 bugtask, BugNotificationLevel.NOTHING)
243 self.assertEqual([], list(subscriptions_for_bugtask))
243244
244 # If the filter is adjusted, the subscription is found again.245 # If the filter is adjusted, the subscription is found again.
245 subscription_filter.importances = [self.bugtask.importance]246 subscription_filter.importances = [bugtask.importance]
246 self.assertSubscriptions([self.subscription])247 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
248 bugtask, BugNotificationLevel.NOTHING)
249 self.assertEqual([subscription], list(subscriptions_for_bugtask))
247250
248 def test_getSubscriptionsForBugTask_with_filter_on_level(self):251 def test_getSubscriptionsForBugTask_with_filter_on_level(self):
249 # All structural subscriptions have a level for bug notifications252 # All structural subscriptions have a level for bug notifications
250 # which getSubscriptionsForBugTask() observes.253 # which getSubscriptionsForBugTask() observes.
254 bugtask = self.makeBugTask()
251255
252 # Adjust the subscription level to METADATA.256 # Create a new METADATA level subscription on self.target.
253 self.subscription.bug_notification_level = (257 login_person(self.ordinary_subscriber)
254 BugNotificationLevel.METADATA)258 subscription = self.target.addSubscription(
259 self.ordinary_subscriber, self.ordinary_subscriber)
260 subscription.bug_notification_level = BugNotificationLevel.METADATA
255261
256 # The subscription is found when looking for NOTHING or above.262 # The subscription is found when looking for NOTHING or above.
257 self.assertSubscriptions(263 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
258 [self.subscription], BugNotificationLevel.NOTHING)264 bugtask, BugNotificationLevel.NOTHING)
265 self.assertEqual([subscription], list(subscriptions_for_bugtask))
259 # The subscription is found when looking for METADATA or above.266 # The subscription is found when looking for METADATA or above.
260 self.assertSubscriptions(267 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
261 [self.subscription], BugNotificationLevel.METADATA)268 bugtask, BugNotificationLevel.METADATA)
269 self.assertEqual([subscription], list(subscriptions_for_bugtask))
262 # The subscription is not found when looking for COMMENTS or above.270 # The subscription is not found when looking for COMMENTS or above.
263 self.assertSubscriptions(271 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
264 [], BugNotificationLevel.COMMENTS)272 bugtask, BugNotificationLevel.COMMENTS)
273 self.assertEqual([], list(subscriptions_for_bugtask))
265274
266 def test_getSubscriptionsForBugTask_with_filter_include_any_tags(self):275 def test_getSubscriptionsForBugTask_with_filter_include_any_tags(self):
267 # If a subscription filter has include_any_tags, a bug with one or276 # If a subscription filter has include_any_tags, a bug with one or
268 # more tags is matched.277 # more tags is matched.
278 bugtask = self.makeBugTask()
269279
270 subscription_filter = self.subscription.newBugFilter()280 # Create a new subscription on self.target.
281 login_person(self.ordinary_subscriber)
282 subscription = self.target.addSubscription(
283 self.ordinary_subscriber, self.ordinary_subscriber)
284 subscription.bug_notification_level = BugNotificationLevel.COMMENTS
285 subscription_filter = subscription.newBugFilter()
271 subscription_filter.include_any_tags = True286 subscription_filter.include_any_tags = True
272287
273 # Without any tags the subscription is not found.288 # Without any tags the subscription is not found.
274 self.assertSubscriptions([])289 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
290 bugtask, BugNotificationLevel.NOTHING)
291 self.assertEqual([], list(subscriptions_for_bugtask))
275292
276 # With any tag the subscription is found.293 # With any tag the subscription is found.
277 self.bug.tags = ["foo"]294 bugtask.bug.tags = ["foo"]
278 self.assertSubscriptions([self.subscription])295 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
296 bugtask, BugNotificationLevel.NOTHING)
297 self.assertEqual([subscription], list(subscriptions_for_bugtask))
279298
280 def test_getSubscriptionsForBugTask_with_filter_exclude_any_tags(self):299 def test_getSubscriptionsForBugTask_with_filter_exclude_any_tags(self):
281 # If a subscription filter has exclude_any_tags, only bugs with no300 # If a subscription filter has exclude_any_tags, only bugs with no
282 # tags are matched.301 # tags are matched.
302 bugtask = self.makeBugTask()
283303
284 subscription_filter = self.subscription.newBugFilter()304 # Create a new subscription on self.target.
305 login_person(self.ordinary_subscriber)
306 subscription = self.target.addSubscription(
307 self.ordinary_subscriber, self.ordinary_subscriber)
308 subscription.bug_notification_level = BugNotificationLevel.COMMENTS
309 subscription_filter = subscription.newBugFilter()
285 subscription_filter.exclude_any_tags = True310 subscription_filter.exclude_any_tags = True
286311
287 # Without any tags the subscription is found.312 # Without any tags the subscription is found.
288 self.assertSubscriptions([self.subscription])313 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
314 bugtask, BugNotificationLevel.NOTHING)
315 self.assertEqual([subscription], list(subscriptions_for_bugtask))
289316
290 # With any tag the subscription is not found.317 # With any tag the subscription is not found.
291 self.bug.tags = ["foo"]318 bugtask.bug.tags = ["foo"]
292 self.assertSubscriptions([])319 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
293320 bugtask, BugNotificationLevel.NOTHING)
294 def test_getSubscriptionsForBugTask_with_filter_for_any_tag(self):321 self.assertEqual([], list(subscriptions_for_bugtask))
295 # If a subscription filter specifies that any of one or more specific
296 # tags must be present, bugs with any of those tags are matched.
297
298 # Looking for either the "foo" or the "bar" tag.
299 subscription_filter = self.subscription.newBugFilter()
300 subscription_filter.tags = [u"foo", u"bar"]
301 subscription_filter.find_all_tags = False
302
303 # Without either tag the subscription is not found.
304 self.assertSubscriptions([])
305
306 # With either tag the subscription is found.
307 self.bug.tags = ["bar", "baz"]
308 self.assertSubscriptions([self.subscription])
309
310 def test_getSubscriptionsForBugTask_with_filter_for_all_tags(self):
311 # If a subscription filter specifies that all of one or more specific
312 # tags must be present, bugs with all of those tags are matched.
313
314 # Looking for both the "foo" and the "bar" tag.
315 subscription_filter = self.subscription.newBugFilter()
316 subscription_filter.tags = [u"foo", u"bar"]
317 subscription_filter.find_all_tags = True
318
319 # Without either tag the subscription is not found.
320 self.assertSubscriptions([])
321
322 # Without only one of the required tags the subscription is not found.
323 self.bug.tags = ["foo"]
324 self.assertSubscriptions([])
325
326 # With both required tags the subscription is found.
327 self.bug.tags = ["foo", "bar"]
328 self.assertSubscriptions([self.subscription])
329
330 def test_getSubscriptionsForBugTask_with_filter_for_not_any_tag(self):
331 # If a subscription filter specifies that any of one or more specific
332 # tags must not be present, bugs without any of those tags are
333 # matched.
334
335 # Looking to exclude the "foo" or "bar" tags.
336 subscription_filter = self.subscription.newBugFilter()
337 subscription_filter.tags = [u"-foo", u"-bar"]
338 subscription_filter.find_all_tags = False
339
340 # Without either tag the subscription is found.
341 self.assertSubscriptions([self.subscription])
342
343 # With either tag the subscription is no longer found.
344 self.bug.tags = ["foo"]
345 self.assertSubscriptions([])
346
347 def test_getSubscriptionsForBugTask_with_filter_for_not_all_tags(self):
348 # If a subscription filter specifies that all of one or more specific
349 # tags must not be present, bugs without all of those tags are
350 # matched.
351
352 # Looking to exclude the "foo" and "bar" tags.
353 subscription_filter = self.subscription.newBugFilter()
354 subscription_filter.tags = [u"-foo", u"-bar"]
355 subscription_filter.find_all_tags = True
356
357 # Without either tag the subscription is found.
358 self.assertSubscriptions([self.subscription])
359
360 # With only one of the excluded tags the subscription is found.
361 self.bug.tags = ["foo"]
362 self.assertSubscriptions([self.subscription])
363
364 # With both tags the subscription is no longer found.
365 self.bug.tags = ["foo", "bar"]
366 self.assertSubscriptions([])
367322
368 def test_getSubscriptionsForBugTask_with_multiple_filters(self):323 def test_getSubscriptionsForBugTask_with_multiple_filters(self):
369 # If multiple filters exist for a subscription, all filters must324 # If multiple filters exist for a subscription, all filters must
370 # match.325 # match.
326 bugtask = self.makeBugTask()
371327
372 # Add the "foo" tag to the bug.328 # Create a new subscription on self.target.
373 self.bug.tags = ["foo"]329 login_person(self.ordinary_subscriber)
330 subscription = self.target.addSubscription(
331 self.ordinary_subscriber, self.ordinary_subscriber)
332 subscription.bug_notification_level = BugNotificationLevel.COMMENTS
374333
375 # Filter the subscription to bugs in the CRITICAL state.334 # Filter the subscription to bugs in the CRITICAL state.
376 subscription_filter = self.subscription.newBugFilter()335 subscription_filter = BugSubscriptionFilter()
336 subscription_filter.structural_subscription = subscription
377 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]337 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
378 subscription_filter.importances = [BugTaskImportance.CRITICAL]338 subscription_filter.importances = [BugTaskImportance.CRITICAL]
379339
380 # With the filter the subscription is not found.340 # With the filter the subscription is not found.
381 self.assertSubscriptions([])341 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
342 bugtask, BugNotificationLevel.NOTHING)
343 self.assertEqual([], list(subscriptions_for_bugtask))
382344
383 # If the filter is adjusted to match status but not importance, the345 # If the filter is adjusted to match status but not importance, the
384 # subscription is still not found.346 # subscription is still not found.
385 subscription_filter.statuses = [self.bugtask.status]347 subscription_filter.statuses = [bugtask.status]
386 self.assertSubscriptions([])348 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
349 bugtask, BugNotificationLevel.NOTHING)
350 self.assertEqual([], list(subscriptions_for_bugtask))
387351
388 # If the filter is adjusted to also match importance, the subscription352 # If the filter is adjusted to also match importance, the subscription
389 # is found again.353 # is found again.
390 subscription_filter.importances = [self.bugtask.importance]354 subscription_filter.importances = [bugtask.importance]
391 self.assertSubscriptions([self.subscription])355 subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
392356 bugtask, BugNotificationLevel.NOTHING)
393 # If the filter is given some tag criteria, the subscription is not357 self.assertEqual([subscription], list(subscriptions_for_bugtask))
394 # found.
395 subscription_filter.tags = [u"-foo", u"bar", u"baz"]
396 subscription_filter.find_all_tags = False
397 self.assertSubscriptions([])
398
399 # After removing the "foo" tag and adding the "bar" tag, the
400 # subscription is found.
401 self.bug.tags = ["bar"]
402 self.assertSubscriptions([self.subscription])
403
404 # Requiring that all tag criteria are fulfilled causes the
405 # subscription to no longer be found.
406 subscription_filter.find_all_tags = True
407 self.assertSubscriptions([])
408
409 # After adding the "baz" tag, the subscription is found again.
410 self.bug.tags = ["bar", "baz"]
411 self.assertSubscriptions([self.subscription])
412
413 def test_getSubscriptionsForBugTask_any_filter_is_a_match(self):
414 # If a subscription has multiple filters, the subscription is selected
415 # when any filter is found to match. Put another way, the filters are
416 # ORed together.
417 subscription_filter1 = self.subscription.newBugFilter()
418 subscription_filter1.statuses = [BugTaskStatus.CONFIRMED]
419 subscription_filter2 = self.subscription.newBugFilter()
420 subscription_filter2.tags = [u"foo"]
421
422 # With the filter the subscription is not found.
423 self.assertSubscriptions([])
424
425 # If the bugtask is adjusted to match the criteria of the first filter
426 # but not those of the second, the subscription is found.
427 self.bugtask.transitionToStatus(
428 BugTaskStatus.CONFIRMED, self.ordinary_subscriber)
429 self.assertSubscriptions([self.subscription])
430
431 # If the filter is adjusted to also match the criteria of the second
432 # filter, the subscription is still found.
433 self.bugtask.bug.tags = [u"foo"]
434 self.assertSubscriptions([self.subscription])
435
436 # If the bugtask is adjusted to no longer match the criteria of the
437 # first filter, the subscription is still found.
438 self.bugtask.transitionToStatus(
439 BugTaskStatus.INPROGRESS, self.ordinary_subscriber)
440 self.assertSubscriptions([self.subscription])
441358
442359
443class TestStructuralSubscriptionForDistro(360class TestStructuralSubscriptionForDistro(
444 RestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):361 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
445362
446 layer = LaunchpadFunctionalLayer363 layer = LaunchpadFunctionalLayer
447364
@@ -504,15 +421,10 @@
504 StructuralSubscription)421 StructuralSubscription)
505422
506423
507class TestStructuralSubscriptionFiltersForDistro(
508 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
509
510 def makeTarget(self):
511 return self.factory.makeDistribution()
512
513
514class TestStructuralSubscriptionForProduct(424class TestStructuralSubscriptionForProduct(
515 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):425 UnrestrictedStructuralSubscription,
426 FilteredStructuralSubscriptionTestBase,
427 TestCaseWithFactory):
516428
517 layer = LaunchpadFunctionalLayer429 layer = LaunchpadFunctionalLayer
518430
@@ -521,15 +433,10 @@
521 self.target = self.factory.makeProduct()433 self.target = self.factory.makeProduct()
522434
523435
524class TestStructuralSubscriptionFiltersForProduct(
525 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
526
527 def makeTarget(self):
528 return self.factory.makeProduct()
529
530
531class TestStructuralSubscriptionForDistroSourcePackage(436class TestStructuralSubscriptionForDistroSourcePackage(
532 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):437 UnrestrictedStructuralSubscription,
438 FilteredStructuralSubscriptionTestBase,
439 TestCaseWithFactory):
533440
534 layer = LaunchpadFunctionalLayer441 layer = LaunchpadFunctionalLayer
535442
@@ -539,15 +446,10 @@
539 self.target = ProxyFactory(self.target)446 self.target = ProxyFactory(self.target)
540447
541448
542class TestStructuralSubscriptionFiltersForDistroSourcePackage(
543 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
544
545 def makeTarget(self):
546 return self.factory.makeDistributionSourcePackage()
547
548
549class TestStructuralSubscriptionForMilestone(449class TestStructuralSubscriptionForMilestone(
550 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):450 UnrestrictedStructuralSubscription,
451 FilteredStructuralSubscriptionTestBase,
452 TestCaseWithFactory):
551453
552 layer = LaunchpadFunctionalLayer454 layer = LaunchpadFunctionalLayer
553455
@@ -556,19 +458,15 @@
556 self.target = self.factory.makeMilestone()458 self.target = self.factory.makeMilestone()
557 self.target = ProxyFactory(self.target)459 self.target = ProxyFactory(self.target)
558460
559
560class TestStructuralSubscriptionFiltersForMilestone(
561 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
562
563 def makeTarget(self):
564 return self.factory.makeMilestone()
565
566 def makeBugTask(self):461 def makeBugTask(self):
462 # XXX Should test with target *and* series_target.
567 return self.factory.makeBugTask(target=self.target.series_target)463 return self.factory.makeBugTask(target=self.target.series_target)
568464
569465
570class TestStructuralSubscriptionForDistroSeries(466class TestStructuralSubscriptionForDistroSeries(
571 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):467 UnrestrictedStructuralSubscription,
468 FilteredStructuralSubscriptionTestBase,
469 TestCaseWithFactory):
572470
573 layer = LaunchpadFunctionalLayer471 layer = LaunchpadFunctionalLayer
574472
@@ -578,15 +476,10 @@
578 self.target = ProxyFactory(self.target)476 self.target = ProxyFactory(self.target)
579477
580478
581class TestStructuralSubscriptionFiltersForDistroSeries(
582 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
583
584 def makeTarget(self):
585 return self.factory.makeDistroSeries()
586
587
588class TestStructuralSubscriptionForProjectGroup(479class TestStructuralSubscriptionForProjectGroup(
589 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):480 UnrestrictedStructuralSubscription,
481 FilteredStructuralSubscriptionTestBase,
482 TestCaseWithFactory):
590483
591 layer = LaunchpadFunctionalLayer484 layer = LaunchpadFunctionalLayer
592485
@@ -595,20 +488,15 @@
595 self.target = self.factory.makeProject()488 self.target = self.factory.makeProject()
596 self.target = ProxyFactory(self.target)489 self.target = ProxyFactory(self.target)
597490
598
599class TestStructuralSubscriptionFiltersForProjectGroup(
600 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
601
602 def makeTarget(self):
603 return self.factory.makeProject()
604
605 def makeBugTask(self):491 def makeBugTask(self):
606 return self.factory.makeBugTask(492 return self.factory.makeBugTask(
607 target=self.factory.makeProduct(project=self.target))493 target=self.factory.makeProduct(project=self.target))
608494
609495
610class TestStructuralSubscriptionForProductSeries(496class TestStructuralSubscriptionForProductSeries(
611 UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):497 UnrestrictedStructuralSubscription,
498 FilteredStructuralSubscriptionTestBase,
499 TestCaseWithFactory):
612500
613 layer = LaunchpadFunctionalLayer501 layer = LaunchpadFunctionalLayer
614502
@@ -618,13 +506,6 @@
618 self.target = ProxyFactory(self.target)506 self.target = ProxyFactory(self.target)
619507
620508
621class TestStructuralSubscriptionFiltersForProductSeries(
622 FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
623
624 def makeTarget(self):
625 return self.factory.makeProductSeries()
626
627
628class TestStructuralSubscriptionTargetHelper(TestCaseWithFactory):509class TestStructuralSubscriptionTargetHelper(TestCaseWithFactory):
629 """Tests for implementations of `IStructuralSubscriptionTargetHelper`."""510 """Tests for implementations of `IStructuralSubscriptionTargetHelper`."""
630511