ExternalBugTracker: Bugzilla ============================ An ExternalBugtracker is used to talk to remote bug trackers and update bug watches. This document describes how the Bugzilla implementation of ExternalBugTracker works. Basics ------ The class that implements ExternalBugTracker is called Bugzilla. >>> from lp.bugs.externalbugtracker import Bugzilla >>> from lp.bugs.interfaces.externalbugtracker import ( ... IExternalBugTracker) >>> from lp.services.webapp.testing import verifyObject >>> external_bugzilla = Bugzilla('http://example.com') >>> verifyObject(IExternalBugTracker, external_bugzilla) True The Bugzilla ExternalBugTracker works differently depending on which version of Bugzilla it is talking to. If it's a version we can't parse, UnparsableBugTrackerVersion is raised: >>> from lp.testing.layers import LaunchpadZopelessLayer >>> txn = LaunchpadZopelessLayer.txn >>> external_bugzilla = Bugzilla('http://example.com/', version='A.B') Traceback (most recent call last): ... UnparsableBugTrackerVersion: Failed to parse version 'A.B' for http://... The version parsing is carried out by the Bugzilla._parseVersion() method, which takes a version string and returns a tuple of (major_version, minor_version). >>> external_bugzilla = Bugzilla('http://example.com') >>> print external_bugzilla._parseVersion('3.2') (3, 2) It can handle version strings with an -$foo suffix. >>> print external_bugzilla._parseVersion('3.2-foobar') (3, 2) And will also handle versions which contain the string 'rc'. >>> print external_bugzilla._parseVersion('3.2rc') (3, 2) + characters in the version string will be removed. >>> print external_bugzilla._parseVersion('3.2+1') (3, 2, 1) Since we don't want to depend on a working network connection, we use a slightly modified implementation. >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet >>> from lp.bugs.tests.externalbugtracker import TestBugzilla >>> gnome_bugzilla = ( ... getUtility(IBugTrackerSet).getByName('gnome-bugzilla')) >>> external_bugzilla = TestBugzilla(gnome_bugzilla.baseurl) >>> version = external_bugzilla._probe_version() >>> version (2, 20) Launchpad plugin ---------------- Some Bugzillas offer the Bugzilla 3.4 XML-RPC API or have a Launchpad plugin installed. For these bugtrackers, we use the BugzillaAPI ExternalBugTracker or its subclass, BugzillaLPPlugin, depending upon which type of Bugzilla we're dealing with. The Bugzilla ExternalBugTracker class has a getExternalBugTrackerToUse() method which will return a BugzillaAPI instance if the remote Bugzilla offers the 3.4 API or a BugzillaLPPlugin instance if the remote Bugzilla has the plugin installed. If neither of these is offered, a standard Bugzilla ExternalBugTracker will be returned. The Bugzilla ExternalBugTracker has a _test_xmlrpc_proxy property which we override for the purpose of this test. >>> import xmlrpclib >>> class FailingXMLRPCTransport(xmlrpclib.Transport): ... ... error = xmlrpclib.Fault( ... xmlrpclib.METHOD_NOT_FOUND, "Method doesn't exist") ... ... def request(self, host, handler, request, verbose=None): ... if self.error is not None: ... raise self.error ... else: ... # We need to return something here, otherwise ... # xmlrpclib will explode. ... return '0.42-test' ... >>> test_transport = FailingXMLRPCTransport() >>> class BugzillaWithFakeProxy(Bugzilla): ... ... _test_xmlrpc_proxy = xmlrpclib.ServerProxy( ... 'http://example.com/xmlrpc.cgi', transport=test_transport) >>> bugzilla = BugzillaWithFakeProxy('http://example.com') When getExternalBugTrackerToUse() receives a Fault of type METHOD_NOT_FOUND from the remote server in response to its check, it will return a standard Bugzilla instance. >>> transaction.commit() >>> from lp.bugs.externalbugtracker import ( ... BugzillaAPI, BugzillaLPPlugin) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() The returned bugtracker will be a Bugzilla instance bug not a BugzillaAPI instance. >>> (isinstance(bugzilla_to_use, Bugzilla) and ... not isinstance(bugzilla_to_use, BugzillaAPI)) True The same is true if getExternalBugTrackerToUse() receives a 404 error from the remote server. >>> test_transport.error = xmlrpclib.ProtocolError( ... 'http://example.com/xmlrpc.cgi', 404, 'Not Found', None) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, Bugzilla) and ... not isinstance(bugzilla_to_use, BugzillaAPI)) True Some Bugzillas respond to an invalid XML-RPC method call by returning a 500 error. getExternalBugTrackerToUse() handles those, too. >>> test_transport.error = xmlrpclib.ProtocolError( ... 'http://example.com/xmlrpc.cgi', 500, 'Server Error', None) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, Bugzilla) and ... not isinstance(bugzilla_to_use, BugzillaAPI)) True Some other Bugzillas generate an unparsable response, causing ResponseError to be raised. >>> test_transport.error = xmlrpclib.ResponseError() >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, Bugzilla) and ... not isinstance(bugzilla_to_use, BugzillaAPI)) True If the remote Bugzilla offers the Bugzilla 3.4 API, an instance of BuzillaAPI will be returned. To test this, we use a specially-crafted XML-RPC proxy that behaves like a Bugzilla 3.4 instance. >>> class APIXMLRPCTransport(xmlrpclib.Transport): ... ... version = '3.4.2' ... ... def request(self, host, handler, request, verbose=None): ... args, method_name = xmlrpclib.loads(request) ... ... if method_name == 'Bugzilla.version': ... return [{'version': self.version}] ... else: ... raise xmlrpclib.Fault( ... xmlrpclib.METHOD_NOT_FOUND, 'No such method') ... >>> test_transport = APIXMLRPCTransport() >>> bugzilla._test_xmlrpc_proxy = xmlrpclib.ServerProxy( ... 'http://example.com/xmlrpc.cgi', ... transport=test_transport) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, BugzillaAPI) and ... not isinstance(bugzilla_to_use, BugzillaLPPlugin)) True If the remote system has the Launchpad plugin installed, an getExternalBugTrackerToUse() will return a BugzillaLPPlugin instance. >>> class PluginXMLRPCTransport(xmlrpclib.Transport): ... ... def request(self, host, handler, request, verbose=None): ... args, method_name = xmlrpclib.loads(request) ... ... if method_name == 'Launchpad.plugin_version': ... return [{'version': '0.2'}] ... else: ... raise xmlrpclib.Fault( ... xmlrpclib.METHOD_NOT_FOUND, 'No such method') ... >>> test_transport = PluginXMLRPCTransport() >>> bugzilla._test_xmlrpc_proxy = xmlrpclib.ServerProxy( ... 'http://example.com/xmlrpc.cgi', ... transport=test_transport) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> isinstance(bugzilla_to_use, BugzillaLPPlugin) True Older versions of the Bugzilla API return tuples rather than mappings in response to XML-RPC calls. When something other than a mapping is returned, the standard non-API non-plugin external bug tracker is selected. >>> class OldXMLRPCTransport(xmlrpclib.Transport): ... def request(self, host, handler, request, verbose=None): ... args, method_name = xmlrpclib.loads(request) ... ... if method_name == 'Bugzilla.version': ... return ('versionResponse', {'version': '3.2.5+'}) ... else: ... raise xmlrpclib.Fault( ... xmlrpclib.METHOD_NOT_FOUND, 'No such method') ... >>> test_transport = OldXMLRPCTransport() >>> bugzilla._test_xmlrpc_proxy = xmlrpclib.ServerProxy( ... 'http://example.com/xmlrpc.cgi', ... transport=test_transport) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, BugzillaAPI) or ... isinstance(bugzilla_to_use, BugzillaLPPlugin)) False Some Bugzillas return 'Client' instead of METHOD_NOT_FOUND when a method is not discovered over XML-RPC. It's not clear if this is an error in Bugzilla or in and XML-RPC library used by Bugzilla. In any case, we recognize and treat it the same as METHOD_NOT_FOUND. >>> class OldBrokenXMLRPCTransport(xmlrpclib.Transport): ... def request(self, host, handler, request, verbose=None): ... args, method_name = xmlrpclib.loads(request) ... ... if method_name == 'Bugzilla.version': ... return ('versionResponse', {'version': '3.2.5+'}) ... else: ... raise xmlrpclib.Fault('Client', 'No such method') ... >>> test_transport = OldBrokenXMLRPCTransport() >>> bugzilla._test_xmlrpc_proxy = xmlrpclib.ServerProxy( ... 'http://example.com/xmlrpc.cgi', ... transport=test_transport) >>> bugzilla_to_use = bugzilla.getExternalBugTrackerToUse() >>> (isinstance(bugzilla_to_use, BugzillaAPI) or ... isinstance(bugzilla_to_use, BugzillaLPPlugin)) False Status Conversion ----------------- It contains a function for converting one of its own status to a Malone status. Bugzilla statuses consist of two parts, the status, and the resolution, separated by a space character. The resolution only exists if the bug is closed: >>> external_bugzilla.convertRemoteStatus('UNCONFIRMED').title 'New' >>> external_bugzilla.convertRemoteStatus('NEW').title 'Confirmed' >>> external_bugzilla.convertRemoteStatus('ASSIGNED').title 'In Progress' >>> external_bugzilla.convertRemoteStatus('REOPENED').title 'Confirmed' >>> external_bugzilla.convertRemoteStatus('NEEDINFO').title 'Incomplete' >>> external_bugzilla.convertRemoteStatus('NEEDINFO_REPORTER').title 'Incomplete' >>> external_bugzilla.convertRemoteStatus('NEEDSINFO').title 'Incomplete' >>> external_bugzilla.convertRemoteStatus('MODIFIED').title 'Fix Committed' >>> external_bugzilla.convertRemoteStatus('UPSTREAM').title 'Confirmed' >>> external_bugzilla.convertRemoteStatus('PENDINGUPLOAD').title 'Fix Committed' >>> external_bugzilla.convertRemoteStatus('RESOLVED FIXED').title 'Fix Released' >>> external_bugzilla.convertRemoteStatus('RESOLVED UPSTREAM').title "Won't Fix" >>> external_bugzilla.convertRemoteStatus( ... 'CLOSED PATCH_ALREADY_AVAILABLE').title 'Fix Released' >>> external_bugzilla.convertRemoteStatus('RESOLVED CODE_FIX').title 'Fix Released' >>> external_bugzilla.convertRemoteStatus('VERIFIED WONTFIX').title "Won't Fix" >>> external_bugzilla.convertRemoteStatus('CLOSED INVALID').title 'Invalid' >>> external_bugzilla.convertRemoteStatus('CLOSED DUPLICATE').title 'Invalid' >>> external_bugzilla.convertRemoteStatus('CLOSED UPSTREAM').title "Won't Fix" If the status can't be converted an UnknownRemoteStatusError will be returned. >>> external_bugzilla.convertRemoteStatus('FOO').title Traceback (most recent call last): ... UnknownRemoteStatusError: FOO >>> external_bugzilla.convertRemoteStatus('CLOSED BAR').title Traceback (most recent call last): ... UnknownRemoteStatusError: CLOSED BAR Importance Conversion --------------------- There is also a function for conversion of bugzilla importances to launchpad importances. The Bugzilla importance is comprised of priority and severity, but we only use severity in mapping the value unless it isn't available in which case we map against priority values. >>> external_bugzilla.convertRemoteImportance('URGENT BLOCKER').title 'Critical' >>> external_bugzilla.convertRemoteImportance('LOW BLOCKER').title 'Critical' >>> external_bugzilla.convertRemoteImportance('BLOCKER').title 'Critical' >>> external_bugzilla.convertRemoteImportance('URGENT CRITICAL').title 'Critical' >>> external_bugzilla.convertRemoteImportance('LOW CRITICAL').title 'Critical' >>> external_bugzilla.convertRemoteImportance('CRITICAL').title 'Critical' >>> external_bugzilla.convertRemoteImportance('URGENT MAJOR').title 'High' >>> external_bugzilla.convertRemoteImportance('LOW MAJOR').title 'High' >>> external_bugzilla.convertRemoteImportance('MAJOR').title 'High' >>> external_bugzilla.convertRemoteImportance('CRASH').title 'High' >>> external_bugzilla.convertRemoteImportance('GRAVE').title 'High' >>> external_bugzilla.convertRemoteImportance('URGENT NORMAL').title 'Medium' >>> external_bugzilla.convertRemoteImportance('LOW NORMAL').title 'Medium' >>> external_bugzilla.convertRemoteImportance('NORMAL').title 'Medium' >>> external_bugzilla.convertRemoteImportance('NOR').title 'Medium' >>> external_bugzilla.convertRemoteImportance('URGENT MINOR').title 'Low' >>> external_bugzilla.convertRemoteImportance('LOW MINOR').title 'Low' >>> external_bugzilla.convertRemoteImportance('MINOR').title 'Low' >>> external_bugzilla.convertRemoteImportance('URGENT TRIVIAL').title 'Low' >>> external_bugzilla.convertRemoteImportance('LOW TRIVIAL').title 'Low' >>> external_bugzilla.convertRemoteImportance('TRIVIAL').title 'Low' >>> external_bugzilla.convertRemoteImportance('LOW ENHANCEMENT').title 'Wishlist' >>> external_bugzilla.convertRemoteImportance('ENHANCEMENT').title 'Wishlist' >>> external_bugzilla.convertRemoteImportance('WISHLIST').title 'Wishlist' >>> external_bugzilla.convertRemoteImportance('IMMEDIATE').title 'Critical' >>> external_bugzilla.convertRemoteImportance('URGENT').title 'Critical' >>> external_bugzilla.convertRemoteImportance('HIGH').title 'High' >>> external_bugzilla.convertRemoteImportance('MEDIUM').title 'Medium' >>> external_bugzilla.convertRemoteImportance('LOW').title 'Low' >>> external_bugzilla.convertRemoteImportance('P5').title 'Critical' >>> external_bugzilla.convertRemoteImportance('P4').title 'High' >>> external_bugzilla.convertRemoteImportance('P3').title 'Medium' >>> external_bugzilla.convertRemoteImportance('P2').title 'Low' >>> external_bugzilla.convertRemoteImportance('P1').title 'Low' Some bugzillas don't provide a value, resulting in blank strings for priority and severity. We simply leave the importance unknown in this case. >>> external_bugzilla.convertRemoteImportance('').title 'Unknown' However, we still treat as an error if the priority or severity are set to some other unexpected string. >>> external_bugzilla.convertRemoteImportance('foo bar') Traceback (most recent call last): ... UnknownRemoteImportanceError: foo bar >>> external_bugzilla.convertRemoteImportance('%&*@*#&$%!') Traceback (most recent call last): ... UnknownRemoteImportanceError: %&*@*#&$%! Updating Bug Watches -------------------- The main use of an ExternalBugtracker is to update bug watches. This is done through updateBugWatches(), which expects a list of bug watches to update: >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster >>> bug_watch_updater = CheckwatchesMaster(txn) >>> for bug_watch in gnome_bugzilla.watches: ... print "%s: %s %s" % (bug_watch.remotebug, ... bug_watch.remotestatus, ... bug_watch.remote_importance) 304070: None None 3224: None >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, gnome_bugzilla.watches) INFO:...:Updating 2 watches for 2 bugs on http://bugzilla.gnome.org/bugs INFO:...Didn't find bug u'304070' on http://bugzilla.gnome.org/bugs (local bugs: 15). >>> for bug_watch in gnome_bugzilla.watches: ... print "%s: %s %s" % (bug_watch.remotebug, ... bug_watch.remotestatus, ... bug_watch.remote_importance) 304070: None None 3224: RESOLVED FIXED MINOR URGENT Let's add a handful of watches: >>> from lp.bugs.interfaces.bug import IBugSet >>> from lp.bugs.interfaces.bugwatch import IBugWatchSet >>> from lp.registry.interfaces.person import IPersonSet >>> sample_person = getUtility(IPersonSet).getByEmail( ... 'test@canonical.com') >>> bug_one = getUtility(IBugSet).get(1) >>> bug_watch_set = getUtility(IBugWatchSet) >>> expected_remote_statuses = dict( ... [(int(bug_watch.remotebug), bug_watch.remotestatus) ... for bug_watch in gnome_bugzilla.watches]) >>> expected_remote_importances = dict( ... [(int(bug_watch.remotebug), bug_watch.remote_importance) ... for bug_watch in gnome_bugzilla.watches]) >>> for remote_bug_id in range(50,55): ... bug_watch = bug_watch_set.createBugWatch( ... bug=bug_one, owner=sample_person, bugtracker=gnome_bugzilla, ... remotebug=str(remote_bug_id)) ... external_bugzilla.bugzilla_bugs[remote_bug_id] = ( ... 'RESOLVED', 'FIXED', 'HIGH', 'ENHANCEMENT') ... expected_remote_statuses[remote_bug_id] = 'RESOLVED FIXED' ... expected_remote_importances[remote_bug_id] = 'HIGH ENHANCEMENT' Set the batch threshold higher than the number of bug watches. >>> external_bugzilla.batch_query_threshold = 10 Then updateBugWatches() will make one request per bug watch: >>> external_bugzilla.trace_calls = True >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, gnome_bugzilla.watches) INFO:...:Updating 7 watches for 7 bugs on http://bugzilla.gnome.org/bugs CALLED _postPage() CALLED _postPage() CALLED _postPage() CALLED _postPage() CALLED _postPage() CALLED _postPage() CALLED _postPage() INFO:...:Didn't find bug u'304070' on http://bugzilla.gnome.org/bugs (local bugs: 15). >>> remote_statuses = dict( ... [(int(bug_watch.remotebug), bug_watch.remotestatus) ... for bug_watch in gnome_bugzilla.watches]) >>> remote_statuses == expected_remote_statuses True >>> remote_importances = dict( ... [(int(bug_watch.remotebug), bug_watch.remote_importance) ... for bug_watch in gnome_bugzilla.watches]) >>> remote_importances == expected_remote_importances True >>> external_bugzilla.trace_calls = False Let's add a few more watches: >>> expected_remote_statuses = dict( ... [(int(bug_watch.remotebug), bug_watch.remotestatus) ... for bug_watch in gnome_bugzilla.watches]) >>> expected_remote_importances = dict( ... [(int(bug_watch.remotebug), bug_watch.remote_importance) ... for bug_watch in gnome_bugzilla.watches]) >>> for remote_bug_id in range(100,300): ... bug_watch = bug_watch_set.createBugWatch( ... bug=bug_one, owner=sample_person, bugtracker=gnome_bugzilla, ... remotebug=str(remote_bug_id)) ... external_bugzilla.bugzilla_bugs[remote_bug_id] = ( ... 'ASSIGNED', '', 'MEDIUM', 'URGENT') ... expected_remote_statuses[remote_bug_id] = 'ASSIGNED' ... expected_remote_importances[remote_bug_id] = 'MEDIUM URGENT' Set the batch threshold very low and remove the batch size limit: >>> external_bugzilla.batch_query_threshold = 0 >>> external_bugzilla.batch_size = None Instead of issuing one request per bug watch, like was done before, updateBugWatches() issues only one request to update all watches: >>> external_bugzilla.trace_calls = True >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, gnome_bugzilla.watches) INFO:...:Updating 207 watches for 207 bugs... CALLED _postPage() INFO:...:Didn't find bug u'304070' on http://bugzilla.gnome.org/bugs (local bugs: 15). >>> remote_statuses = dict( ... [(int(bug_watch.remotebug), bug_watch.remotestatus) ... for bug_watch in gnome_bugzilla.watches]) >>> remote_statuses == expected_remote_statuses True >>> remote_importances = dict( ... [(int(bug_watch.remotebug), bug_watch.remote_importance) ... for bug_watch in gnome_bugzilla.watches]) >>> remote_importances == expected_remote_importances True >>> external_bugzilla.trace_calls = False updateBugWatches() updates the lastchecked attribute on the watches, so now no bug watches are in need of updating: >>> from canonical.database.sqlbase import flush_database_updates >>> flush_database_updates() >>> gnome_bugzilla.watches_needing_update.count() 0 If the status isn't different, the lastchanged attribute doesn't get updated: >>> import pytz >>> from datetime import datetime, timedelta >>> bug_watch = gnome_bugzilla.watches[0] >>> now = datetime.now(pytz.timezone('UTC')) >>> bug_watch.lastchanged = now - timedelta(weeks=2) >>> old_last_changed = bug_watch.lastchanged >>> bug_watch_updater.updateBugWatches(external_bugzilla, [bug_watch]) INFO:...:Updating 1 watches for 1 bugs on http://bugzilla.gnome.org/bugs >>> bug_watch.lastchanged == old_last_changed True Now let's take a look at what happens when a bug watch is linked to from a bug task. >>> bug_nine = getUtility(IBugSet).get(9) >>> thunderbird_task = bug_nine.bugtasks[0] >>> print thunderbird_task.status.title Unknown >>> thunderbird_task.bugwatch.remotestatus is None True >>> thunderbird_task.bugwatch.remote_importance is None True Importance gets updated for Bugzilla bugs. Let's set it to some bogus value, and see that it gets set to a proper value. >>> from lp.bugs.interfaces.bugtask import BugTaskImportance >>> thunderbird_task.transitionToImportance( ... BugTaskImportance.HIGH, ... thunderbird_task.pillar.owner) We need to create a new ExternalBugtracker for the Mozilla tracker: >>> mozilla_bugzilla = getUtility(IBugTrackerSet).getByName( ... 'mozilla.org') >>> external_bugzilla = TestBugzilla(mozilla_bugzilla.baseurl, '2.20') >>> external_bugzilla.bugzilla_bugs = {1234: ( ... 'ASSIGNED', '', 'MEDIUM', 'ENHANCEMENT')} Let's update the bug watch, and see that the linked bug watch got synced: >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, [thunderbird_task.bugwatch]) INFO:...:Updating 1 watches for 1 bugs on https://bugzilla.mozilla.org >>> bug_nine = getUtility(IBugSet).get(9) >>> thunderbird_task = bug_nine.bugtasks[0] >>> print thunderbird_task.status.title In Progress >>> print thunderbird_task.importance.title Wishlist >>> print thunderbird_task.bugwatch.remotestatus ASSIGNED >>> print thunderbird_task.bugwatch.remote_importance MEDIUM ENHANCEMENT If we change the bugtask status, it will be updated again even though the remote status hasn't changed. This can happen if we change the status mapping. >>> from lp.bugs.interfaces.bugtask import BugTaskStatus >>> thunderbird_task.transitionToStatus( ... BugTaskStatus.CONFIRMED, ... getUtility(IPersonSet).getByName('no-priv')) >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, [thunderbird_task.bugwatch]) INFO:...:Updating 1 watches for 1 bugs on https://bugzilla.mozilla.org >>> bug_nine = getUtility(IBugSet).get(9) >>> thunderbird_task = bug_nine.bugtasks[0] >>> print thunderbird_task.status.title In Progress >>> print thunderbird_task.importance.title Wishlist >>> print thunderbird_task.bugwatch.remotestatus ASSIGNED >>> print thunderbird_task.bugwatch.remote_importance MEDIUM ENHANCEMENT If there are two bug watches, linked to different bugs, pointing to the same remote bug, both will of course be updated. >>> external_bugzilla.bugzilla_bugs[42] = ( ... 'RESOLVED', 'FIXED', 'LOW', 'BLOCKER') >>> bug_watch1 = bug_watch_set.createBugWatch( ... bug=bug_one, owner=sample_person, bugtracker=mozilla_bugzilla, ... remotebug='42') >>> bug_watch1_id = bug_watch1.id >>> bug_two = getUtility(IBugSet).get(2) >>> bug_watch2 = bug_watch_set.createBugWatch( ... bug=bug_two, owner=sample_person, bugtracker=mozilla_bugzilla, ... remotebug='42') >>> bug_watch2_id = bug_watch2.id >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, [bug_watch1, bug_watch2]) INFO:...:Updating 2 watches for 1 bugs on https://bugzilla.mozilla.org >>> bug_watch1 = getUtility(IBugWatchSet).get(bug_watch1_id) >>> print bug_watch1.remotestatus RESOLVED FIXED >>> print bug_watch1.remote_importance LOW BLOCKER >>> bug_watch2 = getUtility(IBugWatchSet).get(bug_watch2_id) >>> print bug_watch2.remotestatus RESOLVED FIXED >>> print bug_watch2.remote_importance LOW BLOCKER If updateBugWatches() can't parse the XML file returned from the remote bug tracker, an error is logged. >>> external_bugzilla._postPage = ( ... lambda self, data, repost_on_redirect: '') >>> bug_watch_updater.updateBugWatches( ... external_bugzilla, [bug_watch1, bug_watch2]) Traceback (most recent call last): ... UnparsableBugData: Failed to parse XML description for https://bugzilla.mozilla.org... The error is also recorded in each bug watch's last_error_type field so that it can be displayed to the user. >>> bug_watch1 = getUtility(IBugWatchSet).get(bug_watch1_id) >>> bug_watch1.last_error_type.title 'Unparsable Bug' >>> bug_watch2 = getUtility(IBugWatchSet).get(bug_watch2_id) >>> bug_watch2.last_error_type.title 'Unparsable Bug' Getting Remote Product ---------------------- getRemoteProduct() returns the product a remote bug is associated with in Bugzilla. getRemoteProduct() has to be called after initializeRemoteBugDB() has been called, in order for the bug information to be fetched from the external Bugzilla instance. >>> external_bugzilla = TestBugzilla() >>> external_bugzilla.bugzilla_bugs = {84: ( ... 'RESOLVED', 'FIXED', 'MEDIUM', 'NORMAL')} >>> external_bugzilla.initializeRemoteBugDB(['84']) >>> external_bugzilla.remote_bug_product['84'] u'product-84' >>> external_bugzilla.getRemoteProduct('84') u'product-84' Sometimes we might not get the product in the bug listing. In these cases getRemoteProduct() returns None. >>> external_bugzilla = TestBugzilla() >>> external_bugzilla.bugzilla_bugs = {84: ( ... 'RESOLVED', 'FIXED', 'MEDIUM', 'NORMAL')} >>> # Make the buglist XML not include the product tag. >>> external_bugzilla.bug_item_file = 'gnome_bug_li_item_noproduct.xml' >>> external_bugzilla.initializeRemoteBugDB(['84']) >>> print external_bugzilla.getRemoteProduct('84') None Requesting the product for a bug that doesn't exist raises BugNotFound. >>> external_bugzilla = TestBugzilla() >>> external_bugzilla.bugzilla_bugs = {84: ( ... 'RESOLVED', 'FIXED', 'MEDIUM', 'NORMAL')} >>> external_bugzilla.initializeRemoteBugDB(['84']) >>> external_bugzilla.getRemoteProduct('42') Traceback (most recent call last): ... BugNotFound: 42