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
1=== modified file 'lib/lp/registry/model/structuralsubscription.py'
2--- lib/lp/registry/model/structuralsubscription.py 2010-11-19 14:33:01 +0000
3+++ lib/lp/registry/model/structuralsubscription.py 2010-11-25 21:03:11 +0000
4@@ -2,26 +2,14 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 __metaclass__ = type
8-__all__ = [
9- 'StructuralSubscription',
10- 'StructuralSubscriptionTargetMixin',
11- ]
12+__all__ = ['StructuralSubscription',
13+ 'StructuralSubscriptionTargetMixin']
14
15 from sqlobject import ForeignKey
16 from storm.expr import (
17- Alias,
18 And,
19- CompoundOper,
20- Except,
21- In,
22- Intersect,
23 LeftJoin,
24- NamedFunc,
25- Not,
26 Or,
27- Select,
28- SQL,
29- Union,
30 )
31 from storm.store import Store
32 from zope.component import (
33@@ -46,7 +34,6 @@
34 from lp.bugs.model.bugsubscriptionfilterstatus import (
35 BugSubscriptionFilterStatus,
36 )
37-from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag
38 from lp.registry.enum import BugNotificationLevel
39 from lp.registry.errors import (
40 DeleteSubscriptionError,
41@@ -484,242 +471,50 @@
42
43 def getSubscriptionsForBugTask(self, bugtask, level):
44 """See `IStructuralSubscriptionTarget`."""
45- set_builder = BugFilterSetBuilder(
46- bugtask, level, self.__helper.join)
47- return Store.of(self.__helper.pillar).find(
48- StructuralSubscription, In(
49- StructuralSubscription.id,
50- set_builder.subscriptions))
51-
52-
53-class ArrayAgg(NamedFunc):
54- __slots__ = ()
55- name = "ARRAY_AGG"
56-
57-
58-class ArrayContains(CompoundOper):
59- __slots__ = ()
60- oper = "@>"
61-
62-
63-class BugFilterSetBuilder:
64- """A convenience class to build queries for getSubscriptionsForBugTask."""
65-
66- def __init__(self, bugtask, level, join_condition):
67- """Initialize a new set builder for bug filters.
68-
69- :param bugtask: The `IBugTask` to match against.
70- :param level: A member of `BugNotificationLevel`.
71- :param join_condition: A condition for selecting structural
72- subscriptions. Generally this should limit the subscriptions to a
73- particular target (i.e. project or distribution).
74- """
75- self.status = bugtask.status
76- self.importance = bugtask.importance
77- # The list() gets around some weirdness with security proxies; Storm
78- # does not know how to compile an expression with a proxied list.
79- self.tags = list(bugtask.bug.tags)
80- # Set up common conditions.
81- self.base_conditions = And(
82+ origin = [
83+ StructuralSubscription,
84+ LeftJoin(
85+ BugSubscriptionFilter,
86+ BugSubscriptionFilter.structural_subscription_id == (
87+ StructuralSubscription.id)),
88+ LeftJoin(
89+ BugSubscriptionFilterStatus,
90+ BugSubscriptionFilterStatus.filter_id == (
91+ BugSubscriptionFilter.id)),
92+ LeftJoin(
93+ BugSubscriptionFilterImportance,
94+ BugSubscriptionFilterImportance.filter_id == (
95+ BugSubscriptionFilter.id)),
96+ ]
97+
98+ if len(bugtask.bug.tags) == 0:
99+ tag_conditions = [
100+ BugSubscriptionFilter.include_any_tags == False,
101+ ]
102+ else:
103+ tag_conditions = [
104+ BugSubscriptionFilter.exclude_any_tags == False,
105+ ]
106+
107+ conditions = [
108 StructuralSubscription.bug_notification_level >= level,
109- join_condition)
110- # Set up common filter conditions.
111- if len(self.tags) == 0:
112- self.filter_conditions = And(
113- # When the bug has no tags, filters with include_any_tags set
114- # can never match.
115- Not(BugSubscriptionFilter.include_any_tags),
116- self.base_conditions)
117- else:
118- self.filter_conditions = And(
119- # When the bug has tags, filters with exclude_any_tags set can
120- # never match.
121- Not(BugSubscriptionFilter.exclude_any_tags),
122- self.base_conditions)
123-
124- @property
125- def subscriptions_without_filters(self):
126- """Subscriptions without filters."""
127- return Select(
128- StructuralSubscription.id,
129- tables=(
130- StructuralSubscription,
131- LeftJoin(
132- BugSubscriptionFilter,
133- BugSubscriptionFilter.structural_subscription_id == (
134- StructuralSubscription.id))),
135- where=And(
136+ Or(
137+ # There's no filter or ...
138 BugSubscriptionFilter.id == None,
139- self.base_conditions))
140-
141- def _filters_matching_x(self, join, where_condition, **extra):
142- """Return an expression yielding `(subscription_id, filter_id)` rows.
143-
144- The expressions returned by this function are used in set (union,
145- intersect, except) operations at the *filter* level. However, the
146- interesting result of these set operations is the structural
147- subscription, hence both columns are included in the expressions
148- generated. Since a structural subscription can have zero or more
149- filters, and a filter can never be associated with more than one
150- subscription, the set operations are unaffected.
151- """
152- return Select(
153- columns=(
154- # Alias this column so it can be selected in
155- # subscriptions_matching.
156- Alias(
157- BugSubscriptionFilter.structural_subscription_id,
158- "structural_subscription_id"),
159- BugSubscriptionFilter.id),
160- tables=(
161- StructuralSubscription, BugSubscriptionFilter, join),
162- where=And(
163- BugSubscriptionFilter.structural_subscription_id == (
164- StructuralSubscription.id),
165- self.filter_conditions,
166- where_condition),
167- **extra)
168-
169- @property
170- def filters_matching_status(self):
171- """Filters with the given bugtask's status."""
172- join = LeftJoin(
173- BugSubscriptionFilterStatus,
174- BugSubscriptionFilterStatus.filter_id == (
175- BugSubscriptionFilter.id))
176- condition = Or(
177- BugSubscriptionFilterStatus.id == None,
178- BugSubscriptionFilterStatus.status == self.status)
179- return self._filters_matching_x(join, condition)
180-
181- @property
182- def filters_matching_importance(self):
183- """Filters with the given bugtask's importance."""
184- join = LeftJoin(
185- BugSubscriptionFilterImportance,
186- BugSubscriptionFilterImportance.filter_id == (
187- BugSubscriptionFilter.id))
188- condition = Or(
189- BugSubscriptionFilterImportance.id == None,
190- BugSubscriptionFilterImportance.importance == self.importance)
191- return self._filters_matching_x(join, condition)
192-
193- @property
194- def filters_without_include_tags(self):
195- """Filters with no tags required."""
196- join = LeftJoin(
197- BugSubscriptionFilterTag,
198- And(BugSubscriptionFilterTag.filter_id == (
199- BugSubscriptionFilter.id),
200- BugSubscriptionFilterTag.include))
201- return self._filters_matching_x(
202- join, BugSubscriptionFilterTag.id == None)
203-
204- @property
205- def filters_matching_any_include_tags(self):
206- """Filters including any of the bug's tags."""
207- condition = And(
208- BugSubscriptionFilterTag.filter_id == (
209- BugSubscriptionFilter.id),
210- BugSubscriptionFilterTag.include,
211- Not(BugSubscriptionFilter.find_all_tags),
212- In(BugSubscriptionFilterTag.tag, self.tags))
213- return self._filters_matching_x(
214- BugSubscriptionFilterTag, condition)
215-
216- @property
217- def filters_matching_any_exclude_tags(self):
218- """Filters excluding any of the bug's tags."""
219- condition = And(
220- BugSubscriptionFilterTag.filter_id == (
221- BugSubscriptionFilter.id),
222- Not(BugSubscriptionFilterTag.include),
223- Not(BugSubscriptionFilter.find_all_tags),
224- In(BugSubscriptionFilterTag.tag, self.tags))
225- return self._filters_matching_x(
226- BugSubscriptionFilterTag, condition)
227-
228- def _filters_matching_all_x_tags(self, where_condition):
229- """Return an expression yielding `(subscription_id, filter_id)` rows.
230-
231- This joins to `BugSubscriptionFilterTag` and calls up to
232- `_filters_matching_x`, and groups by filter. Conditions are added to
233- ensure that all rows in each group are a subset of the bug's tags.
234- """
235- tags_array = "ARRAY[%s]::TEXT[]" % ",".join(
236- quote(tag) for tag in self.tags)
237- return self._filters_matching_x(
238- BugSubscriptionFilterTag,
239- And(
240- BugSubscriptionFilterTag.filter_id == (
241- BugSubscriptionFilter.id),
242- BugSubscriptionFilter.find_all_tags,
243- self.filter_conditions,
244- where_condition),
245- group_by=(
246- BugSubscriptionFilter.structural_subscription_id,
247- BugSubscriptionFilter.id),
248- having=ArrayContains(
249- SQL(tags_array), ArrayAgg(
250- BugSubscriptionFilterTag.tag)))
251-
252- @property
253- def filters_matching_all_include_tags(self):
254- """Filters including the bug's tags."""
255- return self._filters_matching_all_x_tags(
256- BugSubscriptionFilterTag.include)
257-
258- @property
259- def filters_matching_all_exclude_tags(self):
260- """Filters excluding the bug's tags."""
261- return self._filters_matching_all_x_tags(
262- Not(BugSubscriptionFilterTag.include))
263-
264- @property
265- def filters_matching_include_tags(self):
266- """Filters with tag filters including the bug."""
267- return Union(
268- self.filters_matching_any_include_tags,
269- self.filters_matching_all_include_tags)
270-
271- @property
272- def filters_matching_exclude_tags(self):
273- """Filters with tag filters excluding the bug."""
274- return Union(
275- self.filters_matching_any_exclude_tags,
276- self.filters_matching_all_exclude_tags)
277-
278- @property
279- def filters_matching_tags(self):
280- """Filters with tag filters matching the bug."""
281- if len(self.tags) == 0:
282- # The filter's required tags must be an empty set. The filter's
283- # excluded tags can be anything so no condition is needed.
284- return self.filters_without_include_tags
285- else:
286- return Except(
287- Union(self.filters_without_include_tags,
288- self.filters_matching_include_tags),
289- self.filters_matching_exclude_tags)
290-
291- @property
292- def filters_matching(self):
293- """Filters matching the bug."""
294- return Intersect(
295- self.filters_matching_status,
296- self.filters_matching_importance,
297- self.filters_matching_tags)
298-
299- @property
300- def subscriptions_with_matching_filters(self):
301- """Subscriptions with one or more filters matching the bug."""
302- return Select(
303- # I don't know of a more Storm-like way of doing this.
304- SQL("filters_matching.structural_subscription_id"),
305- tables=Alias(self.filters_matching, "filters_matching"))
306-
307- @property
308- def subscriptions(self):
309- return Union(
310- self.subscriptions_without_filters,
311- self.subscriptions_with_matching_filters)
312+ # There is a filter and ...
313+ And(
314+ # There's no status filter, or there is a status filter
315+ # and and it matches.
316+ Or(BugSubscriptionFilterStatus.id == None,
317+ BugSubscriptionFilterStatus.status == bugtask.status),
318+ # There's no importance filter, or there is an importance
319+ # filter and it matches.
320+ Or(BugSubscriptionFilterImportance.id == None,
321+ BugSubscriptionFilterImportance.importance == (
322+ bugtask.importance)),
323+ # Any number of conditions relating to tags.
324+ *tag_conditions)),
325+ ]
326+
327+ return Store.of(self.__helper.pillar).using(*origin).find(
328+ StructuralSubscription, self.__helper.join, *conditions)
329
330=== modified file 'lib/lp/registry/tests/test_structuralsubscriptiontarget.py'
331--- lib/lp/registry/tests/test_structuralsubscriptiontarget.py 2010-11-18 21:29:44 +0000
332+++ lib/lp/registry/tests/test_structuralsubscriptiontarget.py 2010-11-25 21:03:11 +0000
333@@ -32,6 +32,7 @@
334 BugTaskImportance,
335 BugTaskStatus,
336 )
337+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
338 from lp.bugs.tests.test_bugtarget import bugtarget_filebug
339 from lp.registry.enum import BugNotificationLevel
340 from lp.registry.errors import (
341@@ -56,11 +57,10 @@
342 from lp.testing.matchers import Provides
343
344
345-class RestrictedStructuralSubscriptionTestBase:
346- """Tests suitable for a target that restricts structural subscriptions."""
347+class StructuralSubscriptionTestBase:
348
349 def setUp(self):
350- super(RestrictedStructuralSubscriptionTestBase, self).setUp()
351+ super(StructuralSubscriptionTestBase, self).setUp()
352 self.ordinary_subscriber = self.factory.makePerson()
353 self.bug_supervisor_subscriber = self.factory.makePerson()
354 self.team_owner = self.factory.makePerson()
355@@ -124,12 +124,7 @@
356 self.ordinary_subscriber, self.ordinary_subscriber)
357
358
359-class UnrestrictedStructuralSubscriptionTestBase(
360- RestrictedStructuralSubscriptionTestBase):
361- """
362- Tests suitable for a target that does not restrict structural
363- subscriptions.
364- """
365+class UnrestrictedStructuralSubscription(StructuralSubscriptionTestBase):
366
367 def test_structural_subscription_by_ordinary_user(self):
368 # ordinary users can subscribe themselves
369@@ -172,276 +167,198 @@
370 None)
371
372
373-class FilteredStructuralSubscriptionTestBase:
374+class FilteredStructuralSubscriptionTestBase(StructuralSubscriptionTestBase):
375 """Tests for filtered structural subscriptions."""
376
377- layer = LaunchpadFunctionalLayer
378-
379- def makeTarget(self):
380- raise NotImplementedError(self.makeTarget)
381-
382 def makeBugTask(self):
383 return self.factory.makeBugTask(target=self.target)
384
385- def setUp(self):
386- super(FilteredStructuralSubscriptionTestBase, self).setUp()
387- self.ordinary_subscriber = self.factory.makePerson()
388- login_person(self.ordinary_subscriber)
389- self.target = self.makeTarget()
390- self.bugtask = self.makeBugTask()
391- self.bug = self.bugtask.bug
392- self.subscription = self.target.addSubscription(
393- self.ordinary_subscriber, self.ordinary_subscriber)
394- self.subscription.bug_notification_level = (
395- BugNotificationLevel.COMMENTS)
396-
397- def assertSubscriptions(
398- self, expected_subscriptions, level=BugNotificationLevel.NOTHING):
399- observed_subscriptions = list(
400- self.target.getSubscriptionsForBugTask(self.bugtask, level))
401- self.assertEqual(expected_subscriptions, observed_subscriptions)
402-
403 def test_getSubscriptionsForBugTask(self):
404 # If no one has a filtered subscription for the given bug, the result
405 # of getSubscriptionsForBugTask() is the same as for
406 # getSubscriptions().
407+ bugtask = self.makeBugTask()
408 subscriptions = self.target.getSubscriptions(
409 min_bug_notification_level=BugNotificationLevel.NOTHING)
410- self.assertSubscriptions(list(subscriptions))
411+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
412+ bugtask, BugNotificationLevel.NOTHING)
413+ self.assertEqual(list(subscriptions), list(subscriptions_for_bugtask))
414
415 def test_getSubscriptionsForBugTask_with_filter_on_status(self):
416 # If a status filter exists for a subscription, the result of
417 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
418+ bugtask = self.makeBugTask()
419+
420+ # Create a new subscription on self.target.
421+ login_person(self.ordinary_subscriber)
422+ subscription = self.target.addSubscription(
423+ self.ordinary_subscriber, self.ordinary_subscriber)
424+ subscription.bug_notification_level = BugNotificationLevel.COMMENTS
425
426 # Without any filters the subscription is found.
427- self.assertSubscriptions([self.subscription])
428+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
429+ bugtask, BugNotificationLevel.NOTHING)
430+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
431
432 # Filter the subscription to bugs in the CONFIRMED state.
433- subscription_filter = self.subscription.newBugFilter()
434+ subscription_filter = BugSubscriptionFilter()
435+ subscription_filter.structural_subscription = subscription
436 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
437
438 # With the filter the subscription is not found.
439- self.assertSubscriptions([])
440+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
441+ bugtask, BugNotificationLevel.NOTHING)
442+ self.assertEqual([], list(subscriptions_for_bugtask))
443
444 # If the filter is adjusted, the subscription is found again.
445- subscription_filter.statuses = [self.bugtask.status]
446- self.assertSubscriptions([self.subscription])
447+ subscription_filter.statuses = [bugtask.status]
448+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
449+ bugtask, BugNotificationLevel.NOTHING)
450+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
451
452 def test_getSubscriptionsForBugTask_with_filter_on_importance(self):
453 # If an importance filter exists for a subscription, the result of
454 # getSubscriptionsForBugTask() may be a subset of getSubscriptions().
455+ bugtask = self.makeBugTask()
456+
457+ # Create a new subscription on self.target.
458+ login_person(self.ordinary_subscriber)
459+ subscription = self.target.addSubscription(
460+ self.ordinary_subscriber, self.ordinary_subscriber)
461+ subscription.bug_notification_level = BugNotificationLevel.COMMENTS
462
463 # Without any filters the subscription is found.
464- self.assertSubscriptions([self.subscription])
465+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
466+ bugtask, BugNotificationLevel.NOTHING)
467+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
468
469 # Filter the subscription to bugs in the CRITICAL state.
470- subscription_filter = self.subscription.newBugFilter()
471+ subscription_filter = BugSubscriptionFilter()
472+ subscription_filter.structural_subscription = subscription
473 subscription_filter.importances = [BugTaskImportance.CRITICAL]
474
475 # With the filter the subscription is not found.
476- self.assertSubscriptions([])
477+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
478+ bugtask, BugNotificationLevel.NOTHING)
479+ self.assertEqual([], list(subscriptions_for_bugtask))
480
481 # If the filter is adjusted, the subscription is found again.
482- subscription_filter.importances = [self.bugtask.importance]
483- self.assertSubscriptions([self.subscription])
484+ subscription_filter.importances = [bugtask.importance]
485+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
486+ bugtask, BugNotificationLevel.NOTHING)
487+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
488
489 def test_getSubscriptionsForBugTask_with_filter_on_level(self):
490 # All structural subscriptions have a level for bug notifications
491 # which getSubscriptionsForBugTask() observes.
492+ bugtask = self.makeBugTask()
493
494- # Adjust the subscription level to METADATA.
495- self.subscription.bug_notification_level = (
496- BugNotificationLevel.METADATA)
497+ # Create a new METADATA level subscription on self.target.
498+ login_person(self.ordinary_subscriber)
499+ subscription = self.target.addSubscription(
500+ self.ordinary_subscriber, self.ordinary_subscriber)
501+ subscription.bug_notification_level = BugNotificationLevel.METADATA
502
503 # The subscription is found when looking for NOTHING or above.
504- self.assertSubscriptions(
505- [self.subscription], BugNotificationLevel.NOTHING)
506+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
507+ bugtask, BugNotificationLevel.NOTHING)
508+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
509 # The subscription is found when looking for METADATA or above.
510- self.assertSubscriptions(
511- [self.subscription], BugNotificationLevel.METADATA)
512+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
513+ bugtask, BugNotificationLevel.METADATA)
514+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
515 # The subscription is not found when looking for COMMENTS or above.
516- self.assertSubscriptions(
517- [], BugNotificationLevel.COMMENTS)
518+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
519+ bugtask, BugNotificationLevel.COMMENTS)
520+ self.assertEqual([], list(subscriptions_for_bugtask))
521
522 def test_getSubscriptionsForBugTask_with_filter_include_any_tags(self):
523 # If a subscription filter has include_any_tags, a bug with one or
524 # more tags is matched.
525+ bugtask = self.makeBugTask()
526
527- subscription_filter = self.subscription.newBugFilter()
528+ # Create a new subscription on self.target.
529+ login_person(self.ordinary_subscriber)
530+ subscription = self.target.addSubscription(
531+ self.ordinary_subscriber, self.ordinary_subscriber)
532+ subscription.bug_notification_level = BugNotificationLevel.COMMENTS
533+ subscription_filter = subscription.newBugFilter()
534 subscription_filter.include_any_tags = True
535
536 # Without any tags the subscription is not found.
537- self.assertSubscriptions([])
538+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
539+ bugtask, BugNotificationLevel.NOTHING)
540+ self.assertEqual([], list(subscriptions_for_bugtask))
541
542 # With any tag the subscription is found.
543- self.bug.tags = ["foo"]
544- self.assertSubscriptions([self.subscription])
545+ bugtask.bug.tags = ["foo"]
546+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
547+ bugtask, BugNotificationLevel.NOTHING)
548+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
549
550 def test_getSubscriptionsForBugTask_with_filter_exclude_any_tags(self):
551 # If a subscription filter has exclude_any_tags, only bugs with no
552 # tags are matched.
553+ bugtask = self.makeBugTask()
554
555- subscription_filter = self.subscription.newBugFilter()
556+ # Create a new subscription on self.target.
557+ login_person(self.ordinary_subscriber)
558+ subscription = self.target.addSubscription(
559+ self.ordinary_subscriber, self.ordinary_subscriber)
560+ subscription.bug_notification_level = BugNotificationLevel.COMMENTS
561+ subscription_filter = subscription.newBugFilter()
562 subscription_filter.exclude_any_tags = True
563
564 # Without any tags the subscription is found.
565- self.assertSubscriptions([self.subscription])
566+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
567+ bugtask, BugNotificationLevel.NOTHING)
568+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
569
570 # With any tag the subscription is not found.
571- self.bug.tags = ["foo"]
572- self.assertSubscriptions([])
573-
574- def test_getSubscriptionsForBugTask_with_filter_for_any_tag(self):
575- # If a subscription filter specifies that any of one or more specific
576- # tags must be present, bugs with any of those tags are matched.
577-
578- # Looking for either the "foo" or the "bar" tag.
579- subscription_filter = self.subscription.newBugFilter()
580- subscription_filter.tags = [u"foo", u"bar"]
581- subscription_filter.find_all_tags = False
582-
583- # Without either tag the subscription is not found.
584- self.assertSubscriptions([])
585-
586- # With either tag the subscription is found.
587- self.bug.tags = ["bar", "baz"]
588- self.assertSubscriptions([self.subscription])
589-
590- def test_getSubscriptionsForBugTask_with_filter_for_all_tags(self):
591- # If a subscription filter specifies that all of one or more specific
592- # tags must be present, bugs with all of those tags are matched.
593-
594- # Looking for both the "foo" and the "bar" tag.
595- subscription_filter = self.subscription.newBugFilter()
596- subscription_filter.tags = [u"foo", u"bar"]
597- subscription_filter.find_all_tags = True
598-
599- # Without either tag the subscription is not found.
600- self.assertSubscriptions([])
601-
602- # Without only one of the required tags the subscription is not found.
603- self.bug.tags = ["foo"]
604- self.assertSubscriptions([])
605-
606- # With both required tags the subscription is found.
607- self.bug.tags = ["foo", "bar"]
608- self.assertSubscriptions([self.subscription])
609-
610- def test_getSubscriptionsForBugTask_with_filter_for_not_any_tag(self):
611- # If a subscription filter specifies that any of one or more specific
612- # tags must not be present, bugs without any of those tags are
613- # matched.
614-
615- # Looking to exclude the "foo" or "bar" tags.
616- subscription_filter = self.subscription.newBugFilter()
617- subscription_filter.tags = [u"-foo", u"-bar"]
618- subscription_filter.find_all_tags = False
619-
620- # Without either tag the subscription is found.
621- self.assertSubscriptions([self.subscription])
622-
623- # With either tag the subscription is no longer found.
624- self.bug.tags = ["foo"]
625- self.assertSubscriptions([])
626-
627- def test_getSubscriptionsForBugTask_with_filter_for_not_all_tags(self):
628- # If a subscription filter specifies that all of one or more specific
629- # tags must not be present, bugs without all of those tags are
630- # matched.
631-
632- # Looking to exclude the "foo" and "bar" tags.
633- subscription_filter = self.subscription.newBugFilter()
634- subscription_filter.tags = [u"-foo", u"-bar"]
635- subscription_filter.find_all_tags = True
636-
637- # Without either tag the subscription is found.
638- self.assertSubscriptions([self.subscription])
639-
640- # With only one of the excluded tags the subscription is found.
641- self.bug.tags = ["foo"]
642- self.assertSubscriptions([self.subscription])
643-
644- # With both tags the subscription is no longer found.
645- self.bug.tags = ["foo", "bar"]
646- self.assertSubscriptions([])
647+ bugtask.bug.tags = ["foo"]
648+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
649+ bugtask, BugNotificationLevel.NOTHING)
650+ self.assertEqual([], list(subscriptions_for_bugtask))
651
652 def test_getSubscriptionsForBugTask_with_multiple_filters(self):
653 # If multiple filters exist for a subscription, all filters must
654 # match.
655+ bugtask = self.makeBugTask()
656
657- # Add the "foo" tag to the bug.
658- self.bug.tags = ["foo"]
659+ # Create a new subscription on self.target.
660+ login_person(self.ordinary_subscriber)
661+ subscription = self.target.addSubscription(
662+ self.ordinary_subscriber, self.ordinary_subscriber)
663+ subscription.bug_notification_level = BugNotificationLevel.COMMENTS
664
665 # Filter the subscription to bugs in the CRITICAL state.
666- subscription_filter = self.subscription.newBugFilter()
667+ subscription_filter = BugSubscriptionFilter()
668+ subscription_filter.structural_subscription = subscription
669 subscription_filter.statuses = [BugTaskStatus.CONFIRMED]
670 subscription_filter.importances = [BugTaskImportance.CRITICAL]
671
672 # With the filter the subscription is not found.
673- self.assertSubscriptions([])
674+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
675+ bugtask, BugNotificationLevel.NOTHING)
676+ self.assertEqual([], list(subscriptions_for_bugtask))
677
678 # If the filter is adjusted to match status but not importance, the
679 # subscription is still not found.
680- subscription_filter.statuses = [self.bugtask.status]
681- self.assertSubscriptions([])
682+ subscription_filter.statuses = [bugtask.status]
683+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
684+ bugtask, BugNotificationLevel.NOTHING)
685+ self.assertEqual([], list(subscriptions_for_bugtask))
686
687 # If the filter is adjusted to also match importance, the subscription
688 # is found again.
689- subscription_filter.importances = [self.bugtask.importance]
690- self.assertSubscriptions([self.subscription])
691-
692- # If the filter is given some tag criteria, the subscription is not
693- # found.
694- subscription_filter.tags = [u"-foo", u"bar", u"baz"]
695- subscription_filter.find_all_tags = False
696- self.assertSubscriptions([])
697-
698- # After removing the "foo" tag and adding the "bar" tag, the
699- # subscription is found.
700- self.bug.tags = ["bar"]
701- self.assertSubscriptions([self.subscription])
702-
703- # Requiring that all tag criteria are fulfilled causes the
704- # subscription to no longer be found.
705- subscription_filter.find_all_tags = True
706- self.assertSubscriptions([])
707-
708- # After adding the "baz" tag, the subscription is found again.
709- self.bug.tags = ["bar", "baz"]
710- self.assertSubscriptions([self.subscription])
711-
712- def test_getSubscriptionsForBugTask_any_filter_is_a_match(self):
713- # If a subscription has multiple filters, the subscription is selected
714- # when any filter is found to match. Put another way, the filters are
715- # ORed together.
716- subscription_filter1 = self.subscription.newBugFilter()
717- subscription_filter1.statuses = [BugTaskStatus.CONFIRMED]
718- subscription_filter2 = self.subscription.newBugFilter()
719- subscription_filter2.tags = [u"foo"]
720-
721- # With the filter the subscription is not found.
722- self.assertSubscriptions([])
723-
724- # If the bugtask is adjusted to match the criteria of the first filter
725- # but not those of the second, the subscription is found.
726- self.bugtask.transitionToStatus(
727- BugTaskStatus.CONFIRMED, self.ordinary_subscriber)
728- self.assertSubscriptions([self.subscription])
729-
730- # If the filter is adjusted to also match the criteria of the second
731- # filter, the subscription is still found.
732- self.bugtask.bug.tags = [u"foo"]
733- self.assertSubscriptions([self.subscription])
734-
735- # If the bugtask is adjusted to no longer match the criteria of the
736- # first filter, the subscription is still found.
737- self.bugtask.transitionToStatus(
738- BugTaskStatus.INPROGRESS, self.ordinary_subscriber)
739- self.assertSubscriptions([self.subscription])
740+ subscription_filter.importances = [bugtask.importance]
741+ subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask(
742+ bugtask, BugNotificationLevel.NOTHING)
743+ self.assertEqual([subscription], list(subscriptions_for_bugtask))
744
745
746 class TestStructuralSubscriptionForDistro(
747- RestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
748+ FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
749
750 layer = LaunchpadFunctionalLayer
751
752@@ -504,15 +421,10 @@
753 StructuralSubscription)
754
755
756-class TestStructuralSubscriptionFiltersForDistro(
757- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
758-
759- def makeTarget(self):
760- return self.factory.makeDistribution()
761-
762-
763 class TestStructuralSubscriptionForProduct(
764- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
765+ UnrestrictedStructuralSubscription,
766+ FilteredStructuralSubscriptionTestBase,
767+ TestCaseWithFactory):
768
769 layer = LaunchpadFunctionalLayer
770
771@@ -521,15 +433,10 @@
772 self.target = self.factory.makeProduct()
773
774
775-class TestStructuralSubscriptionFiltersForProduct(
776- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
777-
778- def makeTarget(self):
779- return self.factory.makeProduct()
780-
781-
782 class TestStructuralSubscriptionForDistroSourcePackage(
783- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
784+ UnrestrictedStructuralSubscription,
785+ FilteredStructuralSubscriptionTestBase,
786+ TestCaseWithFactory):
787
788 layer = LaunchpadFunctionalLayer
789
790@@ -539,15 +446,10 @@
791 self.target = ProxyFactory(self.target)
792
793
794-class TestStructuralSubscriptionFiltersForDistroSourcePackage(
795- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
796-
797- def makeTarget(self):
798- return self.factory.makeDistributionSourcePackage()
799-
800-
801 class TestStructuralSubscriptionForMilestone(
802- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
803+ UnrestrictedStructuralSubscription,
804+ FilteredStructuralSubscriptionTestBase,
805+ TestCaseWithFactory):
806
807 layer = LaunchpadFunctionalLayer
808
809@@ -556,19 +458,15 @@
810 self.target = self.factory.makeMilestone()
811 self.target = ProxyFactory(self.target)
812
813-
814-class TestStructuralSubscriptionFiltersForMilestone(
815- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
816-
817- def makeTarget(self):
818- return self.factory.makeMilestone()
819-
820 def makeBugTask(self):
821+ # XXX Should test with target *and* series_target.
822 return self.factory.makeBugTask(target=self.target.series_target)
823
824
825 class TestStructuralSubscriptionForDistroSeries(
826- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
827+ UnrestrictedStructuralSubscription,
828+ FilteredStructuralSubscriptionTestBase,
829+ TestCaseWithFactory):
830
831 layer = LaunchpadFunctionalLayer
832
833@@ -578,15 +476,10 @@
834 self.target = ProxyFactory(self.target)
835
836
837-class TestStructuralSubscriptionFiltersForDistroSeries(
838- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
839-
840- def makeTarget(self):
841- return self.factory.makeDistroSeries()
842-
843-
844 class TestStructuralSubscriptionForProjectGroup(
845- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
846+ UnrestrictedStructuralSubscription,
847+ FilteredStructuralSubscriptionTestBase,
848+ TestCaseWithFactory):
849
850 layer = LaunchpadFunctionalLayer
851
852@@ -595,20 +488,15 @@
853 self.target = self.factory.makeProject()
854 self.target = ProxyFactory(self.target)
855
856-
857-class TestStructuralSubscriptionFiltersForProjectGroup(
858- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
859-
860- def makeTarget(self):
861- return self.factory.makeProject()
862-
863 def makeBugTask(self):
864 return self.factory.makeBugTask(
865 target=self.factory.makeProduct(project=self.target))
866
867
868 class TestStructuralSubscriptionForProductSeries(
869- UnrestrictedStructuralSubscriptionTestBase, TestCaseWithFactory):
870+ UnrestrictedStructuralSubscription,
871+ FilteredStructuralSubscriptionTestBase,
872+ TestCaseWithFactory):
873
874 layer = LaunchpadFunctionalLayer
875
876@@ -618,13 +506,6 @@
877 self.target = ProxyFactory(self.target)
878
879
880-class TestStructuralSubscriptionFiltersForProductSeries(
881- FilteredStructuralSubscriptionTestBase, TestCaseWithFactory):
882-
883- def makeTarget(self):
884- return self.factory.makeProductSeries()
885-
886-
887 class TestStructuralSubscriptionTargetHelper(TestCaseWithFactory):
888 """Tests for implementations of `IStructuralSubscriptionTargetHelper`."""
889