Merge lp:~gmb/launchpad/cw-refactor-add-rbu-bug-568881 into lp:launchpad/db-devel
- cw-refactor-add-rbu-bug-568881
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 9337 |
Proposed branch: | lp:~gmb/launchpad/cw-refactor-add-rbu-bug-568881 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
905 lines (+433/-275) 8 files modified
lib/lp/bugs/doc/externalbugtracker.txt (+14/-10) lib/lp/bugs/scripts/checkwatches/bugwatchupdater.py (+3/-4) lib/lp/bugs/scripts/checkwatches/core.py (+14/-225) lib/lp/bugs/scripts/checkwatches/remotebugupdater.py (+218/-0) lib/lp/bugs/scripts/checkwatches/tests/test_core.py (+30/-19) lib/lp/bugs/scripts/checkwatches/tests/test_remotebugupdater.py (+59/-0) lib/lp/bugs/scripts/checkwatches/utilities.py (+61/-0) lib/lp/bugs/scripts/tests/test_bugimport.py (+34/-17) |
To merge this branch: | bzr merge lp:~gmb/launchpad/cw-refactor-add-rbu-bug-568881 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | code | Approve | |
Canonical Launchpad Engineering | code | Pending | |
Review via email: mp+24404@code.launchpad.net |
Commit message
The remote bug-specific part of CheckwatchesMaster has been moved into a RemoteBugUpdater class.
Description of the change
This branch is the second in the mad-checkwatche
It moves the remote-bug-specific part of CheckwatchesMaster into a
RemoteBugUpdater class.
The majority of changes here are simple moves, though there are some
alterations to tests and callsites to make sure that everything works
properly.
Gavin Panella (allenap) : | # |
Graham Binns (gmb) wrote : | # |
On Thu, Apr 29, 2010 at 12:52:29PM -0000, Gavin Panella wrote:
> Review: Approve
> Hi Graham,
>
> This mad refactorage is looking good :) I have a few comments but
> nothing major, so r=me.
>
> Gavin.
>
>
> > @@ -168,6 +126,17 @@
> > else:
> > self._syncable_
> >
> > + def _makeRemoteBugU
> > + bug_watch_ids, unmodified_
> > + """Create and return a `RemoteBugUpdater` instance.
> > +
> > + This method exists purely for the sake of being able to
> > + overrride it in tests.
>
> Have you been listening to Christina Aguilera recently? s/rrr/rr/
What's really sad is that you're confident enough about your knowledge
of her album names to make that gag. Shame on you.
> Another way to do this with less boiler-plate is to have a
> remote_
> _makeRemoteBugU
Right. After discussion on IRC I've done this.
> > + @commit_before
> > + def updateRemoteBug
> > + can_push_
>
> The can_* arguments don't change for the life of the RemoteBugUpdater,
> so I think it makes sense to set them in __init__(). This could be
> done by just passing server_time in.
Agreed. Done.
> > + # Avoid circular imports
>
> Already avoided.
>
Oops, fixed.
> > + with self.transaction:
> > + bug_watches = self._getBugWat
> > + # If there aren't any bug watches for this remote bug,
> > + # just log a warning and carry on.
> > + if len(bug_watches) == 0:
> > + self.warning(
> > + "Spurious remote bug ID: No watches found for "
> > + "remote bug %s on %s" % (
> > + self.remote_bug, self.external_
> > + return
> > + # Mark them all as checked.
> > + for bug_watch in bug_watches:
> > + bug_watch.
> > + bug_watch.
> > + # Next if this one is definitely unmodified.
>
> s/Next/Return/
>
Fixed.
> > + if self.remote_bug in self.unmodified
> > + return
> > + # Save the remote bug URL for error reporting.
> > + remote_bug_url = bug_watches[0].url
> > + # Save the list of local bug IDs for error reporting.
> > + local_ids = ", ".join(
> > + str(bug_id) for bug_id in sorted(
> > + watch.bug.id for watch in bug_watches))
> > +
> > + try:
> > + new_remote_status = None
> > + new_malone_status = None
> > + new_remote_
> > + new_malone_
> > + error = None
> > + oops_id = None
> > +
> > + # XXX: 2007-10-17 Graham Binns
> > + # This nested set of try:excepts isn't really
> > + ...
Preview Diff
1 | === modified file 'lib/lp/bugs/doc/externalbugtracker.txt' | |||
2 | --- lib/lp/bugs/doc/externalbugtracker.txt 2010-04-23 11:19:49 +0000 | |||
3 | +++ lib/lp/bugs/doc/externalbugtracker.txt 2010-05-05 10:09:40 +0000 | |||
4 | @@ -391,12 +391,15 @@ | |||
5 | 391 | >>> from zope.interface import implements | 391 | >>> from zope.interface import implements |
6 | 392 | >>> from canonical.launchpad.interfaces import ISupportsCommentImport | 392 | >>> from canonical.launchpad.interfaces import ISupportsCommentImport |
7 | 393 | >>> class CommentImportExternalBugTracker(TimeUnknownExternalBugTracker): | 393 | >>> class CommentImportExternalBugTracker(TimeUnknownExternalBugTracker): |
8 | 394 | ... baseurl = 'http://whatever.com' | ||
9 | 394 | ... implements(ISupportsCommentImport) | 395 | ... implements(ISupportsCommentImport) |
10 | 395 | ... sync_comments = True | 396 | ... sync_comments = True |
15 | 396 | >>> bug_watch_updater.updateBugWatches( | 397 | |
16 | 397 | ... CommentImportExternalBugTracker(), [], now=utc_now) | 398 | >>> checkwatches_master = CheckwatchesMaster( |
17 | 398 | getCurrentDBTime() called | 399 | ... transaction, syncable_gnome_products=[]) |
18 | 399 | initializeRemoteBugDB() called: [] | 400 | >>> remote_bug_updater = checkwatches_master.remote_bug_updater_factory( |
19 | 401 | ... checkwatches_master, CommentImportExternalBugTracker(), '1', | ||
20 | 402 | ... [], [], server_time=None) | ||
21 | 400 | WARNING:...:Comment importing supported, but server time can't be | 403 | WARNING:...:Comment importing supported, but server time can't be |
22 | 401 | trusted. No comments will be imported. (OOPS-...) | 404 | trusted. No comments will be imported. (OOPS-...) |
23 | 402 | 405 | ||
24 | @@ -641,7 +644,7 @@ | |||
25 | 641 | 644 | ||
26 | 642 | === Converting statuses === | 645 | === Converting statuses === |
27 | 643 | 646 | ||
29 | 644 | Once it has retrieved the bugs from the remote server, CheckwatchesMaster | 647 | Once it has retrieved the bugs from the remote server, RemoteBugUpdater |
30 | 645 | attempts to convert their statuses into Launchpad BugTaskStatuses by | 648 | attempts to convert their statuses into Launchpad BugTaskStatuses by |
31 | 646 | calling the convertRemoteStatus() method on the ExternalBugTracker via | 649 | calling the convertRemoteStatus() method on the ExternalBugTracker via |
32 | 647 | its own _convertRemoteStatus() method. | 650 | its own _convertRemoteStatus() method. |
33 | @@ -660,17 +663,18 @@ | |||
34 | 660 | ... else: | 663 | ... else: |
35 | 661 | ... raise UnknownRemoteStatusError(remote_status) | 664 | ... raise UnknownRemoteStatusError(remote_status) |
36 | 662 | 665 | ||
38 | 663 | CheckwatchesMaster._convertRemoteStatus() will handle these errors and will | 666 | RemoteBugUpdater._convertRemoteStatus() will handle these errors and will |
39 | 664 | return BugTaskStatus.UNKNOWN when they occur. It will also log a | 667 | return BugTaskStatus.UNKNOWN when they occur. It will also log a |
40 | 665 | warning. | 668 | warning. |
41 | 666 | 669 | ||
44 | 667 | >>> status = bug_watch_updater._convertRemoteStatus( | 670 | >>> remote_bug_updater = bug_watch_updater.remote_bug_updater_factory( |
45 | 668 | ... StatusConvertingExternalBugTracker(), 'new') | 671 | ... bug_watch_updater, StatusConvertingExternalBugTracker(), |
46 | 672 | ... '1', [1], [], utc_now) | ||
47 | 673 | >>> status = remote_bug_updater._convertRemoteStatus('new') | ||
48 | 669 | >>> print status.title | 674 | >>> print status.title |
49 | 670 | New | 675 | New |
50 | 671 | 676 | ||
53 | 672 | >>> status = bug_watch_updater._convertRemoteStatus( | 677 | >>> status = remote_bug_updater._convertRemoteStatus('spam') |
52 | 673 | ... StatusConvertingExternalBugTracker(), 'spam') | ||
54 | 674 | WARNING...Unknown remote status 'spam'. (OOPS-...) | 678 | WARNING...Unknown remote status 'spam'. (OOPS-...) |
55 | 675 | >>> print status.title | 679 | >>> print status.title |
56 | 676 | Unknown | 680 | Unknown |
57 | 677 | 681 | ||
58 | === modified file 'lib/lp/bugs/scripts/checkwatches/bugwatchupdater.py' | |||
59 | --- lib/lp/bugs/scripts/checkwatches/bugwatchupdater.py 2010-04-26 12:34:47 +0000 | |||
60 | +++ lib/lp/bugs/scripts/checkwatches/bugwatchupdater.py 2010-05-05 10:09:40 +0000 | |||
61 | @@ -26,9 +26,12 @@ | |||
62 | 26 | from lp.bugs.interfaces.bug import IBugSet | 26 | from lp.bugs.interfaces.bug import IBugSet |
63 | 27 | from lp.bugs.scripts.checkwatches.base import ( | 27 | from lp.bugs.scripts.checkwatches.base import ( |
64 | 28 | WorkingBase, commit_before) | 28 | WorkingBase, commit_before) |
65 | 29 | from lp.bugs.scripts.checkwatches.utilities import ( | ||
66 | 30 | get_remote_system_oops_properties) | ||
67 | 29 | from lp.registry.interfaces.person import PersonCreationRationale | 31 | from lp.registry.interfaces.person import PersonCreationRationale |
68 | 30 | 32 | ||
69 | 31 | 33 | ||
70 | 34 | |||
71 | 32 | class BugWatchUpdater(WorkingBase): | 35 | class BugWatchUpdater(WorkingBase): |
72 | 33 | """Handles the updating of a single BugWatch for checkwatches.""" | 36 | """Handles the updating of a single BugWatch for checkwatches.""" |
73 | 34 | 37 | ||
74 | @@ -81,10 +84,6 @@ | |||
75 | 81 | @commit_before | 84 | @commit_before |
76 | 82 | def importBugComments(self): | 85 | def importBugComments(self): |
77 | 83 | """Import all the comments from the remote bug.""" | 86 | """Import all the comments from the remote bug.""" |
78 | 84 | # Avoid circularity. | ||
79 | 85 | from lp.bugs.scripts.checkwatches.core import ( | ||
80 | 86 | get_remote_system_oops_properties) | ||
81 | 87 | |||
82 | 88 | with self.transaction: | 87 | with self.transaction: |
83 | 89 | local_bug_id = self.bug_watch.bug.id | 88 | local_bug_id = self.bug_watch.bug.id |
84 | 90 | remote_bug_id = self.bug_watch.remotebug | 89 | remote_bug_id = self.bug_watch.remotebug |
85 | 91 | 90 | ||
86 | === modified file 'lib/lp/bugs/scripts/checkwatches/core.py' | |||
87 | --- lib/lp/bugs/scripts/checkwatches/core.py 2010-04-30 03:10:17 +0000 | |||
88 | +++ lib/lp/bugs/scripts/checkwatches/core.py 2010-05-05 10:09:40 +0000 | |||
89 | @@ -37,22 +37,21 @@ | |||
90 | 37 | from canonical.database.constants import UTC_NOW | 37 | from canonical.database.constants import UTC_NOW |
91 | 38 | from canonical.database.sqlbase import flush_database_updates | 38 | from canonical.database.sqlbase import flush_database_updates |
92 | 39 | from canonical.launchpad.interfaces import ( | 39 | from canonical.launchpad.interfaces import ( |
97 | 40 | BugTaskStatus, BugWatchActivityStatus, CreateBugParams, | 40 | CreateBugParams, IBugTrackerSet, IBugWatchSet, IDistribution, |
98 | 41 | IBugTrackerSet, IBugWatchSet, IDistribution, ILaunchpadCelebrities, | 41 | ILaunchpadCelebrities, IPersonSet, ISupportsCommentImport, |
99 | 42 | IPersonSet, ISupportsCommentImport, ISupportsCommentPushing, | 42 | ISupportsCommentPushing, PersonCreationRationale) |
96 | 43 | PersonCreationRationale, UNKNOWN_REMOTE_STATUS) | ||
100 | 44 | from canonical.launchpad.scripts.logger import log as default_log | 43 | from canonical.launchpad.scripts.logger import log as default_log |
101 | 45 | 44 | ||
102 | 46 | from lp.bugs import externalbugtracker | 45 | from lp.bugs import externalbugtracker |
103 | 47 | from lp.bugs.externalbugtracker import ( | 46 | from lp.bugs.externalbugtracker import ( |
108 | 48 | BATCH_SIZE_UNLIMITED, BugNotFound, BugTrackerConnectError, | 47 | BATCH_SIZE_UNLIMITED, BugWatchUpdateError, |
109 | 49 | BugWatchUpdateError, InvalidBugId, PrivateRemoteBug, | 48 | UnknownBugTrackerTypeError) |
106 | 50 | UnknownBugTrackerTypeError, UnknownRemoteStatusError, UnparseableBugData, | ||
107 | 51 | UnparseableBugTrackerVersion, UnsupportedBugTrackerVersion) | ||
110 | 52 | from lp.bugs.interfaces.externalbugtracker import ISupportsBackLinking | 49 | from lp.bugs.interfaces.externalbugtracker import ISupportsBackLinking |
111 | 53 | from lp.bugs.scripts.checkwatches.base import ( | 50 | from lp.bugs.scripts.checkwatches.base import ( |
112 | 54 | WorkingBase, commit_before, with_interaction) | 51 | WorkingBase, commit_before, with_interaction) |
114 | 55 | from lp.bugs.scripts.checkwatches.bugwatchupdater import BugWatchUpdater | 52 | from lp.bugs.scripts.checkwatches.remotebugupdater import RemoteBugUpdater |
115 | 53 | from lp.bugs.scripts.checkwatches.utilities import ( | ||
116 | 54 | get_bugwatcherrortype_for_error) | ||
117 | 56 | from lp.services.scripts.base import LaunchpadCronScript | 55 | from lp.services.scripts.base import LaunchpadCronScript |
118 | 57 | 56 | ||
119 | 58 | 57 | ||
120 | @@ -77,30 +76,6 @@ | |||
121 | 77 | """Time difference between ourselves and the remote server is too much.""" | 76 | """Time difference between ourselves and the remote server is too much.""" |
122 | 78 | 77 | ||
123 | 79 | 78 | ||
124 | 80 | _exception_to_bugwatcherrortype = [ | ||
125 | 81 | (BugTrackerConnectError, BugWatchActivityStatus.CONNECTION_ERROR), | ||
126 | 82 | (PrivateRemoteBug, BugWatchActivityStatus.PRIVATE_REMOTE_BUG), | ||
127 | 83 | (UnparseableBugData, BugWatchActivityStatus.UNPARSABLE_BUG), | ||
128 | 84 | (UnparseableBugTrackerVersion, | ||
129 | 85 | BugWatchActivityStatus.UNPARSABLE_BUG_TRACKER), | ||
130 | 86 | (UnsupportedBugTrackerVersion, | ||
131 | 87 | BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER), | ||
132 | 88 | (UnknownBugTrackerTypeError, | ||
133 | 89 | BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER), | ||
134 | 90 | (InvalidBugId, BugWatchActivityStatus.INVALID_BUG_ID), | ||
135 | 91 | (BugNotFound, BugWatchActivityStatus.BUG_NOT_FOUND), | ||
136 | 92 | (PrivateRemoteBug, BugWatchActivityStatus.PRIVATE_REMOTE_BUG), | ||
137 | 93 | (socket.timeout, BugWatchActivityStatus.TIMEOUT)] | ||
138 | 94 | |||
139 | 95 | def get_bugwatcherrortype_for_error(error): | ||
140 | 96 | """Return the correct `BugWatchActivityStatus` for a given error.""" | ||
141 | 97 | for exc_type, bugwatcherrortype in _exception_to_bugwatcherrortype: | ||
142 | 98 | if isinstance(error, exc_type): | ||
143 | 99 | return bugwatcherrortype | ||
144 | 100 | else: | ||
145 | 101 | return BugWatchActivityStatus.UNKNOWN | ||
146 | 102 | |||
147 | 103 | |||
148 | 104 | def unique(iterator): | 79 | def unique(iterator): |
149 | 105 | """Generate only unique items from an iterator.""" | 80 | """Generate only unique items from an iterator.""" |
150 | 106 | seen = set() | 81 | seen = set() |
151 | @@ -126,26 +101,11 @@ | |||
152 | 126 | int(SUGGESTED_BATCH_SIZE_PROPORTION * num_watches)) | 101 | int(SUGGESTED_BATCH_SIZE_PROPORTION * num_watches)) |
153 | 127 | 102 | ||
154 | 128 | 103 | ||
155 | 129 | def get_remote_system_oops_properties(remote_system): | ||
156 | 130 | """Return (name, value) tuples describing a remote system. | ||
157 | 131 | |||
158 | 132 | Each item in the list is intended for use as an OOPS property. | ||
159 | 133 | |||
160 | 134 | :remote_system: The `ExternalBugTracker` instance from which the | ||
161 | 135 | OOPS properties should be extracted. | ||
162 | 136 | """ | ||
163 | 137 | return [ | ||
164 | 138 | ('batch_size', remote_system.batch_size), | ||
165 | 139 | ('batch_query_threshold', remote_system.batch_query_threshold), | ||
166 | 140 | ('sync_comments', remote_system.sync_comments), | ||
167 | 141 | ('externalbugtracker', remote_system.__class__.__name__), | ||
168 | 142 | ('baseurl', remote_system.baseurl) | ||
169 | 143 | ] | ||
170 | 144 | |||
171 | 145 | |||
172 | 146 | class CheckwatchesMaster(WorkingBase): | 104 | class CheckwatchesMaster(WorkingBase): |
173 | 147 | """Takes responsibility for updating remote bug watches.""" | 105 | """Takes responsibility for updating remote bug watches.""" |
174 | 148 | 106 | ||
175 | 107 | remote_bug_updater_factory = RemoteBugUpdater | ||
176 | 108 | |||
177 | 149 | def __init__(self, transaction_manager, logger=default_log, | 109 | def __init__(self, transaction_manager, logger=default_log, |
178 | 150 | syncable_gnome_products=None): | 110 | syncable_gnome_products=None): |
179 | 151 | """Initialize a CheckwatchesMaster. | 111 | """Initialize a CheckwatchesMaster. |
180 | @@ -455,37 +415,6 @@ | |||
181 | 455 | self.logger.debug( | 415 | self.logger.debug( |
182 | 456 | "No watches to update on %s" % bug_tracker.baseurl) | 416 | "No watches to update on %s" % bug_tracker.baseurl) |
183 | 457 | 417 | ||
184 | 458 | def _convertRemoteStatus(self, remotesystem, remote_status): | ||
185 | 459 | """Convert a remote bug status to a Launchpad status and return it. | ||
186 | 460 | |||
187 | 461 | :param remotesystem: The `IExternalBugTracker` instance | ||
188 | 462 | representing the remote system. | ||
189 | 463 | :param remote_status: The remote status to be converted into a | ||
190 | 464 | Launchpad status. | ||
191 | 465 | |||
192 | 466 | If the remote status cannot be mapped to a Launchpad status, | ||
193 | 467 | BugTaskStatus.UNKNOWN will be returned and a warning will be | ||
194 | 468 | logged. | ||
195 | 469 | """ | ||
196 | 470 | # We don't bother trying to convert UNKNOWN_REMOTE_STATUS. | ||
197 | 471 | if remote_status == UNKNOWN_REMOTE_STATUS: | ||
198 | 472 | return BugTaskStatus.UNKNOWN | ||
199 | 473 | |||
200 | 474 | try: | ||
201 | 475 | launchpad_status = remotesystem.convertRemoteStatus( | ||
202 | 476 | remote_status) | ||
203 | 477 | except UnknownRemoteStatusError: | ||
204 | 478 | # We log the warning, since we need to know about statuses | ||
205 | 479 | # that we don't handle correctly. | ||
206 | 480 | self.warning( | ||
207 | 481 | "Unknown remote status '%s'." % remote_status, | ||
208 | 482 | get_remote_system_oops_properties(remotesystem), | ||
209 | 483 | sys.exc_info()) | ||
210 | 484 | |||
211 | 485 | launchpad_status = BugTaskStatus.UNKNOWN | ||
212 | 486 | |||
213 | 487 | return launchpad_status | ||
214 | 488 | |||
215 | 489 | def _getRemoteIdsToCheck(self, remotesystem, bug_watches, | 418 | def _getRemoteIdsToCheck(self, remotesystem, bug_watches, |
216 | 490 | server_time=None, now=None, batch_size=None): | 419 | server_time=None, now=None, batch_size=None): |
217 | 491 | """Return the remote bug IDs to check for a set of bug watches. | 420 | """Return the remote bug IDs to check for a set of bug watches. |
218 | @@ -607,17 +536,6 @@ | |||
219 | 607 | 'unmodified_remote_ids': sorted(unmodified_remote_ids), | 536 | 'unmodified_remote_ids': sorted(unmodified_remote_ids), |
220 | 608 | } | 537 | } |
221 | 609 | 538 | ||
222 | 610 | def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids): | ||
223 | 611 | """Return a list of bug watches for the given remote bug. | ||
224 | 612 | |||
225 | 613 | The returned watches will all be members of `bug_watch_ids`. | ||
226 | 614 | |||
227 | 615 | This method exists primarily to be overridden during testing. | ||
228 | 616 | """ | ||
229 | 617 | return list( | ||
230 | 618 | getUtility(IBugWatchSet).getBugWatchesForRemoteBug( | ||
231 | 619 | remote_bug_id, bug_watch_ids)) | ||
232 | 620 | |||
233 | 621 | # XXX gmb 2008-11-07 [bug=295319] | 539 | # XXX gmb 2008-11-07 [bug=295319] |
234 | 622 | # This method is 186 lines long. It needs to be shorter. | 540 | # This method is 186 lines long. It needs to be shorter. |
235 | 623 | @commit_before | 541 | @commit_before |
236 | @@ -678,140 +596,11 @@ | |||
237 | 678 | bug_watch_ids, get_bugwatcherrortype_for_error(error)) | 596 | bug_watch_ids, get_bugwatcherrortype_for_error(error)) |
238 | 679 | raise | 597 | raise |
239 | 680 | 598 | ||
240 | 681 | # Whether we can import and / or push comments is determined | ||
241 | 682 | # on a per-bugtracker-type level. | ||
242 | 683 | can_import_comments = ( | ||
243 | 684 | ISupportsCommentImport.providedBy(remotesystem) and | ||
244 | 685 | remotesystem.sync_comments) | ||
245 | 686 | can_push_comments = ( | ||
246 | 687 | ISupportsCommentPushing.providedBy(remotesystem) and | ||
247 | 688 | remotesystem.sync_comments) | ||
248 | 689 | can_back_link = ( | ||
249 | 690 | ISupportsBackLinking.providedBy(remotesystem) and | ||
250 | 691 | remotesystem.sync_comments) | ||
251 | 692 | |||
252 | 693 | if can_import_comments and server_time is None: | ||
253 | 694 | can_import_comments = False | ||
254 | 695 | self.warning( | ||
255 | 696 | "Comment importing supported, but server time can't be" | ||
256 | 697 | " trusted. No comments will be imported.") | ||
257 | 698 | |||
258 | 699 | error_type_messages = { | ||
259 | 700 | BugWatchActivityStatus.INVALID_BUG_ID: | ||
260 | 701 | ("Invalid bug %(bug_id)r on %(base_url)s " | ||
261 | 702 | "(local bugs: %(local_ids)s)."), | ||
262 | 703 | BugWatchActivityStatus.BUG_NOT_FOUND: | ||
263 | 704 | ("Didn't find bug %(bug_id)r on %(base_url)s " | ||
264 | 705 | "(local bugs: %(local_ids)s)."), | ||
265 | 706 | BugWatchActivityStatus.PRIVATE_REMOTE_BUG: | ||
266 | 707 | ("Remote bug %(bug_id)r on %(base_url)s is private " | ||
267 | 708 | "(local bugs: %(local_ids)s)."), | ||
268 | 709 | } | ||
269 | 710 | error_type_message_default = ( | ||
270 | 711 | "remote bug: %(bug_id)r; " | ||
271 | 712 | "base url: %(base_url)s; " | ||
272 | 713 | "local bugs: %(local_ids)s" | ||
273 | 714 | ) | ||
274 | 715 | |||
275 | 716 | for remote_bug_id in all_remote_ids: | 599 | for remote_bug_id in all_remote_ids: |
374 | 717 | with self.transaction: | 600 | remote_bug_updater = self.remote_bug_updater_factory( |
375 | 718 | bug_watches = self._getBugWatchesForRemoteBug( | 601 | self, remotesystem, remote_bug_id, bug_watch_ids, |
376 | 719 | remote_bug_id, bug_watch_ids) | 602 | unmodified_remote_ids, server_time) |
377 | 720 | # If there aren't any bug watches for this remote bug, | 603 | remote_bug_updater.updateRemoteBug() |
280 | 721 | # just log a warning and carry on. | ||
281 | 722 | if len(bug_watches) == 0: | ||
282 | 723 | self.warning( | ||
283 | 724 | "Spurious remote bug ID: No watches found for " | ||
284 | 725 | "remote bug %s on %s" % ( | ||
285 | 726 | remote_bug_id, remotesystem.baseurl)) | ||
286 | 727 | continue | ||
287 | 728 | # Mark them all as checked. | ||
288 | 729 | for bug_watch in bug_watches: | ||
289 | 730 | bug_watch.lastchecked = UTC_NOW | ||
290 | 731 | bug_watch.next_check = None | ||
291 | 732 | # Next if this one is definitely unmodified. | ||
292 | 733 | if remote_bug_id in unmodified_remote_ids: | ||
293 | 734 | continue | ||
294 | 735 | # Save the remote bug URL for error reporting. | ||
295 | 736 | remote_bug_url = bug_watches[0].url | ||
296 | 737 | # Save the list of local bug IDs for error reporting. | ||
297 | 738 | local_ids = ", ".join( | ||
298 | 739 | str(bug_id) for bug_id in sorted( | ||
299 | 740 | watch.bug.id for watch in bug_watches)) | ||
300 | 741 | |||
301 | 742 | try: | ||
302 | 743 | new_remote_status = None | ||
303 | 744 | new_malone_status = None | ||
304 | 745 | new_remote_importance = None | ||
305 | 746 | new_malone_importance = None | ||
306 | 747 | error = None | ||
307 | 748 | oops_id = None | ||
308 | 749 | |||
309 | 750 | # XXX: 2007-10-17 Graham Binns | ||
310 | 751 | # This nested set of try:excepts isn't really | ||
311 | 752 | # necessary and can be refactored out when bug | ||
312 | 753 | # 136391 is dealt with. | ||
313 | 754 | try: | ||
314 | 755 | new_remote_status = ( | ||
315 | 756 | remotesystem.getRemoteStatus(remote_bug_id)) | ||
316 | 757 | new_malone_status = self._convertRemoteStatus( | ||
317 | 758 | remotesystem, new_remote_status) | ||
318 | 759 | new_remote_importance = ( | ||
319 | 760 | remotesystem.getRemoteImportance(remote_bug_id)) | ||
320 | 761 | new_malone_importance = ( | ||
321 | 762 | remotesystem.convertRemoteImportance( | ||
322 | 763 | new_remote_importance)) | ||
323 | 764 | except (InvalidBugId, BugNotFound, PrivateRemoteBug), ex: | ||
324 | 765 | error = get_bugwatcherrortype_for_error(ex) | ||
325 | 766 | message = error_type_messages.get( | ||
326 | 767 | error, error_type_message_default) | ||
327 | 768 | oops_id = self.warning( | ||
328 | 769 | message % { | ||
329 | 770 | 'bug_id': remote_bug_id, | ||
330 | 771 | 'base_url': remotesystem.baseurl, | ||
331 | 772 | 'local_ids': local_ids, | ||
332 | 773 | }, | ||
333 | 774 | properties=[ | ||
334 | 775 | ('URL', remote_bug_url), | ||
335 | 776 | ('bug_id', remote_bug_id), | ||
336 | 777 | ('local_ids', local_ids), | ||
337 | 778 | ] + get_remote_system_oops_properties( | ||
338 | 779 | remotesystem), | ||
339 | 780 | info=sys.exc_info()) | ||
340 | 781 | |||
341 | 782 | for bug_watch in bug_watches: | ||
342 | 783 | bug_watch_updater = BugWatchUpdater( | ||
343 | 784 | self, bug_watch, remotesystem) | ||
344 | 785 | |||
345 | 786 | bug_watch_updater.updateBugWatch( | ||
346 | 787 | new_remote_status, new_malone_status, | ||
347 | 788 | new_remote_importance, new_malone_importance, | ||
348 | 789 | can_import_comments, can_push_comments, | ||
349 | 790 | can_back_link, error, oops_id) | ||
350 | 791 | |||
351 | 792 | except (KeyboardInterrupt, SystemExit): | ||
352 | 793 | # We should never catch KeyboardInterrupt or SystemExit. | ||
353 | 794 | raise | ||
354 | 795 | |||
355 | 796 | except Exception, error: | ||
356 | 797 | # Send the error to the log. | ||
357 | 798 | oops_id = self.error( | ||
358 | 799 | "Failure updating bug %r on %s (local bugs: %s)." % | ||
359 | 800 | (remote_bug_id, bug_tracker_url, local_ids), | ||
360 | 801 | properties=[ | ||
361 | 802 | ('URL', remote_bug_url), | ||
362 | 803 | ('bug_id', remote_bug_id), | ||
363 | 804 | ('local_ids', local_ids)] + | ||
364 | 805 | get_remote_system_oops_properties(remotesystem)) | ||
365 | 806 | # We record errors against the bug watches and update | ||
366 | 807 | # their lastchecked dates so that we don't try to | ||
367 | 808 | # re-check them every time checkwatches runs. | ||
368 | 809 | error_type = get_bugwatcherrortype_for_error(error) | ||
369 | 810 | with self.transaction: | ||
370 | 811 | getUtility(IBugWatchSet).bulkSetError( | ||
371 | 812 | bug_watches, error_type) | ||
372 | 813 | getUtility(IBugWatchSet).bulkAddActivity( | ||
373 | 814 | bug_watches, result=error_type, oops_id=oops_id) | ||
378 | 815 | 604 | ||
379 | 816 | def importBug(self, external_bugtracker, bugtracker, bug_target, | 605 | def importBug(self, external_bugtracker, bugtracker, bug_target, |
380 | 817 | remote_bug): | 606 | remote_bug): |
381 | 818 | 607 | ||
382 | === added file 'lib/lp/bugs/scripts/checkwatches/remotebugupdater.py' | |||
383 | --- lib/lp/bugs/scripts/checkwatches/remotebugupdater.py 1970-01-01 00:00:00 +0000 | |||
384 | +++ lib/lp/bugs/scripts/checkwatches/remotebugupdater.py 2010-05-05 10:09:40 +0000 | |||
385 | @@ -0,0 +1,218 @@ | |||
386 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
387 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
388 | 3 | |||
389 | 4 | """Classes and logic for the remote bug updater.""" | ||
390 | 5 | |||
391 | 6 | from __future__ import with_statement | ||
392 | 7 | |||
393 | 8 | __metaclass__ = type | ||
394 | 9 | __all__ = [ | ||
395 | 10 | 'RemoteBugUpdater', | ||
396 | 11 | ] | ||
397 | 12 | |||
398 | 13 | import sys | ||
399 | 14 | |||
400 | 15 | from zope.component import getUtility | ||
401 | 16 | |||
402 | 17 | from canonical.database.constants import UTC_NOW | ||
403 | 18 | |||
404 | 19 | from lp.bugs.externalbugtracker import ( | ||
405 | 20 | BugNotFound, InvalidBugId, PrivateRemoteBug, UnknownRemoteStatusError) | ||
406 | 21 | |||
407 | 22 | from lp.bugs.interfaces.bugtask import BugTaskStatus | ||
408 | 23 | from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus, IBugWatchSet | ||
409 | 24 | from lp.bugs.interfaces.externalbugtracker import ( | ||
410 | 25 | ISupportsBackLinking, ISupportsCommentImport, | ||
411 | 26 | ISupportsCommentPushing, UNKNOWN_REMOTE_STATUS) | ||
412 | 27 | from lp.bugs.scripts.checkwatches.base import WorkingBase, commit_before | ||
413 | 28 | from lp.bugs.scripts.checkwatches.bugwatchupdater import BugWatchUpdater | ||
414 | 29 | from lp.bugs.scripts.checkwatches.utilities import ( | ||
415 | 30 | get_bugwatcherrortype_for_error, get_remote_system_oops_properties) | ||
416 | 31 | |||
417 | 32 | |||
418 | 33 | class RemoteBugUpdater(WorkingBase): | ||
419 | 34 | |||
420 | 35 | def __init__(self, parent, external_bugtracker, remote_bug, | ||
421 | 36 | bug_watch_ids, unmodified_remote_ids, server_time): | ||
422 | 37 | self.initFromParent(parent) | ||
423 | 38 | self.external_bugtracker = external_bugtracker | ||
424 | 39 | self.bug_tracker_url = external_bugtracker.baseurl | ||
425 | 40 | self.remote_bug = remote_bug | ||
426 | 41 | self.bug_watch_ids = bug_watch_ids | ||
427 | 42 | self.unmodified_remote_ids = unmodified_remote_ids | ||
428 | 43 | |||
429 | 44 | self.error_type_messages = { | ||
430 | 45 | BugWatchActivityStatus.INVALID_BUG_ID: | ||
431 | 46 | ("Invalid bug %(bug_id)r on %(base_url)s " | ||
432 | 47 | "(local bugs: %(local_ids)s)."), | ||
433 | 48 | BugWatchActivityStatus.BUG_NOT_FOUND: | ||
434 | 49 | ("Didn't find bug %(bug_id)r on %(base_url)s " | ||
435 | 50 | "(local bugs: %(local_ids)s)."), | ||
436 | 51 | BugWatchActivityStatus.PRIVATE_REMOTE_BUG: | ||
437 | 52 | ("Remote bug %(bug_id)r on %(base_url)s is private " | ||
438 | 53 | "(local bugs: %(local_ids)s)."), | ||
439 | 54 | } | ||
440 | 55 | self.error_type_message_default = ( | ||
441 | 56 | "remote bug: %(bug_id)r; " | ||
442 | 57 | "base url: %(base_url)s; " | ||
443 | 58 | "local bugs: %(local_ids)s" | ||
444 | 59 | ) | ||
445 | 60 | |||
446 | 61 | # Whether we can import and / or push comments is determined | ||
447 | 62 | # on a per-bugtracker-type level. | ||
448 | 63 | self.can_import_comments = ( | ||
449 | 64 | ISupportsCommentImport.providedBy(external_bugtracker) and | ||
450 | 65 | external_bugtracker.sync_comments) | ||
451 | 66 | self.can_push_comments = ( | ||
452 | 67 | ISupportsCommentPushing.providedBy(external_bugtracker) and | ||
453 | 68 | external_bugtracker.sync_comments) | ||
454 | 69 | self.can_back_link = ( | ||
455 | 70 | ISupportsBackLinking.providedBy(external_bugtracker) and | ||
456 | 71 | external_bugtracker.sync_comments) | ||
457 | 72 | |||
458 | 73 | if self.can_import_comments and server_time is None: | ||
459 | 74 | self.can_import_comments = False | ||
460 | 75 | self.warning( | ||
461 | 76 | "Comment importing supported, but server time can't be " | ||
462 | 77 | "trusted. No comments will be imported.") | ||
463 | 78 | |||
464 | 79 | def _getBugWatchesForRemoteBug(self): | ||
465 | 80 | """Return a list of bug watches for the current remote bug. | ||
466 | 81 | |||
467 | 82 | The returned watches will all be members of `self.bug_watch_ids`. | ||
468 | 83 | |||
469 | 84 | This method exists primarily to be overridden during testing. | ||
470 | 85 | """ | ||
471 | 86 | return list( | ||
472 | 87 | getUtility(IBugWatchSet).getBugWatchesForRemoteBug( | ||
473 | 88 | self.remote_bug, self.bug_watch_ids)) | ||
474 | 89 | |||
475 | 90 | @commit_before | ||
476 | 91 | def updateRemoteBug(self): | ||
477 | 92 | with self.transaction: | ||
478 | 93 | bug_watches = self._getBugWatchesForRemoteBug() | ||
479 | 94 | # If there aren't any bug watches for this remote bug, | ||
480 | 95 | # just log a warning and carry on. | ||
481 | 96 | if len(bug_watches) == 0: | ||
482 | 97 | self.warning( | ||
483 | 98 | "Spurious remote bug ID: No watches found for " | ||
484 | 99 | "remote bug %s on %s" % ( | ||
485 | 100 | self.remote_bug, self.external_bugtracker.baseurl)) | ||
486 | 101 | return | ||
487 | 102 | # Mark them all as checked. | ||
488 | 103 | for bug_watch in bug_watches: | ||
489 | 104 | bug_watch.lastchecked = UTC_NOW | ||
490 | 105 | bug_watch.next_check = None | ||
491 | 106 | # Return if this one is definitely unmodified. | ||
492 | 107 | if self.remote_bug in self.unmodified_remote_ids: | ||
493 | 108 | return | ||
494 | 109 | # Save the remote bug URL for error reporting. | ||
495 | 110 | remote_bug_url = bug_watches[0].url | ||
496 | 111 | # Save the list of local bug IDs for error reporting. | ||
497 | 112 | local_ids = ", ".join( | ||
498 | 113 | str(bug_id) for bug_id in sorted( | ||
499 | 114 | watch.bug.id for watch in bug_watches)) | ||
500 | 115 | |||
501 | 116 | try: | ||
502 | 117 | new_remote_status = None | ||
503 | 118 | new_malone_status = None | ||
504 | 119 | new_remote_importance = None | ||
505 | 120 | new_malone_importance = None | ||
506 | 121 | error = None | ||
507 | 122 | oops_id = None | ||
508 | 123 | |||
509 | 124 | # XXX: 2007-10-17 Graham Binns | ||
510 | 125 | # This nested set of try:excepts isn't really | ||
511 | 126 | # necessary and can be refactored out when bug | ||
512 | 127 | # 136391 is dealt with. | ||
513 | 128 | try: | ||
514 | 129 | new_remote_status = ( | ||
515 | 130 | self.external_bugtracker.getRemoteStatus( | ||
516 | 131 | self.remote_bug)) | ||
517 | 132 | new_malone_status = self._convertRemoteStatus( | ||
518 | 133 | new_remote_status) | ||
519 | 134 | new_remote_importance = ( | ||
520 | 135 | self.external_bugtracker.getRemoteImportance( | ||
521 | 136 | self.remote_bug)) | ||
522 | 137 | new_malone_importance = ( | ||
523 | 138 | self.external_bugtracker.convertRemoteImportance( | ||
524 | 139 | new_remote_importance)) | ||
525 | 140 | except (InvalidBugId, BugNotFound, PrivateRemoteBug), ex: | ||
526 | 141 | error = get_bugwatcherrortype_for_error(ex) | ||
527 | 142 | message = self.error_type_messages.get( | ||
528 | 143 | error, self.error_type_message_default) | ||
529 | 144 | oops_id = self.warning( | ||
530 | 145 | message % { | ||
531 | 146 | 'bug_id': self.remote_bug, | ||
532 | 147 | 'base_url': self.external_bugtracker.baseurl, | ||
533 | 148 | 'local_ids': local_ids, | ||
534 | 149 | }, | ||
535 | 150 | properties=[ | ||
536 | 151 | ('URL', remote_bug_url), | ||
537 | 152 | ('bug_id', self.remote_bug), | ||
538 | 153 | ('local_ids', local_ids), | ||
539 | 154 | ] + get_remote_system_oops_properties( | ||
540 | 155 | self.external_bugtracker), | ||
541 | 156 | info=sys.exc_info()) | ||
542 | 157 | |||
543 | 158 | for bug_watch in bug_watches: | ||
544 | 159 | bug_watch_updater = BugWatchUpdater( | ||
545 | 160 | self, bug_watch, self.external_bugtracker) | ||
546 | 161 | |||
547 | 162 | bug_watch_updater.updateBugWatch( | ||
548 | 163 | new_remote_status, new_malone_status, | ||
549 | 164 | new_remote_importance, new_malone_importance, | ||
550 | 165 | self.can_import_comments, self.can_push_comments, | ||
551 | 166 | self.can_back_link, error, oops_id) | ||
552 | 167 | |||
553 | 168 | except Exception, error: | ||
554 | 169 | # Send the error to the log. | ||
555 | 170 | oops_id = self.error( | ||
556 | 171 | "Failure updating bug %r on %s (local bugs: %s)." % | ||
557 | 172 | (self.remote_bug, self.bug_tracker_url, local_ids), | ||
558 | 173 | properties=[ | ||
559 | 174 | ('URL', remote_bug_url), | ||
560 | 175 | ('bug_id', self.remote_bug), | ||
561 | 176 | ('local_ids', local_ids)] + | ||
562 | 177 | get_remote_system_oops_properties( | ||
563 | 178 | self.external_bugtracker)) | ||
564 | 179 | # We record errors against the bug watches and update | ||
565 | 180 | # their lastchecked dates so that we don't try to | ||
566 | 181 | # re-check them every time checkwatches runs. | ||
567 | 182 | error_type = get_bugwatcherrortype_for_error(error) | ||
568 | 183 | with self.transaction: | ||
569 | 184 | getUtility(IBugWatchSet).bulkSetError( | ||
570 | 185 | bug_watches, error_type) | ||
571 | 186 | getUtility(IBugWatchSet).bulkAddActivity( | ||
572 | 187 | bug_watches, result=error_type, oops_id=oops_id) | ||
573 | 188 | |||
574 | 189 | def _convertRemoteStatus(self, remote_status): | ||
575 | 190 | """Convert a remote bug status to a Launchpad status and return it. | ||
576 | 191 | |||
577 | 192 | :param remote_status: The remote status to be converted into a | ||
578 | 193 | Launchpad status. | ||
579 | 194 | |||
580 | 195 | If the remote status cannot be mapped to a Launchpad status, | ||
581 | 196 | BugTaskStatus.UNKNOWN will be returned and a warning will be | ||
582 | 197 | logged. | ||
583 | 198 | """ | ||
584 | 199 | # We don't bother trying to convert UNKNOWN_REMOTE_STATUS. | ||
585 | 200 | if remote_status == UNKNOWN_REMOTE_STATUS: | ||
586 | 201 | return BugTaskStatus.UNKNOWN | ||
587 | 202 | |||
588 | 203 | try: | ||
589 | 204 | launchpad_status = self.external_bugtracker.convertRemoteStatus( | ||
590 | 205 | remote_status) | ||
591 | 206 | except UnknownRemoteStatusError: | ||
592 | 207 | # We log the warning, since we need to know about statuses | ||
593 | 208 | # that we don't handle correctly. | ||
594 | 209 | self.warning( | ||
595 | 210 | "Unknown remote status '%s'." % remote_status, | ||
596 | 211 | get_remote_system_oops_properties( | ||
597 | 212 | self.external_bugtracker), | ||
598 | 213 | sys.exc_info()) | ||
599 | 214 | |||
600 | 215 | launchpad_status = BugTaskStatus.UNKNOWN | ||
601 | 216 | |||
602 | 217 | return launchpad_status | ||
603 | 218 | |||
604 | 0 | 219 | ||
605 | === modified file 'lib/lp/bugs/scripts/checkwatches/tests/test_core.py' | |||
606 | --- lib/lp/bugs/scripts/checkwatches/tests/test_core.py 2010-04-23 11:19:49 +0000 | |||
607 | +++ lib/lp/bugs/scripts/checkwatches/tests/test_core.py 2010-05-05 10:09:40 +0000 | |||
608 | @@ -11,6 +11,7 @@ | |||
609 | 11 | 11 | ||
610 | 12 | import transaction | 12 | import transaction |
611 | 13 | 13 | ||
612 | 14 | from datetime import datetime | ||
613 | 14 | from zope.component import getUtility | 15 | from zope.component import getUtility |
614 | 15 | 16 | ||
615 | 16 | from canonical.config import config | 17 | from canonical.config import config |
616 | @@ -24,9 +25,11 @@ | |||
617 | 24 | from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI | 25 | from lp.bugs.externalbugtracker.bugzilla import BugzillaAPI |
618 | 25 | from lp.bugs.interfaces.bugtracker import IBugTrackerSet | 26 | from lp.bugs.interfaces.bugtracker import IBugTrackerSet |
619 | 26 | from lp.bugs.scripts import checkwatches | 27 | from lp.bugs.scripts import checkwatches |
620 | 28 | from lp.bugs.scripts.checkwatches.base import ( | ||
621 | 29 | CheckWatchesErrorUtility, WorkingBase) | ||
622 | 27 | from lp.bugs.scripts.checkwatches.core import ( | 30 | from lp.bugs.scripts.checkwatches.core import ( |
625 | 28 | CheckwatchesMaster, TwistedThreadScheduler) | 31 | CheckwatchesMaster, LOGIN, TwistedThreadScheduler) |
626 | 29 | from lp.bugs.scripts.checkwatches.base import CheckWatchesErrorUtility | 32 | from lp.bugs.scripts.checkwatches.remotebugupdater import RemoteBugUpdater |
627 | 30 | from lp.bugs.tests.externalbugtracker import ( | 33 | from lp.bugs.tests.externalbugtracker import ( |
628 | 31 | TestBugzillaAPIXMLRPCTransport, TestExternalBugTracker, new_bugtracker) | 34 | TestBugzillaAPIXMLRPCTransport, TestExternalBugTracker, new_bugtracker) |
629 | 32 | from lp.testing import TestCaseWithFactory, ZopeTestInSubProcess | 35 | from lp.testing import TestCaseWithFactory, ZopeTestInSubProcess |
630 | @@ -57,10 +60,10 @@ | |||
631 | 57 | return self | 60 | return self |
632 | 58 | 61 | ||
633 | 59 | 62 | ||
636 | 60 | class NoBugWatchesByRemoteBugUpdater(checkwatches.CheckwatchesMaster): | 63 | class NoBugWatchesByRemoteBugUpdater(RemoteBugUpdater): |
637 | 61 | """A subclass of CheckwatchesMaster with methods overridden for testing.""" | 64 | """A subclass of RemoteBugUpdater with methods overridden for testing.""" |
638 | 62 | 65 | ||
640 | 63 | def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids): | 66 | def _getBugWatchesForRemoteBug(self): |
641 | 64 | """Return an empty list. | 67 | """Return an empty list. |
642 | 65 | 68 | ||
643 | 66 | This method overrides _getBugWatchesForRemoteBug() so that bug | 69 | This method overrides _getBugWatchesForRemoteBug() so that bug |
644 | @@ -154,10 +157,8 @@ | |||
645 | 154 | 157 | ||
646 | 155 | def test_bug_497141(self): | 158 | def test_bug_497141(self): |
647 | 156 | # Regression test for bug 497141. KeyErrors raised in | 159 | # Regression test for bug 497141. KeyErrors raised in |
649 | 157 | # CheckwatchesMaster.updateBugWatches() shouldn't cause | 160 | # RemoteBugUpdater.updateRemoteBug() shouldn't cause |
650 | 158 | # checkwatches to abort. | 161 | # checkwatches to abort. |
651 | 159 | updater = NoBugWatchesByRemoteBugUpdater( | ||
652 | 160 | transaction.manager, QuietFakeLogger()) | ||
653 | 161 | 162 | ||
654 | 162 | # Create a couple of bug watches for testing purposes. | 163 | # Create a couple of bug watches for testing purposes. |
655 | 163 | bug_tracker = self.factory.makeBugTracker() | 164 | bug_tracker = self.factory.makeBugTracker() |
656 | @@ -170,17 +171,27 @@ | |||
657 | 170 | remote_system = NonConnectingBugzillaAPI( | 171 | remote_system = NonConnectingBugzillaAPI( |
658 | 171 | bug_tracker.baseurl, xmlrpc_transport=test_transport) | 172 | bug_tracker.baseurl, xmlrpc_transport=test_transport) |
659 | 172 | 173 | ||
671 | 173 | # Calling updateBugWatches() shouldn't raise a KeyError, even | 174 | working_base = WorkingBase() |
672 | 174 | # though with our broken updater _getExternalBugTrackersAndWatches() | 175 | working_base.init(LOGIN, transaction.manager, QuietFakeLogger()) |
673 | 175 | # will return an empty dict. | 176 | |
674 | 176 | updater.updateBugWatches(remote_system, bug_watches) | 177 | for bug_watch in bug_watches: |
675 | 177 | 178 | updater = NoBugWatchesByRemoteBugUpdater( | |
676 | 178 | # An error will have been logged instead of the KeyError being | 179 | working_base, remote_system, bug_watch.remotebug, |
677 | 179 | # raised. | 180 | [bug_watch.id], [], datetime.now()) |
678 | 180 | error_utility = CheckWatchesErrorUtility() | 181 | |
679 | 181 | last_oops = error_utility.getLastOopsReport() | 182 | # Calling updateRemoteBug() shouldn't raise a KeyError, |
680 | 182 | self.assertTrue( | 183 | # even though with our broken updater |
681 | 183 | last_oops.value.startswith('Spurious remote bug ID')) | 184 | # _getExternalBugTrackersAndWatches() will return an empty |
682 | 185 | # dict. | ||
683 | 186 | updater.updateRemoteBug() | ||
684 | 187 | |||
685 | 188 | # An error will have been logged instead of the KeyError being | ||
686 | 189 | # raised. | ||
687 | 190 | error_utility = CheckWatchesErrorUtility() | ||
688 | 191 | last_oops = error_utility.getLastOopsReport() | ||
689 | 192 | self.assertTrue( | ||
690 | 193 | last_oops.value.startswith('Spurious remote bug ID'), | ||
691 | 194 | "Unexpected last OOPS value: %s" % last_oops.value) | ||
692 | 184 | 195 | ||
693 | 185 | def test_suggest_batch_size(self): | 196 | def test_suggest_batch_size(self): |
694 | 186 | class RemoteSystem: pass | 197 | class RemoteSystem: pass |
695 | 187 | 198 | ||
696 | === added file 'lib/lp/bugs/scripts/checkwatches/tests/test_remotebugupdater.py' | |||
697 | --- lib/lp/bugs/scripts/checkwatches/tests/test_remotebugupdater.py 1970-01-01 00:00:00 +0000 | |||
698 | +++ lib/lp/bugs/scripts/checkwatches/tests/test_remotebugupdater.py 2010-05-05 10:09:40 +0000 | |||
699 | @@ -0,0 +1,59 @@ | |||
700 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
701 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
702 | 3 | |||
703 | 4 | """Tests for the checkwatches remotebugupdater module.""" | ||
704 | 5 | |||
705 | 6 | __metaclass__ = type | ||
706 | 7 | |||
707 | 8 | import transaction | ||
708 | 9 | import unittest | ||
709 | 10 | |||
710 | 11 | from canonical.testing import LaunchpadZopelessLayer | ||
711 | 12 | |||
712 | 13 | from lp.bugs.externalbugtracker.bugzilla import Bugzilla | ||
713 | 14 | from lp.bugs.scripts.checkwatches.core import CheckwatchesMaster | ||
714 | 15 | from lp.bugs.scripts.checkwatches.remotebugupdater import RemoteBugUpdater | ||
715 | 16 | from lp.testing import TestCaseWithFactory | ||
716 | 17 | |||
717 | 18 | |||
718 | 19 | class RemoteBugUpdaterTestCase(TestCaseWithFactory): | ||
719 | 20 | |||
720 | 21 | layer = LaunchpadZopelessLayer | ||
721 | 22 | |||
722 | 23 | def test_create(self): | ||
723 | 24 | # CheckwatchesMaster.remote_bug_updater_factory points to the | ||
724 | 25 | # RemoteBugUpdater class, so it can be used to create | ||
725 | 26 | # RemoteBugUpdaters. | ||
726 | 27 | remote_system = Bugzilla('http://example.com') | ||
727 | 28 | remote_bug_id = '42' | ||
728 | 29 | bug_watch_ids = [1, 2] | ||
729 | 30 | unmodified_remote_ids = ['76'] | ||
730 | 31 | |||
731 | 32 | checkwatches_master = CheckwatchesMaster(transaction) | ||
732 | 33 | updater = checkwatches_master.remote_bug_updater_factory( | ||
733 | 34 | checkwatches_master, remote_system, remote_bug_id, | ||
734 | 35 | bug_watch_ids, unmodified_remote_ids, None) | ||
735 | 36 | |||
736 | 37 | self.assertTrue( | ||
737 | 38 | isinstance(updater, RemoteBugUpdater), | ||
738 | 39 | "updater should be an instance of RemoteBugUpdater.") | ||
739 | 40 | self.assertEqual( | ||
740 | 41 | remote_system, updater.external_bugtracker, | ||
741 | 42 | "Unexpected external_bugtracker for RemoteBugUpdater.") | ||
742 | 43 | self.assertEqual( | ||
743 | 44 | remote_bug_id, updater.remote_bug, | ||
744 | 45 | "RemoteBugUpdater's remote_bug should be '%s', was '%s'" % | ||
745 | 46 | (remote_bug_id, updater.remote_bug)) | ||
746 | 47 | self.assertEqual( | ||
747 | 48 | bug_watch_ids, updater.bug_watch_ids, | ||
748 | 49 | "RemoteBugUpdater's bug_watch_ids should be '%s', were '%s'" % | ||
749 | 50 | (bug_watch_ids, updater.bug_watch_ids)) | ||
750 | 51 | self.assertEqual( | ||
751 | 52 | unmodified_remote_ids, updater.unmodified_remote_ids, | ||
752 | 53 | "RemoteBugUpdater's unmodified_remote_ids should be '%s', " | ||
753 | 54 | "were '%s'" % | ||
754 | 55 | (unmodified_remote_ids, updater.unmodified_remote_ids)) | ||
755 | 56 | |||
756 | 57 | |||
757 | 58 | def test_suite(): | ||
758 | 59 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
759 | 0 | 60 | ||
760 | === added file 'lib/lp/bugs/scripts/checkwatches/utilities.py' | |||
761 | --- lib/lp/bugs/scripts/checkwatches/utilities.py 1970-01-01 00:00:00 +0000 | |||
762 | +++ lib/lp/bugs/scripts/checkwatches/utilities.py 2010-05-05 10:09:40 +0000 | |||
763 | @@ -0,0 +1,61 @@ | |||
764 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
765 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
766 | 3 | |||
767 | 4 | """Utility functions for checkwatches.""" | ||
768 | 5 | |||
769 | 6 | __metaclass__ = type | ||
770 | 7 | __all__ = [ | ||
771 | 8 | 'get_bugwatcherrortype_for_error', | ||
772 | 9 | 'get_remote_system_oops_properties', | ||
773 | 10 | ] | ||
774 | 11 | |||
775 | 12 | import socket | ||
776 | 13 | |||
777 | 14 | from lp.bugs.externalbugtracker import ( | ||
778 | 15 | BugNotFound, BugTrackerConnectError, InvalidBugId, PrivateRemoteBug, | ||
779 | 16 | UnknownBugTrackerTypeError, UnparseableBugData, | ||
780 | 17 | UnparseableBugTrackerVersion, UnsupportedBugTrackerVersion) | ||
781 | 18 | |||
782 | 19 | from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus | ||
783 | 20 | |||
784 | 21 | |||
785 | 22 | _exception_to_bugwatcherrortype = [ | ||
786 | 23 | (BugTrackerConnectError, BugWatchActivityStatus.CONNECTION_ERROR), | ||
787 | 24 | (PrivateRemoteBug, BugWatchActivityStatus.PRIVATE_REMOTE_BUG), | ||
788 | 25 | (UnparseableBugData, BugWatchActivityStatus.UNPARSABLE_BUG), | ||
789 | 26 | (UnparseableBugTrackerVersion, | ||
790 | 27 | BugWatchActivityStatus.UNPARSABLE_BUG_TRACKER), | ||
791 | 28 | (UnsupportedBugTrackerVersion, | ||
792 | 29 | BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER), | ||
793 | 30 | (UnknownBugTrackerTypeError, | ||
794 | 31 | BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER), | ||
795 | 32 | (InvalidBugId, BugWatchActivityStatus.INVALID_BUG_ID), | ||
796 | 33 | (BugNotFound, BugWatchActivityStatus.BUG_NOT_FOUND), | ||
797 | 34 | (PrivateRemoteBug, BugWatchActivityStatus.PRIVATE_REMOTE_BUG), | ||
798 | 35 | (socket.timeout, BugWatchActivityStatus.TIMEOUT)] | ||
799 | 36 | |||
800 | 37 | |||
801 | 38 | def get_bugwatcherrortype_for_error(error): | ||
802 | 39 | """Return the correct `BugWatchActivityStatus` for a given error.""" | ||
803 | 40 | for exc_type, bugwatcherrortype in _exception_to_bugwatcherrortype: | ||
804 | 41 | if isinstance(error, exc_type): | ||
805 | 42 | return bugwatcherrortype | ||
806 | 43 | else: | ||
807 | 44 | return BugWatchActivityStatus.UNKNOWN | ||
808 | 45 | |||
809 | 46 | |||
810 | 47 | def get_remote_system_oops_properties(remote_system): | ||
811 | 48 | """Return (name, value) tuples describing a remote system. | ||
812 | 49 | |||
813 | 50 | Each item in the list is intended for use as an OOPS property. | ||
814 | 51 | |||
815 | 52 | :remote_system: The `ExternalBugTracker` instance from which the | ||
816 | 53 | OOPS properties should be extracted. | ||
817 | 54 | """ | ||
818 | 55 | return [ | ||
819 | 56 | ('batch_size', remote_system.batch_size), | ||
820 | 57 | ('batch_query_threshold', remote_system.batch_query_threshold), | ||
821 | 58 | ('sync_comments', remote_system.sync_comments), | ||
822 | 59 | ('externalbugtracker', remote_system.__class__.__name__), | ||
823 | 60 | ('baseurl', remote_system.baseurl) | ||
824 | 61 | ] | ||
825 | 0 | 62 | ||
826 | === modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py' | |||
827 | --- lib/lp/bugs/scripts/tests/test_bugimport.py 2010-04-29 10:09:24 +0000 | |||
828 | +++ lib/lp/bugs/scripts/tests/test_bugimport.py 2010-05-05 10:09:40 +0000 | |||
829 | @@ -30,6 +30,7 @@ | |||
830 | 30 | from lp.bugs.scripts import bugimport | 30 | from lp.bugs.scripts import bugimport |
831 | 31 | from lp.bugs.scripts.bugimport import ET | 31 | from lp.bugs.scripts.bugimport import ET |
832 | 32 | from lp.bugs.scripts.checkwatches import CheckwatchesMaster | 32 | from lp.bugs.scripts.checkwatches import CheckwatchesMaster |
833 | 33 | from lp.bugs.scripts.checkwatches.remotebugupdater import RemoteBugUpdater | ||
834 | 33 | from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale | 34 | from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale |
835 | 34 | from lp.registry.interfaces.product import IProductSet | 35 | from lp.registry.interfaces.product import IProductSet |
836 | 35 | from lp.registry.model.person import generate_nick | 36 | from lp.registry.model.person import generate_nick |
837 | @@ -892,6 +893,29 @@ | |||
838 | 892 | return BugTaskImportance.UNKNOWN | 893 | return BugTaskImportance.UNKNOWN |
839 | 893 | 894 | ||
840 | 894 | 895 | ||
841 | 896 | class TestRemoteBugUpdater(RemoteBugUpdater): | ||
842 | 897 | |||
843 | 898 | def __init__(self, parent, external_bugtracker, remote_bug, | ||
844 | 899 | bug_watch_ids, unmodified_remote_ids, server_time, | ||
845 | 900 | bugtracker): | ||
846 | 901 | super(TestRemoteBugUpdater, self). __init__( | ||
847 | 902 | parent, external_bugtracker, remote_bug, bug_watch_ids, | ||
848 | 903 | unmodified_remote_ids, server_time) | ||
849 | 904 | self.bugtracker = bugtracker | ||
850 | 905 | |||
851 | 906 | def _getBugWatchesForRemoteBug(self): | ||
852 | 907 | """Returns a list of fake bug watch objects. | ||
853 | 908 | |||
854 | 909 | We override this method so that we always return bug watches | ||
855 | 910 | from our list of fake bug watches. | ||
856 | 911 | """ | ||
857 | 912 | return [ | ||
858 | 913 | bug_watch for bug_watch in ( | ||
859 | 914 | self.bugtracker.watches_needing_update) | ||
860 | 915 | if (bug_watch.remotebug == self.remote_bug and | ||
861 | 916 | bug_watch.id in self.bug_watch_ids) | ||
862 | 917 | ] | ||
863 | 918 | |||
864 | 895 | 919 | ||
865 | 896 | class TestCheckwatchesMaster(CheckwatchesMaster): | 920 | class TestCheckwatchesMaster(CheckwatchesMaster): |
866 | 897 | """A mock `CheckwatchesMaster` object.""" | 921 | """A mock `CheckwatchesMaster` object.""" |
867 | @@ -905,28 +929,21 @@ | |||
868 | 905 | """See `CheckwatchesMaster`.""" | 929 | """See `CheckwatchesMaster`.""" |
869 | 906 | return [(TestExternalBugTracker(bug_tracker.baseurl), bug_watches)] | 930 | return [(TestExternalBugTracker(bug_tracker.baseurl), bug_watches)] |
870 | 907 | 931 | ||
886 | 908 | def _getBugWatchesForRemoteBug(self, remote_bug_id, bug_watch_ids): | 932 | def remote_bug_updater_factory(self, parent, external_bugtracker, |
887 | 909 | """Returns a list of fake bug watch objects. | 933 | remote_bug, bug_watch_ids, |
888 | 910 | 934 | unmodified_remote_ids, server_time): | |
889 | 911 | We override this method so that we always return bug watches | 935 | return TestRemoteBugUpdater( |
890 | 912 | from our list of fake bug watches. | 936 | self, external_bugtracker, remote_bug, bug_watch_ids, |
891 | 913 | """ | 937 | unmodified_remote_ids, server_time, self.bugtracker) |
892 | 914 | return [ | 938 | |
893 | 915 | bug_watch for bug_watch in ( | 939 | |
894 | 916 | self.bugtracker.watches_needing_update) | 940 | class CheckwatchesErrorRecoveryTestCase(unittest.TestCase): |
880 | 917 | if (bug_watch.remotebug == remote_bug_id and | ||
881 | 918 | bug_watch.id in bug_watch_ids) | ||
882 | 919 | ] | ||
883 | 920 | |||
884 | 921 | |||
885 | 922 | class CheckBugWatchesErrorRecoveryTestCase(unittest.TestCase): | ||
895 | 923 | """Test that errors in the bugwatch import process don't | 941 | """Test that errors in the bugwatch import process don't |
896 | 924 | invalidate the entire run. | 942 | invalidate the entire run. |
897 | 925 | """ | 943 | """ |
898 | 926 | layer = LaunchpadZopelessLayer | 944 | layer = LaunchpadZopelessLayer |
899 | 927 | 945 | ||
902 | 928 | def test_checkbugwatches_error_recovery(self): | 946 | def test_checkwatches_error_recovery(self): |
901 | 929 | |||
903 | 930 | firefox = getUtility(IProductSet).get(4) | 947 | firefox = getUtility(IProductSet).get(4) |
904 | 931 | foobar = getUtility(IPersonSet).get(16) | 948 | foobar = getUtility(IPersonSet).get(16) |
905 | 932 | params = CreateBugParams( | 949 | params = CreateBugParams( |
Hi Graham,
This mad refactorage is looking good :) I have a few comments but
nothing major, so r=me.
Gavin.
> === modified file 'lib/lp/ bugs/doc/ externalbugtrac ker.txt' bugs/doc/ externalbugtrac ker.txt 2010-04-21 10:33:55 +0000 bugs/doc/ externalbugtrac ker.txt 2010-04-29 10:42:49 +0000 atus() method on the ExternalBugTracker via tatus() method. atusError( remote_ status) ster._convertRe moteStatus( ) will handle these errors and will er._convertRemo teStatus( ) will handle these errors and will UNKNOWN when they occur. It will also log a updater. _convertRemoteS tatus( gExternalBugTra cker(), 'new') updater. _makeRemoteBugU pdater( gExternalBugTra cker(), '1', [1], []) bug_updater. _convertRemoteS tatus(' new') updater. _convertRemoteS tatus( gExternalBugTra cker(), 'spam') bug_updater. _convertRemoteS tatus(' spam') bugs/scripts/ checkwatches/ bugwatchupdater .py' bugs/scripts/ checkwatches/ bugwatchupdater .py 2010-04-26 12:34:47 +0000 bugs/scripts/ checkwatches/ bugwatchupdater .py 2010-04-29 10:42:49 +0000 interfaces. bug import IBugSet scripts. checkwatches. base import ( scripts. checkwatches. utilities import ( system_ oops_properties ) interfaces. person import PersonCreationR ationale (WorkingBase) : ts(self) : scripts. checkwatches. core import ( system_ oops_properties ) watch.bug. id watch.remotebug bugs/scripts/ checkwatches/ core.py' bugs/scripts/ checkwatches/ core.py 2010-04-26 12:34:47 +0000 bugs/scripts/ checkwatches/ core.py 2010-04-29 10:42:49 +0000 database. constants import UTC_NOW database. sqlbase import flush_database_ updates
> --- lib/lp/
> +++ lib/lp/
> @@ -641,7 +641,7 @@
>
> === Converting statuses ===
>
> -Once it has retrieved the bugs from the remote server, CheckwatchesMaster
> +Once it has retrieved the bugs from the remote server, RemoteBugUpdater
> attempts to convert their statuses into Launchpad BugTaskStatuses by
> calling the convertRemoteSt
> its own _convertRemoteS
> @@ -660,17 +660,17 @@
> ... else:
> ... raise UnknownRemoteSt
>
> -CheckwatchesMa
> +RemoteBugUpdat
> return BugTaskStatus.
> warning.
>
> - >>> status = bug_watch_
> - ... StatusConvertin
> + >>> remote_bug_updater = bug_watch_
> + ... StatusConvertin
> + >>> status = remote_
> >>> print status.title
> New
>
> - >>> status = bug_watch_
> - ... StatusConvertin
> + >>> status = remote_
> WARNING...Unknown remote status 'spam'. (OOPS-...)
> >>> print status.title
> Unknown
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -26,9 +26,12 @@
> from lp.bugs.
> from lp.bugs.
> WorkingBase, commit_before)
> +from lp.bugs.
> + get_remote_
> from lp.registry.
>
>
> +
> class BugWatchUpdater
> """Handles the updating of a single BugWatch for checkwatches."""
>
> @@ -81,10 +84,6 @@
> @commit_before
> def importBugCommen
> """Import all the comments from the remote bug."""
> - # Avoid circularity.
> - from lp.bugs.
> - get_remote_
> -
> with self.transaction:
> local_bug_id = self.bug_
> remote_bug_id = self.bug_
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -37,22 +37,21 @@
> from canonical.
> from canonical.
> f...