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
1=== removed file 'lib/canonical/launchpad/scripts/ftests/test_checkwatches.py'
2--- lib/canonical/launchpad/scripts/ftests/test_checkwatches.py 2009-06-25 05:30:52 +0000
3+++ lib/canonical/launchpad/scripts/ftests/test_checkwatches.py 1970-01-01 00:00:00 +0000
4@@ -1,93 +0,0 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6-# GNU Affero General Public License version 3 (see the file LICENSE).
7-
8-"""Tests for the checkwatches remote bug synchronisation code."""
9-
10-__metaclass__ = type
11-__all__ = []
12-
13-import unittest
14-
15-from zope.component import getUtility
16-
17-from canonical.config import config
18-from canonical.database.sqlbase import commit
19-from canonical.launchpad.ftests import login
20-from lp.bugs.tests.externalbugtracker import new_bugtracker
21-from canonical.launchpad.interfaces import (BugTaskStatus, BugTrackerType,
22- IBugSet, IBugTaskSet, ILaunchpadCelebrities, IPersonSet,
23- IProductSet, IQuestionSet)
24-from canonical.testing import LaunchpadZopelessLayer
25-
26-
27-class TestCheckwatches(unittest.TestCase):
28- """Tests for the bugwatch updating system."""
29- layer = LaunchpadZopelessLayer
30-
31- def setUp(self):
32- """Set up bugs, watches and questions to test with."""
33- super(TestCheckwatches, self).setUp()
34-
35- # For test_can_update_bug_with_questions we need a bug that has
36- # a question linked to it.
37- bug_with_question = getUtility(IBugSet).get(10)
38- question = getUtility(IQuestionSet).get(1)
39-
40- # XXX gmb 2007-12-11 bug 175545:
41- # We shouldn't have to login() here, but since
42- # database.buglinktarget.BugLinkTargetMixin.linkBug()
43- # doesn't accept a user parameter, instead depending on the
44- # currently logged in user, we get an exception if we don't.
45- login('test@canonical.com')
46- question.linkBug(bug_with_question)
47-
48- # We subscribe launchpad_developers to the question since this
49- # indirectly subscribes foo.bar@canonical.com to it, too. We can
50- # then use this to test the updating of a question with indirect
51- # subscribers from a bug watch.
52- question.subscribe(
53- getUtility(ILaunchpadCelebrities).launchpad_developers)
54- commit()
55-
56- # We now need to switch to the checkwatches DB user so that
57- # we're testing with the correct set of permissions.
58- self.layer.switchDbUser(config.checkwatches.dbuser)
59-
60- # For test_can_update_bug_with_questions we also need a bug
61- # watch and by extension a bug tracker.
62- sample_person = getUtility(IPersonSet).getByEmail(
63- 'test@canonical.com')
64- bugtracker = new_bugtracker(BugTrackerType.ROUNDUP)
65- self.bugtask_with_question = getUtility(IBugTaskSet).createTask(
66- bug_with_question, sample_person,
67- product=getUtility(IProductSet).getByName('firefox'))
68- self.bugwatch_with_question = bug_with_question.addWatch(
69- bugtracker, '1', getUtility(ILaunchpadCelebrities).janitor)
70- self.bugtask_with_question.bugwatch = self.bugwatch_with_question
71- commit()
72-
73- def test_can_update_bug_with_questions(self):
74- """Test whether bugs with linked questions can be updated.
75-
76- This will also test whether indirect subscribers of linked
77- questions will be notified of the changes made when the bugwatch
78- is updated.
79- """
80- # We need to check that the bug task we created in setUp() is
81- # still being referenced by our bug watch.
82- self.assertEqual(self.bugwatch_with_question.bugtasks[0].id,
83- self.bugtask_with_question.id)
84-
85- # We can now update the bug watch, which will in turn update the
86- # bug task and the linked question.
87- self.bugwatch_with_question.updateStatus('some status',
88- BugTaskStatus.INPROGRESS)
89- self.assertEqual(self.bugwatch_with_question.bugtasks[0].status,
90- BugTaskStatus.INPROGRESS,
91- "BugTask status is inconsistent. Expected %s but got %s" %
92- (BugTaskStatus.INPROGRESS.title,
93- self.bugtask_with_question.status.title))
94-
95-
96-def test_suite():
97- return unittest.TestLoader().loadTestsFromName(__name__)
98
99=== modified file 'lib/lp/bugs/doc/externalbugtracker.txt'
100--- lib/lp/bugs/doc/externalbugtracker.txt 2009-11-28 08:31:15 +0000
101+++ lib/lp/bugs/doc/externalbugtracker.txt 2010-01-05 16:11:20 +0000
102@@ -42,7 +42,6 @@
103 ... InitializingExternalBugTracker(), [])
104 COMMIT
105 initializeRemoteBugDB() called: []
106- BEGIN
107
108
109 === Choosing another ExternalBugTracker instance ===
110@@ -318,7 +317,6 @@
111 COMMIT
112 getCurrentDBTime() called
113 initializeRemoteBugDB() called: []
114- BEGIN
115
116 If the difference between what we and the remote system think the time
117 is, an error is raised.
118@@ -370,7 +368,6 @@
119 >>> bug_watch_updater.updateBugWatches(
120 ... CorrectTimeExternalBugTracker(), [], now=utc_now)
121 COMMIT
122- BEGIN
123
124 If the timezone is known, the local time time should be returned, rather
125 than the UTC time.
126@@ -383,7 +380,6 @@
127 >>> bug_watch_updater.updateBugWatches(
128 ... LocalTimeExternalBugTracker(), [], now=utc_now)
129 COMMIT
130- BEGIN
131
132 If the remote server time is unknown, we will refuse to import any
133 comments from it. Bug watches will still be updated, but a warning is
134@@ -399,7 +395,6 @@
135 COMMIT
136 getCurrentDBTime() called
137 initializeRemoteBugDB() called: []
138- BEGIN
139 WARNING:...:Comment importing supported, but server time can't be
140 trusted. No comments will be imported.
141
142@@ -494,9 +489,16 @@
143 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
144 BEGIN
145 getRemoteStatus() called: u'1'
146+ COMMIT
147+ BEGIN
148 getRemoteStatus() called: u'2'
149+ COMMIT
150+ BEGIN
151 getRemoteStatus() called: u'3'
152+ COMMIT
153+ BEGIN
154 getRemoteStatus() called: u'4'
155+ COMMIT
156
157 If the bug watches have the lastchecked attribute set, they will be
158 passed to getModifiedRemoteBugs(). Only the bugs that have been modified
159@@ -530,7 +532,12 @@
160 initializeRemoteBugDB() called: [u'1', u'4']
161 BEGIN
162 getRemoteStatus() called: u'1'
163+ COMMIT
164+ BEGIN
165 getRemoteStatus() called: u'4'
166+ COMMIT
167+ BEGIN
168+ BEGIN
169
170 The bug watches that are deemed as not being modified are still marked
171 as being checked.
172@@ -580,9 +587,16 @@
173 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
174 BEGIN
175 getRemoteStatus() called: u'1'
176+ COMMIT
177+ BEGIN
178 getRemoteStatus() called: u'2'
179+ COMMIT
180+ BEGIN
181 getRemoteStatus() called: u'3'
182+ COMMIT
183+ BEGIN
184 getRemoteStatus() called: u'4'
185+ COMMIT
186
187 As mentioned earlier, getModifiedRemoteBugs() is only called if we can
188 get the current time of the remote system. If the time is unknown, we
189@@ -600,9 +614,16 @@
190 initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
191 BEGIN
192 getRemoteStatus() called: u'1'
193+ COMMIT
194+ BEGIN
195 getRemoteStatus() called: u'2'
196+ COMMIT
197+ BEGIN
198 getRemoteStatus() called: u'3'
199+ COMMIT
200+ BEGIN
201 getRemoteStatus() called: u'4'
202+ COMMIT
203
204 The only exception to the rule of only updating modified bugs is the set
205 of bug watches which have comments that need to be pushed to the remote
206@@ -1102,7 +1123,7 @@
207 >>> bug_watch_updater.updateBugTrackers(
208 ... bug_tracker_names=[standard_bugzilla.name], batch_size=2)
209 DEBUG Using a global batch size of 2
210- INFO Updating 4 watches for 2 bugs on http://example.com
211+ INFO Updating 2 watches for 2 bugs on http://example.com
212 initializeRemoteBugDB() called: [u'5', u'6']
213 getRemoteStatus() called: u'5'
214 getRemoteStatus() called: u'6'
215
216=== modified file 'lib/lp/bugs/interfaces/bugwatch.py'
217--- lib/lp/bugs/interfaces/bugwatch.py 2009-09-04 09:48:04 +0000
218+++ lib/lp/bugs/interfaces/bugwatch.py 2010-01-05 16:11:20 +0000
219@@ -268,6 +268,19 @@
220 If no bug tracker type can be guessed, None is returned.
221 """
222
223+ def getBugWatchesForRemoteBug(self, remote_bug, bug_watch_ids=None):
224+ """Returns bug watches referring to the given remote bug.
225+
226+ Returns a set of those bug watches, optionally limited to
227+ those with IDs in `bug_watch_ids`, that refer to `remote_bug`.
228+
229+ :param remote_bug_id: The ID of the remote bug.
230+ :type remote_bug_id: See `IBugWatch.remotebug`.
231+
232+ :param bug_watch_ids: A collection of `BugWatch` IDs.
233+ :type bug_watch_ids: An iterable of `int`s, or `None`.
234+ """
235+
236
237 class NoBugTrackerFound(Exception):
238 """No bug tracker with the base_url is registered in Launchpad."""
239
240=== modified file 'lib/lp/bugs/interfaces/externalbugtracker.py'
241--- lib/lp/bugs/interfaces/externalbugtracker.py 2009-06-25 00:40:31 +0000
242+++ lib/lp/bugs/interfaces/externalbugtracker.py 2010-01-05 16:11:20 +0000
243@@ -43,35 +43,56 @@
244 """Return the `ExternalBugTracker` instance to use.
245
246 Probe the remote bug tracker and choose the right
247- `ExternalBugTracker` instance to use further on.
248+ `ExternalBugTracker` instance to use further on. In most cases
249+ this will simply return `self`.
250 """
251
252 def getCurrentDBTime():
253 """Return the current time of the bug tracker's DB server.
254
255 The current time will be returned as a timezone-aware datetime.
256+
257+ :return: `datetime.datetime` with timezone.
258 """
259
260 def getModifiedRemoteBugs(remote_bug_ids, last_checked):
261 """Return the bug ids that have been modified.
262
263 Return all ids if the modified bugs can't be determined.
264+
265+ :param remote_bug_ids: The remote bug IDs to be checked.
266+ :type remote_bug_ids: `list` of strings
267+
268+ :param last_checked: The date and time since when a bug should
269+ be considered modified.
270+ :param last_checked: `datetime.datetime`
271 """
272
273 def initializeRemoteBugDB(remote_bug_ids):
274- """Do any initialization before each bug watch is updated."""
275+ """Do any initialization before each bug watch is updated.
276+
277+ :param remote_bug_ids: The remote bug IDs that to be checked.
278+ :type remote_bug_ids: `list` of strings
279+ """
280
281 def convertRemoteStatus(remote_status):
282- """Convert a remote status string to a BugTaskStatus item."""
283+ """Convert a remote status string to a BugTaskStatus item.
284+
285+ :return: a member of `BugTaskStatus`
286+ """
287
288 def convertRemoteImportance(remote_importance):
289- """Convert a remote importance to a BugTaskImportance item."""
290+ """Convert a remote importance to a BugTaskImportance item.
291+
292+ :return: a member of `BugTaskImportance`
293+ """
294
295 def getRemoteProduct(remote_bug):
296 """Return the remote product for a given remote bug.
297
298 :param remote_bug: The ID of the remote bug for which to return
299 the remote product.
300+ :type remote_bug: string
301 :return: The remote product for `remote_bug`. If no remote
302 product is recorded for `remote_bug` return None.
303 :raise BugNotFound: If `remote_bug` doesn't exist for the bug
304@@ -123,16 +144,19 @@
305 def getBugReporter(remote_bug):
306 """Return the person who submitted the given bug.
307
308- A tuple of (display name, email) is returned.
309+ :return: `tuple` of (display name, email)
310 """
311
312 def getBugSummaryAndDescription(remote_bug):
313- """Return a tuple of summary and description for the given bug."""
314+ """Return the summary and description for the given bug.
315+
316+ :return: `tuple` of (summary, description)
317+ """
318
319 def getBugTargetName(remote_bug):
320 """Return the specific target name of the bug.
321
322- Return None if no target can be determined.
323+ :return: string, or `None` if no target can be determined
324 """
325
326
327
328=== modified file 'lib/lp/bugs/model/bugwatch.py'
329--- lib/lp/bugs/model/bugwatch.py 2009-12-24 06:44:57 +0000
330+++ lib/lp/bugs/model/bugwatch.py 2010-01-05 16:11:20 +0000
331@@ -17,7 +17,7 @@
332 # SQL imports
333 from sqlobject import ForeignKey, SQLObjectNotFound, StringCol
334
335-from storm.expr import Desc, Not
336+from storm.expr import Desc, In, Not
337 from storm.store import Store
338
339 from lazr.lifecycle.event import ObjectModifiedEvent
340@@ -31,6 +31,7 @@
341 from canonical.launchpad.database.message import Message
342 from canonical.launchpad.helpers import shortlist
343 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
344+from canonical.launchpad.interfaces.lpstorm import IStore
345 from canonical.launchpad.validators.email import valid_email
346 from canonical.launchpad.webapp import urlappend, urlsplit
347 from canonical.launchpad.webapp.interfaces import NotFoundError
348@@ -603,3 +604,10 @@
349
350 raise UnrecognizedBugTrackerURL(url)
351
352+ def getBugWatchesForRemoteBug(self, remote_bug, bug_watch_ids=None):
353+ """See `IBugWatchSet`."""
354+ query = IStore(BugWatch).find(
355+ BugWatch, BugWatch.remotebug == remote_bug)
356+ if bug_watch_ids is not None:
357+ query = query.find(In(BugWatch.id, bug_watch_ids))
358+ return query
359
360=== modified file 'lib/lp/bugs/scripts/checkwatches.py'
361--- lib/lp/bugs/scripts/checkwatches.py 2009-12-16 16:50:24 +0000
362+++ lib/lp/bugs/scripts/checkwatches.py 2010-01-05 16:11:20 +0000
363@@ -414,26 +414,6 @@
364 """Return the bug watch with id `bug_watch_id`."""
365 return getUtility(IBugWatchSet).get(bug_watch_id)
366
367- def _getBugWatchesByRemoteBug(self, bug_watch_ids):
368- """Returns a dictionary of bug watches mapped to remote bugs.
369-
370- For each bug watch id fetches the corresponding bug watch and
371- appends it to a list of bug watches pointing to one remote
372- bug - the key of the returned mapping.
373- """
374- bug_watches_by_remote_bug = {}
375- for bug_watch_id in bug_watch_ids:
376- bug_watch = self._getBugWatch(bug_watch_id)
377- remote_bug = bug_watch.remotebug
378- # There can be multiple bug watches pointing to the same
379- # remote bug; because of that, we need to store lists of bug
380- # watches related to the remote bug, and later update the
381- # status of each one of them.
382- if remote_bug not in bug_watches_by_remote_bug:
383- bug_watches_by_remote_bug[remote_bug] = []
384- bug_watches_by_remote_bug[remote_bug].append(bug_watch)
385- return bug_watches_by_remote_bug
386-
387 def _getExternalBugTrackersAndWatches(self, bug_tracker, bug_watches):
388 """Return an `ExternalBugTracker` instance for `bug_tracker`."""
389 remotesystem = externalbugtracker.get_external_bugtracker(
390@@ -686,6 +666,17 @@
391 'unmodified_remote_ids': unmodified_remote_ids,
392 }
393
394+ def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
395+ """Return a list of bug watches for the given remote bug.
396+
397+ The returned watches will all be members of `bug_watch_ids`.
398+
399+ This method exists primarily to be overridden during testing.
400+ """
401+ return list(
402+ getUtility(IBugWatchSet).getBugWatchesForRemoteBug(
403+ remote_bug_id, bug_watch_ids))
404+
405 # XXX gmb 2008-11-07 [bug=295319]
406 # This method is 186 lines long. It needs to be shorter.
407 def updateBugWatches(self, remotesystem, bug_watches_to_update, now=None,
408@@ -752,10 +743,6 @@
409 self.txn.commit()
410 raise
411
412- self.txn.begin()
413- bug_watches_by_remote_bug = self._getBugWatchesByRemoteBug(
414- bug_watch_ids)
415-
416 # Whether we can import and / or push comments is determined on
417 # a per-bugtracker-type level.
418 can_import_comments = (
419@@ -788,26 +775,32 @@
420 "local bugs: %(local_ids)s"
421 )
422
423- for bug_id in all_remote_ids:
424- try:
425- bug_watches = bug_watches_by_remote_bug[bug_id]
426- except KeyError:
427+ for remote_bug_id in all_remote_ids:
428+ # Start a fresh transaction every time round the loop.
429+ self.txn.begin()
430+
431+ bug_watches = self._getBugWatchesForRemoteBug(
432+ remote_bug_id, bug_watch_ids)
433+ if len(bug_watches) == 0:
434 # If there aren't any bug watches for this remote bug,
435 # just log a warning and carry on.
436 self.warning(
437 "Spurious remote bug ID: No watches found for "
438- "remote bug %s on %s" % (bug_id, remotesystem.baseurl))
439+ "remote bug %s on %s" % (
440+ remote_bug_id, remotesystem.baseurl))
441 continue
442
443 for bug_watch in bug_watches:
444 bug_watch.lastchecked = UTC_NOW
445- if bug_id in unmodified_remote_ids:
446+ if remote_bug_id in unmodified_remote_ids:
447 continue
448
449 # Save the remote bug URL in case we need to log an error.
450 remote_bug_url = bug_watches[0].url
451
452- local_ids = ", ".join(str(watch.bug.id) for watch in bug_watches)
453+ local_ids = ", ".join(
454+ str(bug_id) for bug_id in sorted(
455+ watch.bug.id for watch in bug_watches))
456 try:
457 new_remote_status = None
458 new_malone_status = None
459@@ -820,12 +813,13 @@
460 # necessary and can be refactored out when bug
461 # 136391 is dealt with.
462 try:
463- new_remote_status = remotesystem.getRemoteStatus(bug_id)
464+ new_remote_status = (
465+ remotesystem.getRemoteStatus(remote_bug_id))
466 new_malone_status = self._convertRemoteStatus(
467 remotesystem, new_remote_status)
468
469- new_remote_importance = remotesystem.getRemoteImportance(
470- bug_id)
471+ new_remote_importance = (
472+ remotesystem.getRemoteImportance(remote_bug_id))
473 new_malone_importance = (
474 remotesystem.convertRemoteImportance(
475 new_remote_importance))
476@@ -835,13 +829,13 @@
477 error, error_type_message_default)
478 self.warning(
479 message % {
480- 'bug_id': bug_id,
481+ 'bug_id': remote_bug_id,
482 'base_url': remotesystem.baseurl,
483 'local_ids': local_ids,
484 },
485 properties=[
486 ('URL', remote_bug_url),
487- ('bug_id', bug_id),
488+ ('bug_id', remote_bug_id),
489 ('local_ids', local_ids),
490 ] + self._getOOPSProperties(remotesystem),
491 info=sys.exc_info())
492@@ -868,17 +862,11 @@
493 except (KeyboardInterrupt, SystemExit):
494 # We should never catch KeyboardInterrupt or SystemExit.
495 raise
496+
497 except Exception, error:
498- # If something unexpected goes wrong, we shouldn't break the
499- # updating of the other bugs.
500-
501- # Restart the transaction so that subsequent
502- # bug watches will get recorded.
503+ # Restart transaction before recording the error.
504 self.txn.abort()
505 self.txn.begin()
506- bug_watches_by_remote_bug = self._getBugWatchesByRemoteBug(
507- bug_watch_ids)
508-
509 # We record errors against the bug watches and update
510 # their lastchecked dates so that we don't try to
511 # re-check them every time checkwatches runs.
512@@ -889,17 +877,20 @@
513 # We need to commit the transaction, in case the next
514 # bug fails to update as well.
515 self.txn.commit()
516- self.txn.begin()
517-
518+ # Send the error to the log too.
519 self.error(
520 "Failure updating bug %r on %s (local bugs: %s)." %
521- (bug_id, bug_tracker_url, local_ids),
522+ (remote_bug_id, bug_tracker_url, local_ids),
523 properties=[
524 ('URL', remote_bug_url),
525- ('bug_id', bug_id),
526+ ('bug_id', remote_bug_id),
527 ('local_ids', local_ids)] +
528 self._getOOPSProperties(remotesystem))
529
530+ else:
531+ # All is well, save it now.
532+ self.txn.commit()
533+
534 def importBug(self, external_bugtracker, bugtracker, bug_target,
535 remote_bug):
536 """Import a remote bug into Launchpad.
537
538=== modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py'
539--- lib/lp/bugs/scripts/tests/test_bugimport.py 2009-11-27 16:16:12 +0000
540+++ lib/lp/bugs/scripts/tests/test_bugimport.py 2010-01-05 16:11:20 +0000
541@@ -892,15 +892,18 @@
542 """See `BugWatchUpdater`."""
543 return [(TestExternalBugTracker(bug_tracker.baseurl), bug_watches)]
544
545- def _getBugWatch(self, bug_watch_id):
546- """Returns a mock bug watch object.
547+ def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
548+ """Returns a list of fake bug watch objects.
549
550- We override this method to force one of our two bug watches
551- to be returned. The first is guaranteed to trigger a db error,
552- the second should update successfuly.
553+ We override this method so that we always return bug watches
554+ from our list of fake bug watches.
555 """
556- return self.bugtracker.getBugWatchesNeedingUpdate(0)[bug_watch_id - 1]
557-
558+ return [
559+ bug_watch for bug_watch in (
560+ self.bugtracker.getBugWatchesNeedingUpdate(0))
561+ if (bug_watch.remotebug == remote_bug_id and
562+ bug_watch.id in bug_watch_ids)
563+ ]
564
565
566 class CheckBugWatchesErrorRecoveryTestCase(unittest.TestCase):
567
568=== modified file 'lib/lp/bugs/scripts/tests/test_checkwatches.py'
569--- lib/lp/bugs/scripts/tests/test_checkwatches.py 2010-01-04 11:39:14 +0000
570+++ lib/lp/bugs/scripts/tests/test_checkwatches.py 2010-01-05 16:11:20 +0000
571@@ -9,14 +9,20 @@
572
573 from zope.component import getUtility
574
575+from canonical.config import config
576+from canonical.database.sqlbase import commit
577+from canonical.launchpad.ftests import login
578+from canonical.launchpad.interfaces import (
579+ BugTaskStatus, BugTrackerType, IBugSet, IBugTaskSet,
580+ ILaunchpadCelebrities, IPersonSet, IProductSet, IQuestionSet)
581 from canonical.launchpad.scripts.logger import QuietFakeLogger
582-from canonical.launchpad.interfaces import ILaunchpadCelebrities
583 from canonical.testing import LaunchpadZopelessLayer
584
585 from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI
586 from lp.bugs.scripts import checkwatches
587 from lp.bugs.scripts.checkwatches import CheckWatchesErrorUtility
588-from lp.bugs.tests.externalbugtracker import TestBugzillaAPIXMLRPCTransport
589+from lp.bugs.tests.externalbugtracker import (
590+ TestBugzillaAPIXMLRPCTransport, new_bugtracker)
591 from lp.testing import TestCaseWithFactory
592
593
594@@ -38,37 +44,17 @@
595 def getExternalBugTrackerToUse(self):
596 return self
597
598- def getProductsForRemoteBugs(self, remote_bugs):
599- """Return the products for some remote bugs.
600-
601- This method is basically the same as that of the superclass but
602- without the call to initializeRemoteBugDB().
603- """
604- bug_products = {}
605- for bug_id in bug_ids:
606- # If one of the bugs we're trying to get the product for
607- # doesn't exist, just skip it.
608- try:
609- actual_bug_id = self._getActualBugId(bug_id)
610- except BugNotFound:
611- continue
612-
613- bug_dict = self._bugs[actual_bug_id]
614- bug_products[bug_id] = bug_dict['product']
615-
616- return bug_products
617-
618
619 class NoBugWatchesByRemoteBugUpdater(checkwatches.BugWatchUpdater):
620 """A subclass of BugWatchUpdater with methods overridden for testing."""
621
622- def _getBugWatchesByRemoteBug(self, bug_watch_ids):
623- """Return an empty dict.
624+ def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids):
625+ """Return an empty list.
626
627- This method overrides _getBugWatchesByRemoteBug() so that bug
628+ This method overrides _getBugWatchesForRemoteBug() so that bug
629 497141 can be regression-tested.
630 """
631- return {}
632+ return []
633
634
635 class TestCheckwatchesWithSyncableGnomeProducts(TestCaseWithFactory):
636@@ -146,5 +132,75 @@
637 last_oops.value.startswith('Spurious remote bug ID'))
638
639
640+class TestUpdateBugsWithLinkedQuestions(unittest.TestCase):
641+ """Tests for updating bugs with linked questions."""
642+
643+ layer = LaunchpadZopelessLayer
644+
645+ def setUp(self):
646+ """Set up bugs, watches and questions to test with."""
647+ super(TestUpdateBugsWithLinkedQuestions, self).setUp()
648+
649+ # For test_can_update_bug_with_questions we need a bug that has
650+ # a question linked to it.
651+ bug_with_question = getUtility(IBugSet).get(10)
652+ question = getUtility(IQuestionSet).get(1)
653+
654+ # XXX gmb 2007-12-11 bug 175545:
655+ # We shouldn't have to login() here, but since
656+ # database.buglinktarget.BugLinkTargetMixin.linkBug()
657+ # doesn't accept a user parameter, instead depending on the
658+ # currently logged in user, we get an exception if we don't.
659+ login('test@canonical.com')
660+ question.linkBug(bug_with_question)
661+
662+ # We subscribe launchpad_developers to the question since this
663+ # indirectly subscribes foo.bar@canonical.com to it, too. We can
664+ # then use this to test the updating of a question with indirect
665+ # subscribers from a bug watch.
666+ question.subscribe(
667+ getUtility(ILaunchpadCelebrities).launchpad_developers)
668+ commit()
669+
670+ # We now need to switch to the checkwatches DB user so that
671+ # we're testing with the correct set of permissions.
672+ self.layer.switchDbUser(config.checkwatches.dbuser)
673+
674+ # For test_can_update_bug_with_questions we also need a bug
675+ # watch and by extension a bug tracker.
676+ sample_person = getUtility(IPersonSet).getByEmail(
677+ 'test@canonical.com')
678+ bugtracker = new_bugtracker(BugTrackerType.ROUNDUP)
679+ self.bugtask_with_question = getUtility(IBugTaskSet).createTask(
680+ bug_with_question, sample_person,
681+ product=getUtility(IProductSet).getByName('firefox'))
682+ self.bugwatch_with_question = bug_with_question.addWatch(
683+ bugtracker, '1', getUtility(ILaunchpadCelebrities).janitor)
684+ self.bugtask_with_question.bugwatch = self.bugwatch_with_question
685+ commit()
686+
687+ def test_can_update_bug_with_questions(self):
688+ """Test whether bugs with linked questions can be updated.
689+
690+ This will also test whether indirect subscribers of linked
691+ questions will be notified of the changes made when the bugwatch
692+ is updated.
693+ """
694+ # We need to check that the bug task we created in setUp() is
695+ # still being referenced by our bug watch.
696+ self.assertEqual(self.bugwatch_with_question.bugtasks[0].id,
697+ self.bugtask_with_question.id)
698+
699+ # We can now update the bug watch, which will in turn update the
700+ # bug task and the linked question.
701+ self.bugwatch_with_question.updateStatus('some status',
702+ BugTaskStatus.INPROGRESS)
703+ self.assertEqual(self.bugwatch_with_question.bugtasks[0].status,
704+ BugTaskStatus.INPROGRESS,
705+ "BugTask status is inconsistent. Expected %s but got %s" %
706+ (BugTaskStatus.INPROGRESS.title,
707+ self.bugtask_with_question.status.title))
708+
709+
710 def test_suite():
711 return unittest.TestLoader().loadTestsFromName(__name__)
712
713=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
714--- lib/lp/bugs/tests/test_bugwatch.py 2009-12-15 17:28:51 +0000
715+++ lib/lp/bugs/tests/test_bugwatch.py 2010-01-05 16:11:20 +0000
716@@ -11,18 +11,20 @@
717
718 from zope.component import getUtility
719
720+from canonical.launchpad.ftests import login, ANONYMOUS
721 from canonical.launchpad.webapp import urlsplit
722 from canonical.testing import (
723- DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
724+ DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
725
726 from lp.bugs.interfaces.bugtracker import BugTrackerType, IBugTrackerSet
727 from lp.bugs.interfaces.bugwatch import (
728 IBugWatchSet, NoBugTrackerFound, UnrecognizedBugTrackerURL)
729 from lp.registry.interfaces.person import IPersonSet
730-from lp.testing import ANONYMOUS, TestCaseWithFactory, login
731-
732-
733-class ExtractBugTrackerAndBugTestBase(unittest.TestCase):
734+
735+from lp.testing import TestCaseWithFactory
736+
737+
738+class ExtractBugTrackerAndBugTestBase:
739 """Test base for testing BugWatchSet.extractBugTrackerAndBug."""
740 layer = LaunchpadFunctionalLayer
741
742@@ -85,7 +87,8 @@
743 "NoBugTrackerFound wasn't raised by extractBugTrackerAndBug")
744
745
746-class MantisExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
747+class MantisExtractBugTrackerAndBugTest(
748+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
749 """Ensure BugWatchSet.extractBugTrackerAndBug works with Mantis URLs."""
750
751 bugtracker_type = BugTrackerType.MANTIS
752@@ -94,7 +97,8 @@
753 bug_id = '3224'
754
755
756-class BugzillaExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
757+class BugzillaExtractBugTrackerAndBugTest(
758+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
759 """Ensure BugWatchSet.extractBugTrackerAndBug works with Bugzilla URLs."""
760
761 bugtracker_type = BugTrackerType.BUGZILLA
762@@ -103,7 +107,8 @@
763 bug_id = '3224'
764
765
766-class IssuezillaExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
767+class IssuezillaExtractBugTrackerAndBugTest(
768+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
769 """Ensure BugWatchSet.extractBugTrackerAndBug works with Issuezilla.
770
771 Issuezilla is practically the same as Buzilla, so we treat it as a
772@@ -116,7 +121,8 @@
773 bug_id = '3224'
774
775
776-class RoundUpExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
777+class RoundUpExtractBugTrackerAndBugTest(
778+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
779 """Ensure BugWatchSet.extractBugTrackerAndBug works with RoundUp URLs."""
780
781 bugtracker_type = BugTrackerType.ROUNDUP
782@@ -125,7 +131,8 @@
783 bug_id = '377'
784
785
786-class TracExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
787+class TracExtractBugTrackerAndBugTest(
788+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
789 """Ensure BugWatchSet.extractBugTrackerAndBug works with Trac URLs."""
790
791 bugtracker_type = BugTrackerType.TRAC
792@@ -134,7 +141,8 @@
793 bug_id = '42'
794
795
796-class DebbugsExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
797+class DebbugsExtractBugTrackerAndBugTest(
798+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
799 """Ensure BugWatchSet.extractBugTrackerAndBug works with Debbugs URLs."""
800
801 bugtracker_type = BugTrackerType.DEBBUGS
802@@ -144,7 +152,7 @@
803
804
805 class DebbugsExtractBugTrackerAndBugShorthandTest(
806- ExtractBugTrackerAndBugTestBase):
807+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
808 """Ensure extractBugTrackerAndBug works for short Debbugs URLs."""
809
810 bugtracker_type = BugTrackerType.DEBBUGS
811@@ -156,7 +164,8 @@
812 # bugs.debian.org is already registered, so no dice.
813 pass
814
815-class SFExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
816+class SFExtractBugTrackerAndBugTest(
817+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
818 """Ensure BugWatchSet.extractBugTrackerAndBug works with SF URLs.
819
820 We have only one SourceForge tracker registered in Launchpad, so we
821@@ -216,7 +225,8 @@
822 bug_id = '1568562'
823
824
825-class XForgeExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
826+class XForgeExtractBugTrackerAndBugTest(
827+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
828 """Ensure extractBugTrackerAndBug works with SourceForge-like URLs.
829 """
830
831@@ -228,7 +238,8 @@
832 bug_id = '90812'
833
834
835-class RTExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
836+class RTExtractBugTrackerAndBugTest(
837+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
838 """Ensure BugWatchSet.extractBugTrackerAndBug works with RT URLs."""
839
840 bugtracker_type = BugTrackerType.RT
841@@ -237,7 +248,8 @@
842 bug_id = '2379'
843
844
845-class CpanExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
846+class CpanExtractBugTrackerAndBugTest(
847+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
848 """Ensure BugWatchSet.extractBugTrackerAndBug works with CPAN URLs."""
849
850 bugtracker_type = BugTrackerType.RT
851@@ -246,7 +258,8 @@
852 bug_id = '2379'
853
854
855-class SavannahExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
856+class SavannahExtractBugTrackerAndBugTest(
857+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
858 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savannah URLs.
859 """
860
861@@ -261,7 +274,8 @@
862 pass
863
864
865-class SavaneExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTestBase):
866+class SavaneExtractBugTrackerAndBugTest(
867+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
868 """Ensure BugWatchSet.extractBugTrackerAndBug works with Savane URLs.
869 """
870
871@@ -272,7 +286,7 @@
872
873
874 class EmailAddressExtractBugTrackerAndBugTest(
875- ExtractBugTrackerAndBugTestBase):
876+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
877 """Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.
878 """
879
880@@ -290,7 +304,7 @@
881
882
883 class PHPProjectBugTrackerExtractBugTrackerAndBugTest(
884- ExtractBugTrackerAndBugTestBase):
885+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
886 """Ensure BugWatchSet.extractBugTrackerAndBug works with PHP bug URLs.
887 """
888
889@@ -301,7 +315,7 @@
890
891
892 class GoogleCodeBugTrackerExtractBugTrackerAndBugTest(
893- ExtractBugTrackerAndBugTestBase):
894+ ExtractBugTrackerAndBugTestBase, unittest.TestCase):
895 """Ensure BugWatchSet.extractBugTrackerAndBug works for Google Code URLs.
896 """
897
898@@ -311,6 +325,39 @@
899 bug_id = '12345'
900
901
902+class TestBugWatchSet(TestCaseWithFactory):
903+ """Tests for the bugwatch updating system."""
904+
905+ layer = LaunchpadZopelessLayer
906+
907+ def test_getBugWatchesForRemoteBug(self):
908+ # getBugWatchesForRemoteBug() returns bug watches from that
909+ # refer to the remote bug.
910+ bug_watches_alice = [
911+ self.factory.makeBugWatch(remote_bug="alice"),
912+ ]
913+ bug_watches_bob = [
914+ self.factory.makeBugWatch(remote_bug="bob"),
915+ self.factory.makeBugWatch(remote_bug="bob"),
916+ ]
917+ bug_watch_set = getUtility(IBugWatchSet)
918+ # Passing in the remote bug ID gets us every bug watch that
919+ # refers to that remote bug.
920+ self.failUnlessEqual(
921+ set(bug_watches_alice),
922+ set(bug_watch_set.getBugWatchesForRemoteBug('alice')))
923+ self.failUnlessEqual(
924+ set(bug_watches_bob),
925+ set(bug_watch_set.getBugWatchesForRemoteBug('bob')))
926+ # The search can be narrowed by passing in a list or other
927+ # iterable collection of bug watch IDs.
928+ bug_watches_limited = bug_watches_alice + bug_watches_bob[:1]
929+ self.failUnlessEqual(
930+ set(bug_watches_bob[:1]),
931+ set(bug_watch_set.getBugWatchesForRemoteBug('bob', [
932+ bug_watch.id for bug_watch in bug_watches_limited])))
933+
934+
935 class TestBugWatchBugTasks(TestCaseWithFactory):
936
937 layer = DatabaseFunctionalLayer
938@@ -326,31 +373,4 @@
939
940
941 def test_suite():
942- suite = unittest.TestSuite()
943- suite.addTest(unittest.makeSuite(BugzillaExtractBugTrackerAndBugTest))
944- suite.addTest(unittest.makeSuite(IssuezillaExtractBugTrackerAndBugTest))
945- suite.addTest(unittest.makeSuite(RoundUpExtractBugTrackerAndBugTest))
946- suite.addTest(unittest.makeSuite(TracExtractBugTrackerAndBugTest))
947- suite.addTest(unittest.makeSuite(DebbugsExtractBugTrackerAndBugTest))
948- suite.addTest(
949- unittest.makeSuite(DebbugsExtractBugTrackerAndBugShorthandTest))
950- suite.addTest(unittest.makeSuite(SFExtractBugTrackerAndBugTest))
951- suite.addTest(unittest.makeSuite(SFTracker2ExtractBugTrackerAndBugTest))
952- suite.addTest(unittest.makeSuite(XForgeExtractBugTrackerAndBugTest))
953- suite.addTest(unittest.makeSuite(MantisExtractBugTrackerAndBugTest))
954- suite.addTest(unittest.makeSuite(RTExtractBugTrackerAndBugTest))
955- suite.addTest(unittest.makeSuite(CpanExtractBugTrackerAndBugTest))
956- suite.addTest(unittest.makeSuite(SavannahExtractBugTrackerAndBugTest))
957- suite.addTest(unittest.makeSuite(SavaneExtractBugTrackerAndBugTest))
958- suite.addTest(unittest.makeSuite(EmailAddressExtractBugTrackerAndBugTest))
959- suite.addTest(unittest.makeSuite(
960- PHPProjectBugTrackerExtractBugTrackerAndBugTest))
961- suite.addTest(unittest.makeSuite(
962- GoogleCodeBugTrackerExtractBugTrackerAndBugTest))
963- suite.addTest(unittest.makeSuite(TestBugWatchBugTasks))
964- return suite
965-
966-
967-if __name__ == '__main__':
968- unittest.main()
969-
970+ return unittest.TestLoader().loadTestsFromName(__name__)
971
972=== removed file 'lib/lp/translations/scripts/tests/__init__.py.moved'