Merge lp:~allenap/launchpad/checkwatches-stuff into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Graham Binns
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~allenap/launchpad/checkwatches-stuff
Merge into: lp:launchpad
Diff against target: 972 lines (+280/-237)
9 files modified
lib/canonical/launchpad/scripts/ftests/test_checkwatches.py (+0/-93)
lib/lp/bugs/doc/externalbugtracker.txt (+27/-6)
lib/lp/bugs/interfaces/bugwatch.py (+13/-0)
lib/lp/bugs/interfaces/externalbugtracker.py (+31/-7)
lib/lp/bugs/model/bugwatch.py (+9/-1)
lib/lp/bugs/scripts/checkwatches.py (+39/-48)
lib/lp/bugs/scripts/tests/test_bugimport.py (+10/-7)
lib/lp/bugs/scripts/tests/test_checkwatches.py (+82/-26)
lib/lp/bugs/tests/test_bugwatch.py (+69/-49)
To merge this branch: bzr merge lp:~allenap/launchpad/checkwatches-stuff
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Eleanor Berger Pending
Review via email: mp+16495@code.launchpad.net

Commit message

Commit after every successful bug watch update.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

The main change here is that BugWatchUpdater.updateBugWatches()
commits transactions regularly, instead of trying to keep them
open. It tried to do this to avoid calling an expensive function. I've
changed the code to use a different, new, function which means that
it's not necessary to hold the transaction open.

Looking at it again, I don't know that this was necessary; I could
have just added a commit in there and it would have worked. However,
the expensive function was really doing ugly things, so I feel that
the change was worth it anyway.

Lint free.

Test: bin/test -vvt 'checkwatch|bug-?watch'

There are two other small changes, detailed below.

lib/lp/bugs/doc/externalbugtracker.txt

  Amended this doctest to show the ways transactions are now handled
  in BugWatchUpdater.updateBugWatches().

lib/lp/bugs/interfaces/bugwatch.py

  New method getBugWatchesForRemoteBug().

lib/lp/bugs/interfaces/externalbugtracker.py

  Improved interface docstrings.

lib/lp/bugs/model/bugwatch.py

  Implementation of getBugWatchesForRemoteBug().

lib/lp/bugs/scripts/checkwatches.py

  Removed BugWatchUpdater._getBugWatchesByRemoteBug(). This method is
  reasonably expensive because it can end up iterating through 1000s
  of bug watches, each of which it queries individually from the
  database.

  It needed to be called after each aborted transaction (i.e. after an
  error updating another bug watch), and this has resulted in trying
  to hold transactions open for a long time instead of committing
  after each update.

  From reading the code, it's apparent that this can mean that a
  failure in one bug watch can cause nothing to be recorded for many
  other bug watches that are otherwise fine.

  I've changed updateBugWatches() to commit on every iteration of its
  main loop, and now use IBugWatchSet.getBugWatchesForRemoteBug()
  instead of self._getBugWatchesByRemoteBug().

  In updateBugWatches() I also renamed bug_id to remote_bug_id because
  it was confusing.

lib/canonical/launchpad/scripts/ftests/test_checkwatches.py =>
lib/lp/bugs/scripts/tests/test_checkwatches.py

  Moved this module into the lp tree, and renamed the only TestCase in
  it. I originally added some test code to this file, but later moved
  it elsewhere, but I want to land the move and rename anyway.

lib/lp/bugs/tests/test_bugwatch.py

  Refactored this because the test_suite() function was ugly and
  fragile. It would be easy to add a TestCase, forget to add it to the
  test_suite() method, and no one notice.

  I also added TestBugWatchSet, which currently only tests the new
  method, getBugWatchesForRemoteBug().

Revision history for this message
Graham Binns (gmb) wrote :
Download full text (4.7 KiB)

Hi Gavin,

I'm marking this as Needs Fixing for now because there's some
conflicty-oddness going on. Apart from that, though, and a naming issue,
I'm happy with this. Just let me know when the problems I've mentioned
are fixed.

> === modified file 'lib/lp/bugs/scripts/checkwatches.py'
> --- lib/lp/bugs/scripts/checkwatches.py 2009-12-16 16:50:24 +0000
> +++ lib/lp/bugs/scripts/checkwatches.py 2010-01-04 15:37:12 +0000
> @@ -414,26 +414,6 @@
> """Return the bug watch with id `bug_watch_id`."""
> return getUtility(IBugWatchSet).get(bug_watch_id)
>
> - def _getBugWatchesByRemoteBug(self, bug_watch_ids):
> - """Returns a dictionary of bug watches mapped to remote bugs.
> -
> - For each bug watch id fetches the corresponding bug watch and
> - appends it to a list of bug watches pointing to one remote
> - bug - the key of the returned mapping.
> - """
> - bug_watches_by_remote_bug = {}
> - for bug_watch_id in bug_watch_ids:
> - bug_watch = self._getBugWatch(bug_watch_id)
> - remote_bug = bug_watch.remotebug
> - # There can be multiple bug watches pointing to the same
> - # remote bug; because of that, we need to store lists of bug
> - # watches related to the remote bug, and later update the
> - # status of each one of them.
> - if remote_bug not in bug_watches_by_remote_bug:
> - bug_watches_by_remote_bug[remote_bug] = []
> - bug_watches_by_remote_bug[remote_bug].append(bug_watch)
> - return bug_watches_by_remote_bug
> -
> def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches):
> """Return an `ExternalBugTracker` instance for `bug_tracker`."""
> remotesystem = externalbugtracker.get_external_bugtracker(
> @@ -686,6 +666,17 @@
> 'unmodified_remote_ids': unmodified_remote_ids,
> }
>
> + def getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):

This should either start with an underscore or (if you have the time and
the pain threshold) you should remove all underscores from method names
in BugWatchUpdater, because they aren't relevant. However, that's a big
change and whilst I'd like it I wouldn't advise it.

> + """Return a list of bug watches for the given remote bug.
> +
> + The returned watches will all be members of `bug_watch_ids`.
> +
> + This method exists primarily to be overridden during testing.
> + """
> + return list(
> + getUtility(IBugWatchSet).getBugWatchesForRemoteBug(
> + remote_bug_id, bug_watch_ids))
> +
> # XXX gmb 2008-11-07 [bug=295319]
> # This method is 186 lines long. It needs to be shorter.
> def updateBugWatches(self, remotesystem, bug_watches_to_update, now=None,
> @@ -752,10 +743,6 @@
> self.txn.commit()
> raise
>
> - self.txn.begin()
> - bug_watches_by_remote_bug = self._getBugWatchesByRemoteBug(
> - bug_watch_ids)
> -
> # Whether we can import and / or push comments is determined on
> # a per-bugtracker-type level....

Read more...

review: Needs Fixing (code)
Revision history for this message
Gavin Panella (allenap) wrote :

Hi Graham, thanks for the review. I've merged devel again, changed the method name (not doing all of them; too much pain for now), and have fixed another test which I broke.

Revision history for this message
Graham Binns (gmb) wrote :

Approved pending the removal of the erroneous .moved file.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== removed file 'lib/canonical/launchpad/scripts/ftests/test_checkwatches.py'
--- lib/canonical/launchpad/scripts/ftests/test_checkwatches.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/scripts/ftests/test_checkwatches.py 1970-01-01 00:00:00 +0000
@@ -1,93 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the checkwatches remote bug synchronisation code."""
5
6__metaclass__ = type
7__all__ = []
8
9import unittest
10
11from zope.component import getUtility
12
13from canonical.config import config
14from canonical.database.sqlbase import commit
15from canonical.launchpad.ftests import login
16from lp.bugs.tests.externalbugtracker import new_bugtracker
17from canonical.launchpad.interfaces import (BugTaskStatus, BugTrackerType,
18 IBugSet, IBugTaskSet, ILaunchpadCelebrities, IPersonSet,
19 IProductSet, IQuestionSet)
20from canonical.testing import LaunchpadZopelessLayer
21
22
23class TestCheckwatches(unittest.TestCase):
24 """Tests for the bugwatch updating system."""
25 layer = LaunchpadZopelessLayer
26
27 def setUp(self):
28 """Set up bugs, watches and questions to test with."""
29 super(TestCheckwatches, self).setUp()
30
31 # For test_can_update_bug_with_questions we need a bug that has
32 # a question linked to it.
33 bug_with_question = getUtility(IBugSet).get(10)
34 question = getUtility(IQuestionSet).get(1)
35
36 # XXX gmb 2007-12-11 bug 175545:
37 # We shouldn't have to login() here, but since
38 # database.buglinktarget.BugLinkTargetMixin.linkBug()
39 # doesn't accept a user parameter, instead depending on the
40 # currently logged in user, we get an exception if we don't.
41 login('test@canonical.com')
42 question.linkBug(bug_with_question)
43
44 # We subscribe launchpad_developers to the question since this
45 # indirectly subscribes foo.bar@canonical.com to it, too. We can
46 # then use this to test the updating of a question with indirect
47 # subscribers from a bug watch.
48 question.subscribe(
49 getUtility(ILaunchpadCelebrities).launchpad_developers)
50 commit()
51
52 # We now need to switch to the checkwatches DB user so that
53 # we're testing with the correct set of permissions.
54 self.layer.switchDbUser(config.checkwatches.dbuser)
55
56 # For test_can_update_bug_with_questions we also need a bug
57 # watch and by extension a bug tracker.
58 sample_person = getUtility(IPersonSet).getByEmail(
59 'test@canonical.com')
60 bugtracker = new_bugtracker(BugTrackerType.ROUNDUP)
61 self.bugtask_with_question = getUtility(IBugTaskSet).createTask(
62 bug_with_question, sample_person,
63 product=getUtility(IProductSet).getByName('firefox'))
64 self.bugwatch_with_question = bug_with_question.addWatch(
65 bugtracker, '1', getUtility(ILaunchpadCelebrities).janitor)
66 self.bugtask_with_question.bugwatch = self.bugwatch_with_question
67 commit()
68
69 def test_can_update_bug_with_questions(self):
70 """Test whether bugs with linked questions can be updated.
71
72 This will also test whether indirect subscribers of linked
73 questions will be notified of the changes made when the bugwatch
74 is updated.
75 """
76 # We need to check that the bug task we created in setUp() is
77 # still being referenced by our bug watch.
78 self.assertEqual(self.bugwatch_with_question.bugtasks[0].id,
79 self.bugtask_with_question.id)
80
81 # We can now update the bug watch, which will in turn update the
82 # bug task and the linked question.
83 self.bugwatch_with_question.updateStatus('some status',
84 BugTaskStatus.INPROGRESS)
85 self.assertEqual(self.bugwatch_with_question.bugtasks[0].status,
86 BugTaskStatus.INPROGRESS,
87 "BugTask status is inconsistent. Expected %s but got %s" %
88 (BugTaskStatus.INPROGRESS.title,
89 self.bugtask_with_question.status.title))
90
91
92def test_suite():
93 return unittest.TestLoader().loadTestsFromName(__name__)
940
=== modified file 'lib/lp/bugs/doc/externalbugtracker.txt'
--- lib/lp/bugs/doc/externalbugtracker.txt 2009-11-28 08:31:15 +0000
+++ lib/lp/bugs/doc/externalbugtracker.txt 2010-01-05 16:11:20 +0000
@@ -42,7 +42,6 @@
42 ... InitializingExternalBugTracker(), [])42 ... InitializingExternalBugTracker(), [])
43 COMMIT43 COMMIT
44 initializeRemoteBugDB() called: []44 initializeRemoteBugDB() called: []
45 BEGIN
4645
4746
48=== Choosing another ExternalBugTracker instance ===47=== Choosing another ExternalBugTracker instance ===
@@ -318,7 +317,6 @@
318 COMMIT317 COMMIT
319 getCurrentDBTime() called318 getCurrentDBTime() called
320 initializeRemoteBugDB() called: []319 initializeRemoteBugDB() called: []
321 BEGIN
322320
323If the difference between what we and the remote system think the time321If the difference between what we and the remote system think the time
324is, an error is raised.322is, an error is raised.
@@ -370,7 +368,6 @@
370 >>> bug_watch_updater.updateBugWatches(368 >>> bug_watch_updater.updateBugWatches(
371 ... CorrectTimeExternalBugTracker(), [], now=utc_now)369 ... CorrectTimeExternalBugTracker(), [], now=utc_now)
372 COMMIT370 COMMIT
373 BEGIN
374371
375If the timezone is known, the local time time should be returned, rather372If the timezone is known, the local time time should be returned, rather
376than the UTC time.373than the UTC time.
@@ -383,7 +380,6 @@
383 >>> bug_watch_updater.updateBugWatches(380 >>> bug_watch_updater.updateBugWatches(
384 ... LocalTimeExternalBugTracker(), [], now=utc_now)381 ... LocalTimeExternalBugTracker(), [], now=utc_now)
385 COMMIT382 COMMIT
386 BEGIN
387383
388If the remote server time is unknown, we will refuse to import any384If the remote server time is unknown, we will refuse to import any
389comments from it. Bug watches will still be updated, but a warning is385comments from it. Bug watches will still be updated, but a warning is
@@ -399,7 +395,6 @@
399 COMMIT395 COMMIT
400 getCurrentDBTime() called396 getCurrentDBTime() called
401 initializeRemoteBugDB() called: []397 initializeRemoteBugDB() called: []
402 BEGIN
403 WARNING:...:Comment importing supported, but server time can't be398 WARNING:...:Comment importing supported, but server time can't be
404 trusted. No comments will be imported.399 trusted. No comments will be imported.
405400
@@ -494,9 +489,16 @@
494 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']489 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
495 BEGIN490 BEGIN
496 getRemoteStatus() called: u'1'491 getRemoteStatus() called: u'1'
492 COMMIT
493 BEGIN
497 getRemoteStatus() called: u'2'494 getRemoteStatus() called: u'2'
495 COMMIT
496 BEGIN
498 getRemoteStatus() called: u'3'497 getRemoteStatus() called: u'3'
498 COMMIT
499 BEGIN
499 getRemoteStatus() called: u'4'500 getRemoteStatus() called: u'4'
501 COMMIT
500502
501If the bug watches have the lastchecked attribute set, they will be503If the bug watches have the lastchecked attribute set, they will be
502passed to getModifiedRemoteBugs(). Only the bugs that have been modified504passed to getModifiedRemoteBugs(). Only the bugs that have been modified
@@ -530,7 +532,12 @@
530 initializeRemoteBugDB() called: [u'1', u'4']532 initializeRemoteBugDB() called: [u'1', u'4']
531 BEGIN533 BEGIN
532 getRemoteStatus() called: u'1'534 getRemoteStatus() called: u'1'
535 COMMIT
536 BEGIN
533 getRemoteStatus() called: u'4'537 getRemoteStatus() called: u'4'
538 COMMIT
539 BEGIN
540 BEGIN
534541
535The bug watches that are deemed as not being modified are still marked542The bug watches that are deemed as not being modified are still marked
536as being checked.543as being checked.
@@ -580,9 +587,16 @@
580 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']587 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
581 BEGIN588 BEGIN
582 getRemoteStatus() called: u'1'589 getRemoteStatus() called: u'1'
590 COMMIT
591 BEGIN
583 getRemoteStatus() called: u'2'592 getRemoteStatus() called: u'2'
593 COMMIT
594 BEGIN
584 getRemoteStatus() called: u'3'595 getRemoteStatus() called: u'3'
596 COMMIT
597 BEGIN
585 getRemoteStatus() called: u'4'598 getRemoteStatus() called: u'4'
599 COMMIT
586600
587As mentioned earlier, getModifiedRemoteBugs() is only called if we can601As mentioned earlier, getModifiedRemoteBugs() is only called if we can
588get the current time of the remote system. If the time is unknown, we602get the current time of the remote system. If the time is unknown, we
@@ -600,9 +614,16 @@
600 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']614 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
601 BEGIN615 BEGIN
602 getRemoteStatus() called: u'1'616 getRemoteStatus() called: u'1'
617 COMMIT
618 BEGIN
603 getRemoteStatus() called: u'2'619 getRemoteStatus() called: u'2'
620 COMMIT
621 BEGIN
604 getRemoteStatus() called: u'3'622 getRemoteStatus() called: u'3'
623 COMMIT
624 BEGIN
605 getRemoteStatus() called: u'4'625 getRemoteStatus() called: u'4'
626 COMMIT
606627
607The only exception to the rule of only updating modified bugs is the set628The only exception to the rule of only updating modified bugs is the set
608of bug watches which have comments that need to be pushed to the remote629of bug watches which have comments that need to be pushed to the remote
@@ -1102,7 +1123,7 @@
1102 >>> bug_watch_updater.updateBugTrackers(1123 >>> bug_watch_updater.updateBugTrackers(
1103 ... bug_tracker_names=[standard_bugzilla.name], batch_size=2)1124 ... bug_tracker_names=[standard_bugzilla.name], batch_size=2)
1104 DEBUG Using a global batch size of 21125 DEBUG Using a global batch size of 2
1105 INFO Updating 4 watches for 2 bugs on http://example.com1126 INFO Updating 2 watches for 2 bugs on http://example.com
1106 initializeRemoteBugDB() called: [u'5', u'6']1127 initializeRemoteBugDB() called: [u'5', u'6']
1107 getRemoteStatus() called: u'5'1128 getRemoteStatus() called: u'5'
1108 getRemoteStatus() called: u'6'1129 getRemoteStatus() called: u'6'
11091130
=== modified file 'lib/lp/bugs/interfaces/bugwatch.py'
--- lib/lp/bugs/interfaces/bugwatch.py 2009-09-04 09:48:04 +0000
+++ lib/lp/bugs/interfaces/bugwatch.py 2010-01-05 16:11:20 +0000
@@ -268,6 +268,19 @@
268 If no bug tracker type can be guessed, None is returned.268 If no bug tracker type can be guessed, None is returned.
269 """269 """
270270
271 def getBugWatchesForRemoteBug(self, remote_bug, bug_watch_ids=None):
272 """Returns bug watches referring to the given remote bug.
273
274 Returns a set of those bug watches, optionally limited to
275 those with IDs in `bug_watch_ids`, that refer to `remote_bug`.
276
277 :param remote_bug_id: The ID of the remote bug.
278 :type remote_bug_id: See `IBugWatch.remotebug`.
279
280 :param bug_watch_ids: A collection of `BugWatch` IDs.
281 :type bug_watch_ids: An iterable of `int`s, or `None`.
282 """
283
271284
272class NoBugTrackerFound(Exception):285class NoBugTrackerFound(Exception):
273 """No bug tracker with the base_url is registered in Launchpad."""286 """No bug tracker with the base_url is registered in Launchpad."""
274287
=== modified file 'lib/lp/bugs/interfaces/externalbugtracker.py'
--- lib/lp/bugs/interfaces/externalbugtracker.py 2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/interfaces/externalbugtracker.py 2010-01-05 16:11:20 +0000
@@ -43,35 +43,56 @@
43 """Return the `ExternalBugTracker` instance to use.43 """Return the `ExternalBugTracker` instance to use.
4444
45 Probe the remote bug tracker and choose the right45 Probe the remote bug tracker and choose the right
46 `ExternalBugTracker` instance to use further on.46 `ExternalBugTracker` instance to use further on. In most cases
47 this will simply return `self`.
47 """48 """
4849
49 def getCurrentDBTime():50 def getCurrentDBTime():
50 """Return the current time of the bug tracker's DB server.51 """Return the current time of the bug tracker's DB server.
5152
52 The current time will be returned as a timezone-aware datetime.53 The current time will be returned as a timezone-aware datetime.
54
55 :return: `datetime.datetime` with timezone.
53 """56 """
5457
55 def getModifiedRemoteBugs(remote_bug_ids, last_checked):58 def getModifiedRemoteBugs(remote_bug_ids, last_checked):
56 """Return the bug ids that have been modified.59 """Return the bug ids that have been modified.
5760
58 Return all ids if the modified bugs can't be determined.61 Return all ids if the modified bugs can't be determined.
62
63 :param remote_bug_ids: The remote bug IDs to be checked.
64 :type remote_bug_ids: `list` of strings
65
66 :param last_checked: The date and time since when a bug should
67 be considered modified.
68 :param last_checked: `datetime.datetime`
59 """69 """
6070
61 def initializeRemoteBugDB(remote_bug_ids):71 def initializeRemoteBugDB(remote_bug_ids):
62 """Do any initialization before each bug watch is updated."""72 """Do any initialization before each bug watch is updated.
73
74 :param remote_bug_ids: The remote bug IDs that to be checked.
75 :type remote_bug_ids: `list` of strings
76 """
6377
64 def convertRemoteStatus(remote_status):78 def convertRemoteStatus(remote_status):
65 """Convert a remote status string to a BugTaskStatus item."""79 """Convert a remote status string to a BugTaskStatus item.
80
81 :return: a member of `BugTaskStatus`
82 """
6683
67 def convertRemoteImportance(remote_importance):84 def convertRemoteImportance(remote_importance):
68 """Convert a remote importance to a BugTaskImportance item."""85 """Convert a remote importance to a BugTaskImportance item.
86
87 :return: a member of `BugTaskImportance`
88 """
6989
70 def getRemoteProduct(remote_bug):90 def getRemoteProduct(remote_bug):
71 """Return the remote product for a given remote bug.91 """Return the remote product for a given remote bug.
7292
73 :param remote_bug: The ID of the remote bug for which to return93 :param remote_bug: The ID of the remote bug for which to return
74 the remote product.94 the remote product.
95 :type remote_bug: string
75 :return: The remote product for `remote_bug`. If no remote96 :return: The remote product for `remote_bug`. If no remote
76 product is recorded for `remote_bug` return None.97 product is recorded for `remote_bug` return None.
77 :raise BugNotFound: If `remote_bug` doesn't exist for the bug98 :raise BugNotFound: If `remote_bug` doesn't exist for the bug
@@ -123,16 +144,19 @@
123 def getBugReporter(remote_bug):144 def getBugReporter(remote_bug):
124 """Return the person who submitted the given bug.145 """Return the person who submitted the given bug.
125146
126 A tuple of (display name, email) is returned.147 :return: `tuple` of (display name, email)
127 """148 """
128149
129 def getBugSummaryAndDescription(remote_bug):150 def getBugSummaryAndDescription(remote_bug):
130 """Return a tuple of summary and description for the given bug."""151 """Return the summary and description for the given bug.
152
153 :return: `tuple` of (summary, description)
154 """
131155
132 def getBugTargetName(remote_bug):156 def getBugTargetName(remote_bug):
133 """Return the specific target name of the bug.157 """Return the specific target name of the bug.
134158
135 Return None if no target can be determined.159 :return: string, or `None` if no target can be determined
136 """160 """
137161
138162
139163
=== modified file 'lib/lp/bugs/model/bugwatch.py'
--- lib/lp/bugs/model/bugwatch.py 2009-12-24 06:44:57 +0000
+++ lib/lp/bugs/model/bugwatch.py 2010-01-05 16:11:20 +0000
@@ -17,7 +17,7 @@
17# SQL imports17# SQL imports
18from sqlobject import ForeignKey, SQLObjectNotFound, StringCol18from sqlobject import ForeignKey, SQLObjectNotFound, StringCol
1919
20from storm.expr import Desc, Not20from storm.expr import Desc, In, Not
21from storm.store import Store21from storm.store import Store
2222
23from lazr.lifecycle.event import ObjectModifiedEvent23from lazr.lifecycle.event import ObjectModifiedEvent
@@ -31,6 +31,7 @@
31from canonical.launchpad.database.message import Message31from canonical.launchpad.database.message import Message
32from canonical.launchpad.helpers import shortlist32from canonical.launchpad.helpers import shortlist
33from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities33from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
34from canonical.launchpad.interfaces.lpstorm import IStore
34from canonical.launchpad.validators.email import valid_email35from canonical.launchpad.validators.email import valid_email
35from canonical.launchpad.webapp import urlappend, urlsplit36from canonical.launchpad.webapp import urlappend, urlsplit
36from canonical.launchpad.webapp.interfaces import NotFoundError37from canonical.launchpad.webapp.interfaces import NotFoundError
@@ -603,3 +604,10 @@
603604
604 raise UnrecognizedBugTrackerURL(url)605 raise UnrecognizedBugTrackerURL(url)
605606
607 def getBugWatchesForRemoteBug(self, remote_bug, bug_watch_ids=None):
608 """See `IBugWatchSet`."""
609 query = IStore(BugWatch).find(
610 BugWatch, BugWatch.remotebug == remote_bug)
611 if bug_watch_ids is not None:
612 query = query.find(In(BugWatch.id, bug_watch_ids))
613 return query
606614
=== modified file 'lib/lp/bugs/scripts/checkwatches.py'
--- lib/lp/bugs/scripts/checkwatches.py 2009-12-16 16:50:24 +0000
+++ lib/lp/bugs/scripts/checkwatches.py 2010-01-05 16:11:20 +0000
@@ -414,26 +414,6 @@
414 """Return the bug watch with id `bug_watch_id`."""414 """Return the bug watch with id `bug_watch_id`."""
415 return getUtility(IBugWatchSet).get(bug_watch_id)415 return getUtility(IBugWatchSet).get(bug_watch_id)
416416
417 def _getBugWatchesByRemoteBug(self, bug_watch_ids):
418 """Returns a dictionary of bug watches mapped to remote bugs.
419
420 For each bug watch id fetches the corresponding bug watch and
421 appends it to a list of bug watches pointing to one remote
422 bug - the key of the returned mapping.
423 """
424 bug_watches_by_remote_bug = {}
425 for bug_watch_id in bug_watch_ids:
426 bug_watch = self._getBugWatch(bug_watch_id)
427 remote_bug = bug_watch.remotebug
428 # There can be multiple bug watches pointing to the same
429 # remote bug; because of that, we need to store lists of bug
430 # watches related to the remote bug, and later update the
431 # status of each one of them.
432 if remote_bug not in bug_watches_by_remote_bug:
433 bug_watches_by_remote_bug[remote_bug] = []
434 bug_watches_by_remote_bug[remote_bug].append(bug_watch)
435 return bug_watches_by_remote_bug
436
437 def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches):417 def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches):
438 """Return an `ExternalBugTracker` instance for `bug_tracker`."""418 """Return an `ExternalBugTracker` instance for `bug_tracker`."""
439 remotesystem = externalbugtracker.get_external_bugtracker(419 remotesystem = externalbugtracker.get_external_bugtracker(
@@ -686,6 +666,17 @@
686 'unmodified_remote_ids': unmodified_remote_ids,666 'unmodified_remote_ids': unmodified_remote_ids,
687 }667 }
688668
669 def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
670 """Return a list of bug watches for the given remote bug.
671
672 The returned watches will all be members of `bug_watch_ids`.
673
674 This method exists primarily to be overridden during testing.
675 """
676 return list(
677 getUtility(IBugWatchSet).getBugWatchesForRemoteBug(
678 remote_bug_id, bug_watch_ids))
679
689 # XXX gmb 2008-11-07 [bug=295319]680 # XXX gmb 2008-11-07 [bug=295319]
690 # This method is 186 lines long. It needs to be shorter.681 # This method is 186 lines long. It needs to be shorter.
691 def updateBugWatches(self, remotesystem, bug_watches_to_update, now=None,682 def updateBugWatches(self, remotesystem, bug_watches_to_update, now=None,
@@ -752,10 +743,6 @@
752 self.txn.commit()743 self.txn.commit()
753 raise744 raise
754745
755 self.txn.begin()
756 bug_watches_by_remote_bug = self._getBugWatchesByRemoteBug(
757 bug_watch_ids)
758
759 # Whether we can import and / or push comments is determined on746 # Whether we can import and / or push comments is determined on
760 # a per-bugtracker-type level.747 # a per-bugtracker-type level.
761 can_import_comments = (748 can_import_comments = (
@@ -788,26 +775,32 @@
788 "local bugs: %(local_ids)s"775 "local bugs: %(local_ids)s"
789 )776 )
790777
791 for bug_id in all_remote_ids:778 for remote_bug_id in all_remote_ids:
792 try:779 # Start a fresh transaction every time round the loop.
793 bug_watches = bug_watches_by_remote_bug[bug_id]780 self.txn.begin()
794 except KeyError:781
782 bug_watches = self._getBugWatchesForRemoteBug(
783 remote_bug_id, bug_watch_ids)
784 if len(bug_watches) == 0:
795 # If there aren't any bug watches for this remote bug,785 # If there aren't any bug watches for this remote bug,
796 # just log a warning and carry on.786 # just log a warning and carry on.
797 self.warning(787 self.warning(
798 "Spurious remote bug ID: No watches found for "788 "Spurious remote bug ID: No watches found for "
799 "remote bug %s on %s" % (bug_id, remotesystem.baseurl))789 "remote bug %s on %s" % (
790 remote_bug_id, remotesystem.baseurl))
800 continue791 continue
801792
802 for bug_watch in bug_watches:793 for bug_watch in bug_watches:
803 bug_watch.lastchecked = UTC_NOW794 bug_watch.lastchecked = UTC_NOW
804 if bug_id in unmodified_remote_ids:795 if remote_bug_id in unmodified_remote_ids:
805 continue796 continue
806797
807 # Save the remote bug URL in case we need to log an error.798 # Save the remote bug URL in case we need to log an error.
808 remote_bug_url = bug_watches[0].url799 remote_bug_url = bug_watches[0].url
809800
810 local_ids = ", ".join(str(watch.bug.id) for watch in bug_watches)801 local_ids = ", ".join(
802 str(bug_id) for bug_id in sorted(
803 watch.bug.id for watch in bug_watches))
811 try:804 try:
812 new_remote_status = None805 new_remote_status = None
813 new_malone_status = None806 new_malone_status = None
@@ -820,12 +813,13 @@
820 # necessary and can be refactored out when bug813 # necessary and can be refactored out when bug
821 # 136391 is dealt with.814 # 136391 is dealt with.
822 try:815 try:
823 new_remote_status = remotesystem.getRemoteStatus(bug_id)816 new_remote_status = (
817 remotesystem.getRemoteStatus(remote_bug_id))
824 new_malone_status = self._convertRemoteStatus(818 new_malone_status = self._convertRemoteStatus(
825 remotesystem, new_remote_status)819 remotesystem, new_remote_status)
826820
827 new_remote_importance = remotesystem.getRemoteImportance(821 new_remote_importance = (
828 bug_id)822 remotesystem.getRemoteImportance(remote_bug_id))
829 new_malone_importance = (823 new_malone_importance = (
830 remotesystem.convertRemoteImportance(824 remotesystem.convertRemoteImportance(
831 new_remote_importance))825 new_remote_importance))
@@ -835,13 +829,13 @@
835 error, error_type_message_default)829 error, error_type_message_default)
836 self.warning(830 self.warning(
837 message % {831 message % {
838 'bug_id': bug_id,832 'bug_id': remote_bug_id,
839 'base_url': remotesystem.baseurl,833 'base_url': remotesystem.baseurl,
840 'local_ids': local_ids,834 'local_ids': local_ids,
841 },835 },
842 properties=[836 properties=[
843 ('URL', remote_bug_url),837 ('URL', remote_bug_url),
844 ('bug_id', bug_id),838 ('bug_id', remote_bug_id),
845 ('local_ids', local_ids),839 ('local_ids', local_ids),
846 ] + self._getOOPSProperties(remotesystem),840 ] + self._getOOPSProperties(remotesystem),
847 info=sys.exc_info())841 info=sys.exc_info())
@@ -868,17 +862,11 @@
868 except (KeyboardInterrupt, SystemExit):862 except (KeyboardInterrupt, SystemExit):
869 # We should never catch KeyboardInterrupt or SystemExit.863 # We should never catch KeyboardInterrupt or SystemExit.
870 raise864 raise
865
871 except Exception, error:866 except Exception, error:
872 # If something unexpected goes wrong, we shouldn't break the867 # Restart transaction before recording the error.
873 # updating of the other bugs.
874
875 # Restart the transaction so that subsequent
876 # bug watches will get recorded.
877 self.txn.abort()868 self.txn.abort()
878 self.txn.begin()869 self.txn.begin()
879 bug_watches_by_remote_bug = self._getBugWatchesByRemoteBug(
880 bug_watch_ids)
881
882 # We record errors against the bug watches and update870 # We record errors against the bug watches and update
883 # their lastchecked dates so that we don't try to871 # their lastchecked dates so that we don't try to
884 # re-check them every time checkwatches runs.872 # re-check them every time checkwatches runs.
@@ -889,17 +877,20 @@
889 # We need to commit the transaction, in case the next877 # We need to commit the transaction, in case the next
890 # bug fails to update as well.878 # bug fails to update as well.
891 self.txn.commit()879 self.txn.commit()
892 self.txn.begin()880 # Send the error to the log too.
893
894 self.error(881 self.error(
895 "Failure updating bug %r on %s (local bugs: %s)." %882 "Failure updating bug %r on %s (local bugs: %s)." %
896 (bug_id, bug_tracker_url, local_ids),883 (remote_bug_id, bug_tracker_url, local_ids),
897 properties=[884 properties=[
898 ('URL', remote_bug_url),885 ('URL', remote_bug_url),
899 ('bug_id', bug_id),886 ('bug_id', remote_bug_id),
900 ('local_ids', local_ids)] +887 ('local_ids', local_ids)] +
901 self._getOOPSProperties(remotesystem))888 self._getOOPSProperties(remotesystem))
902889
890 else:
891 # All is well, save it now.
892 self.txn.commit()
893
903 def importBug(self, external_bugtracker, bugtracker, bug_target,894 def importBug(self, external_bugtracker, bugtracker, bug_target,
904 remote_bug):895 remote_bug):
905 """Import a remote bug into Launchpad.896 """Import a remote bug into Launchpad.
906897
=== modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py'
--- lib/lp/bugs/scripts/tests/test_bugimport.py 2009-11-27 16:16:12 +0000
+++ lib/lp/bugs/scripts/tests/test_bugimport.py 2010-01-05 16:11:20 +0000
@@ -892,15 +892,18 @@
892 """See `BugWatchUpdater`."""892 """See `BugWatchUpdater`."""
893 return [(TestExternalBugTracker(bug_tracker.baseurl), bug_watches)]893 return [(TestExternalBugTracker(bug_tracker.baseurl), bug_watches)]
894894
895 def _getBugWatch(self, bug_watch_id):895 def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
896 """Returns a mock bug watch object.896 """Returns a list of fake bug watch objects.
897897
898 We override this method to force one of our two bug watches898 We override this method so that we always return bug watches
899 to be returned. The first is guaranteed to trigger a db error,899 from our list of fake bug watches.
900 the second should update successfuly.
901 """900 """
902 return self.bugtracker.getBugWatchesNeedingUpdate(0)[bug_watch_id - 1]901 return [
903902 bug_watch for bug_watch in (
903 self.bugtracker.getBugWatchesNeedingUpdate(0))
904 if (bug_watch.remotebug == remote_bug_id and
905 bug_watch.id in bug_watch_ids)
906 ]
904907
905908
906class CheckBugWatchesErrorRecoveryTestCase(unittest.TestCase):909class CheckBugWatchesErrorRecoveryTestCase(unittest.TestCase):
907910
=== modified file 'lib/lp/bugs/scripts/tests/test_checkwatches.py'
--- lib/lp/bugs/scripts/tests/test_checkwatches.py 2010-01-04 11:39:14 +0000
+++ lib/lp/bugs/scripts/tests/test_checkwatches.py 2010-01-05 16:11:20 +0000
@@ -9,14 +9,20 @@
99
10from zope.component import getUtility10from zope.component import getUtility
1111
12from canonical.config import config
13from canonical.database.sqlbase import commit
14from canonical.launchpad.ftests import login
15from canonical.launchpad.interfaces import (
16 BugTaskStatus, BugTrackerType, IBugSet, IBugTaskSet,
17 ILaunchpadCelebrities, IPersonSet, IProductSet, IQuestionSet)
12from canonical.launchpad.scripts.logger import QuietFakeLogger18from canonical.launchpad.scripts.logger import QuietFakeLogger
13from canonical.launchpad.interfaces import ILaunchpadCelebrities
14from canonical.testing import LaunchpadZopelessLayer19from canonical.testing import LaunchpadZopelessLayer
1520
16from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI21from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI
17from lp.bugs.scripts import checkwatches22from lp.bugs.scripts import checkwatches
18from lp.bugs.scripts.checkwatches import CheckWatchesErrorUtility23from lp.bugs.scripts.checkwatches import CheckWatchesErrorUtility
19from lp.bugs.tests.externalbugtracker import TestBugzillaAPIXMLRPCTransport24from lp.bugs.tests.externalbugtracker import (
25 TestBugzillaAPIXMLRPCTransport, new_bugtracker)
20from lp.testing import TestCaseWithFactory26from lp.testing import TestCaseWithFactory
2127
2228
@@ -38,37 +44,17 @@
38 def getExternalBugTrackerToUse(self):44 def getExternalBugTrackerToUse(self):
39 return self45 return self
4046
41 def getProductsForRemoteBugs(self, remote_bugs):
42 """Return the products for some remote bugs.
43
44 This method is basically the same as that of the superclass but
45 without the call to initializeRemoteBugDB().
46 """
47 bug_products = {}
48 for bug_id in bug_ids:
49 # If one of the bugs we're trying to get the product for
50 # doesn't exist, just skip it.
51 try:
52 actual_bug_id = self._getActualBugId(bug_id)
53 except BugNotFound:
54 continue
55
56 bug_dict = self._bugs[actual_bug_id]
57 bug_products[bug_id] = bug_dict['product']
58
59 return bug_products
60
6147
62class NoBugWatchesByRemoteBugUpdater(checkwatches.BugWatchUpdater):48class NoBugWatchesByRemoteBugUpdater(checkwatches.BugWatchUpdater):
63 """A subclass of BugWatchUpdater with methods overridden for testing."""49 """A subclass of BugWatchUpdater with methods overridden for testing."""
6450
65 def _getBugWatchesByRemoteBug(self, bug_watch_ids):51 def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
66 """Return an empty dict.52 """Return an empty list.
6753
68 This method overrides _getBugWatchesByRemoteBug() so that bug54 This method overrides _getBugWatchesForRemoteBug() so that bug
69 497141 can be regression-tested.55 497141 can be regression-tested.
70 """56 """
71 return {}57 return []
7258
7359
74class TestCheckwatchesWithSyncableGnomeProducts(TestCaseWithFactory):60class TestCheckwatchesWithSyncableGnomeProducts(TestCaseWithFactory):
@@ -146,5 +132,75 @@
146 last_oops.value.startswith('Spurious remote bug ID'))132 last_oops.value.startswith('Spurious remote bug ID'))
147133
148134
135class TestUpdateBugsWithLinkedQuestions(unittest.TestCase):
136 """Tests for updating bugs with linked questions."""
137
138 layer = LaunchpadZopelessLayer
139
140 def setUp(self):
141 """Set up bugs, watches and questions to test with."""
142 super(TestUpdateBugsWithLinkedQuestions, self).setUp()
143
144 # For test_can_update_bug_with_questions we need a bug that has
145 # a question linked to it.
146 bug_with_question = getUtility(IBugSet).get(10)
147 question = getUtility(IQuestionSet).get(1)
148
149 # XXX gmb 2007-12-11 bug 175545:
150 # We shouldn't have to login() here, but since
151 # database.buglinktarget.BugLinkTargetMixin.linkBug()
152 # doesn't accept a user parameter, instead depending on the
153 # currently logged in user, we get an exception if we don't.
154 login('test@canonical.com')
155 question.linkBug(bug_with_question)
156
157 # We subscribe launchpad_developers to the question since this
158 # indirectly subscribes foo.bar@canonical.com to it, too. We can
159 # then use this to test the updating of a question with indirect
160 # subscribers from a bug watch.
161 question.subscribe(
162 getUtility(ILaunchpadCelebrities).launchpad_developers)
163 commit()
164
165 # We now need to switch to the checkwatches DB user so that
166 # we're testing with the correct set of permissions.
167 self.layer.switchDbUser(config.checkwatches.dbuser)
168
169 # For test_can_update_bug_with_questions we also need a bug
170 # watch and by extension a bug tracker.
171 sample_person = getUtility(IPersonSet).getByEmail(
172 'test@canonical.com')
173 bugtracker = new_bugtracker(BugTrackerType.ROUNDUP)
174 self.bugtask_with_question = getUtility(IBugTaskSet).createTask(
175 bug_with_question, sample_person,
176 product=getUtility(IProductSet).getByName('firefox'))
177 self.bugwatch_with_question = bug_with_question.addWatch(
178 bugtracker, '1', getUtility(ILaunchpadCelebrities).janitor)
179 self.bugtask_with_question.bugwatch = self.bugwatch_with_question
180 commit()
181
182 def test_can_update_bug_with_questions(self):
183 """Test whether bugs with linked questions can be updated.
184
185 This will also test whether indirect subscribers of linked
186 questions will be notified of the changes made when the bugwatch
187 is updated.
188 """
189 # We need to check that the bug task we created in setUp() is
190 # still being referenced by our bug watch.
191 self.assertEqual(self.bugwatch_with_question.bugtasks[0].id,
192 self.bugtask_with_question.id)
193
194 # We can now update the bug watch, which will in turn update the
195 # bug task and the linked question.
196 self.bugwatch_with_question.updateStatus('some status',
197 BugTaskStatus.INPROGRESS)
198 self.assertEqual(self.bugwatch_with_question.bugtasks[0].status,
199 BugTaskStatus.INPROGRESS,
200 "BugTask status is inconsistent. Expected %s but got %s" %
201 (BugTaskStatus.INPROGRESS.title,
202 self.bugtask_with_question.status.title))
203
204
149def test_suite():205def test_suite():
150 return unittest.TestLoader().loadTestsFromName(__name__)206 return unittest.TestLoader().loadTestsFromName(__name__)
151207
=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
--- lib/lp/bugs/tests/test_bugwatch.py 2009-12-15 17:28:51 +0000
+++ lib/lp/bugs/tests/test_bugwatch.py 2010-01-05 16:11:20 +0000
@@ -11,18 +11,20 @@
1111
12from zope.component import getUtility12from zope.component import getUtility
1313
14from canonical.launchpad.ftests import login, ANONYMOUS
14from canonical.launchpad.webapp import urlsplit15from canonical.launchpad.webapp import urlsplit
15from canonical.testing import (16from canonical.testing import (
16 DatabaseFunctionalLayer, LaunchpadFunctionalLayer)17 DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
1718
18from lp.bugs.interfaces.bugtracker import BugTrackerType, IBugTrackerSet19from lp.bugs.interfaces.bugtracker import BugTrackerType, IBugTrackerSet
19from lp.bugs.interfaces.bugwatch import (20from lp.bugs.interfaces.bugwatch import (
20 IBugWatchSet, NoBugTrackerFound, UnrecognizedBugTrackerURL)21 IBugWatchSet, NoBugTrackerFound, UnrecognizedBugTrackerURL)
21from lp.registry.interfaces.person import IPersonSet22from lp.registry.interfaces.person import IPersonSet
22from lp.testing import ANONYMOUS, TestCaseWithFactory, login23
2324from lp.testing import TestCaseWithFactory
2425
25class ExtractBugTrackerAndBugTestBase(unittest.TestCase):26
27class ExtractBugTrackerAndBugTestBase:
26 """Test base for testing BugWatchSet.extractBugTrackerAndBug."""28 """Test base for testing BugWatchSet.extractBugTrackerAndBug."""
27 layer = LaunchpadFunctionalLayer29 layer = LaunchpadFunctionalLayer
2830
@@ -85,7 +87,8 @@
85 "NoBugTrackerFound wasn't raised by extractBugTrackerAndBug")87 "NoBugTrackerFound wasn't raised by extractBugTrackerAndBug")
8688
8789
88class MantisExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):90class MantisExtractBugTrackerAndBugTest(
91 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
89 """Ensure BugWatchSet.extractBugTrackerAndBug works with Mantis URLs."""92 """Ensure BugWatchSet.extractBugTrackerAndBug works with Mantis URLs."""
9093
91 bugtracker_type = BugTrackerType.MANTIS94 bugtracker_type = BugTrackerType.MANTIS
@@ -94,7 +97,8 @@
94 bug_id = '3224'97 bug_id = '3224'
9598
9699
97class BugzillaExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):100class BugzillaExtractBugTrackerAndBugTest(
101 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
98 """Ensure BugWatchSet.extractBugTrackerAndBug works with Bugzilla URLs."""102 """Ensure BugWatchSet.extractBugTrackerAndBug works with Bugzilla URLs."""
99103
100 bugtracker_type = BugTrackerType.BUGZILLA104 bugtracker_type = BugTrackerType.BUGZILLA
@@ -103,7 +107,8 @@
103 bug_id = '3224'107 bug_id = '3224'
104108
105109
106class IssuezillaExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):110class IssuezillaExtractBugTrackerAndBugTest(
111 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
107 """Ensure BugWatchSet.extractBugTrackerAndBug works with Issuezilla.112 """Ensure BugWatchSet.extractBugTrackerAndBug works with Issuezilla.
108113
109 Issuezilla is practically the same as Buzilla, so we treat it as a114 Issuezilla is practically the same as Buzilla, so we treat it as a
@@ -116,7 +121,8 @@
116 bug_id = '3224'121 bug_id = '3224'
117122
118123
119class RoundUpExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):124class RoundUpExtractBugTrackerAndBugTest(
125 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
120 """Ensure BugWatchSet.extractBugTrackerAndBug works with RoundUp URLs."""126 """Ensure BugWatchSet.extractBugTrackerAndBug works with RoundUp URLs."""
121127
122 bugtracker_type = BugTrackerType.ROUNDUP128 bugtracker_type = BugTrackerType.ROUNDUP
@@ -125,7 +131,8 @@
125 bug_id = '377'131 bug_id = '377'
126132
127133
128class TracExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):134class TracExtractBugTrackerAndBugTest(
135 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
129 """Ensure BugWatchSet.extractBugTrackerAndBug works with Trac URLs."""136 """Ensure BugWatchSet.extractBugTrackerAndBug works with Trac URLs."""
130137
131 bugtracker_type = BugTrackerType.TRAC138 bugtracker_type = BugTrackerType.TRAC
@@ -134,7 +141,8 @@
134 bug_id = '42'141 bug_id = '42'
135142
136143
137class DebbugsExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):144class DebbugsExtractBugTrackerAndBugTest(
145 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
138 """Ensure BugWatchSet.extractBugTrackerAndBug works with Debbugs URLs."""146 """Ensure BugWatchSet.extractBugTrackerAndBug works with Debbugs URLs."""
139147
140 bugtracker_type = BugTrackerType.DEBBUGS148 bugtracker_type = BugTrackerType.DEBBUGS
@@ -144,7 +152,7 @@
144152
145153
146class DebbugsExtractBugTrackerAndBugShorthandTest(154class DebbugsExtractBugTrackerAndBugShorthandTest(
147 ExtractBugTrackerAndBugTestBase):155 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
148 """Ensure extractBugTrackerAndBug works for short Debbugs URLs."""156 """Ensure extractBugTrackerAndBug works for short Debbugs URLs."""
149157
150 bugtracker_type = BugTrackerType.DEBBUGS158 bugtracker_type = BugTrackerType.DEBBUGS
@@ -156,7 +164,8 @@
156 # bugs.debian.org is already registered, so no dice.164 # bugs.debian.org is already registered, so no dice.
157 pass165 pass
158166
159class SFExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):167class SFExtractBugTrackerAndBugTest(
168 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
160 """Ensure BugWatchSet.extractBugTrackerAndBug works with SF URLs.169 """Ensure BugWatchSet.extractBugTrackerAndBug works with SF URLs.
161170
162 We have only one SourceForge tracker registered in Launchpad, so we171 We have only one SourceForge tracker registered in Launchpad, so we
@@ -216,7 +225,8 @@
216 bug_id = '1568562'225 bug_id = '1568562'
217226
218227
219class XForgeExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):228class XForgeExtractBugTrackerAndBugTest(
229 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
220 """Ensure extractBugTrackerAndBug works with SourceForge-like URLs.230 """Ensure extractBugTrackerAndBug works with SourceForge-like URLs.
221 """231 """
222232
@@ -228,7 +238,8 @@
228 bug_id = '90812'238 bug_id = '90812'
229239
230240
231class RTExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):241class RTExtractBugTrackerAndBugTest(
242 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
232 """Ensure BugWatchSet.extractBugTrackerAndBug works with RT URLs."""243 """Ensure BugWatchSet.extractBugTrackerAndBug works with RT URLs."""
233244
234 bugtracker_type = BugTrackerType.RT245 bugtracker_type = BugTrackerType.RT
@@ -237,7 +248,8 @@
237 bug_id = '2379'248 bug_id = '2379'
238249
239250
240class CpanExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):251class CpanExtractBugTrackerAndBugTest(
252 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
241 """Ensure BugWatchSet.extractBugTrackerAndBug works with CPAN URLs."""253 """Ensure BugWatchSet.extractBugTrackerAndBug works with CPAN URLs."""
242254
243 bugtracker_type = BugTrackerType.RT255 bugtracker_type = BugTrackerType.RT
@@ -246,7 +258,8 @@
246 bug_id = '2379'258 bug_id = '2379'
247259
248260
249class SavannahExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):261class SavannahExtractBugTrackerAndBugTest(
262 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
250 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savannah URLs.263 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savannah URLs.
251 """264 """
252265
@@ -261,7 +274,8 @@
261 pass274 pass
262275
263276
264class SavaneExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):277class SavaneExtractBugTrackerAndBugTest(
278 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
265 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savane URLs.279 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savane URLs.
266 """280 """
267281
@@ -272,7 +286,7 @@
272286
273287
274class EmailAddressExtractBugTrackerAndBugTest(288class EmailAddressExtractBugTrackerAndBugTest(
275 ExtractBugTrackerAndBugTestBase):289 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
276 """Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.290 """Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.
277 """291 """
278292
@@ -290,7 +304,7 @@
290304
291305
292class PHPProjectBugTrackerExtractBugTrackerAndBugTest(306class PHPProjectBugTrackerExtractBugTrackerAndBugTest(
293 ExtractBugTrackerAndBugTestBase):307 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
294 """Ensure BugWatchSet.extractBugTrackerAndBug works with PHP bug URLs.308 """Ensure BugWatchSet.extractBugTrackerAndBug works with PHP bug URLs.
295 """309 """
296310
@@ -301,7 +315,7 @@
301315
302316
303class GoogleCodeBugTrackerExtractBugTrackerAndBugTest(317class GoogleCodeBugTrackerExtractBugTrackerAndBugTest(
304 ExtractBugTrackerAndBugTestBase):318 ExtractBugTrackerAndBugTestBase, unittest.TestCase):
305 """Ensure BugWatchSet.extractBugTrackerAndBug works for Google Code URLs.319 """Ensure BugWatchSet.extractBugTrackerAndBug works for Google Code URLs.
306 """320 """
307321
@@ -311,6 +325,39 @@
311 bug_id = '12345'325 bug_id = '12345'
312326
313327
328class TestBugWatchSet(TestCaseWithFactory):
329 """Tests for the bugwatch updating system."""
330
331 layer = LaunchpadZopelessLayer
332
333 def test_getBugWatchesForRemoteBug(self):
334 # getBugWatchesForRemoteBug() returns bug watches from that
335 # refer to the remote bug.
336 bug_watches_alice = [
337 self.factory.makeBugWatch(remote_bug="alice"),
338 ]
339 bug_watches_bob = [
340 self.factory.makeBugWatch(remote_bug="bob"),
341 self.factory.makeBugWatch(remote_bug="bob"),
342 ]
343 bug_watch_set = getUtility(IBugWatchSet)
344 # Passing in the remote bug ID gets us every bug watch that
345 # refers to that remote bug.
346 self.failUnlessEqual(
347 set(bug_watches_alice),
348 set(bug_watch_set.getBugWatchesForRemoteBug('alice')))
349 self.failUnlessEqual(
350 set(bug_watches_bob),
351 set(bug_watch_set.getBugWatchesForRemoteBug('bob')))
352 # The search can be narrowed by passing in a list or other
353 # iterable collection of bug watch IDs.
354 bug_watches_limited = bug_watches_alice + bug_watches_bob[:1]
355 self.failUnlessEqual(
356 set(bug_watches_bob[:1]),
357 set(bug_watch_set.getBugWatchesForRemoteBug('bob', [
358 bug_watch.id for bug_watch in bug_watches_limited])))
359
360
314class TestBugWatchBugTasks(TestCaseWithFactory):361class TestBugWatchBugTasks(TestCaseWithFactory):
315362
316 layer = DatabaseFunctionalLayer363 layer = DatabaseFunctionalLayer
@@ -326,31 +373,4 @@
326373
327374
328def test_suite():375def test_suite():
329 suite = unittest.TestSuite()376 return unittest.TestLoader().loadTestsFromName(__name__)
330 suite.addTest(unittest.makeSuite(BugzillaExtractBugTrackerAndBugTest))
331 suite.addTest(unittest.makeSuite(IssuezillaExtractBugTrackerAndBugTest))
332 suite.addTest(unittest.makeSuite(RoundUpExtractBugTrackerAndBugTest))
333 suite.addTest(unittest.makeSuite(TracExtractBugTrackerAndBugTest))
334 suite.addTest(unittest.makeSuite(DebbugsExtractBugTrackerAndBugTest))
335 suite.addTest(
336 unittest.makeSuite(DebbugsExtractBugTrackerAndBugShorthandTest))
337 suite.addTest(unittest.makeSuite(SFExtractBugTrackerAndBugTest))
338 suite.addTest(unittest.makeSuite(SFTracker2ExtractBugTrackerAndBugTest))
339 suite.addTest(unittest.makeSuite(XForgeExtractBugTrackerAndBugTest))
340 suite.addTest(unittest.makeSuite(MantisExtractBugTrackerAndBugTest))
341 suite.addTest(unittest.makeSuite(RTExtractBugTrackerAndBugTest))
342 suite.addTest(unittest.makeSuite(CpanExtractBugTrackerAndBugTest))
343 suite.addTest(unittest.makeSuite(SavannahExtractBugTrackerAndBugTest))
344 suite.addTest(unittest.makeSuite(SavaneExtractBugTrackerAndBugTest))
345 suite.addTest(unittest.makeSuite(EmailAddressExtractBugTrackerAndBugTest))
346 suite.addTest(unittest.makeSuite(
347 PHPProjectBugTrackerExtractBugTrackerAndBugTest))
348 suite.addTest(unittest.makeSuite(
349 GoogleCodeBugTrackerExtractBugTrackerAndBugTest))
350 suite.addTest(unittest.makeSuite(TestBugWatchBugTasks))
351 return suite
352
353
354if __name__ == '__main__':
355 unittest.main()
356
357377
=== removed file 'lib/lp/translations/scripts/tests/__init__.py.moved'