= ExternalBugTracker: RT = This covers the implementation of an ExternalBugTracker class for RT instances. == Basics == When importing bugs from remote RT instances, we use an RT-specific implementation of ExternalBugTracker, RequestTracker. >>> from lp.bugs.externalbugtracker import ( ... RequestTracker) >>> from lp.bugs.interfaces.bugtracker import BugTrackerType >>> from lp.bugs.interfaces.externalbugtracker import IExternalBugTracker >>> from lp.bugs.tests.externalbugtracker import ( ... new_bugtracker) >>> from canonical.launchpad.webapp.testing import verifyObject >>> verifyObject( ... IExternalBugTracker, ... RequestTracker('http://example.com/')) True The RequestTracker class offers an _opener property, an instance of urllib2.OpenerDirector which will handle cookies and so allow the RequestTracker instance to work correctly with RT cookies. We can demonstrate this by creating a test class which contains a stub method for RequestTracker._logIn(). >>> class NoLogInRequestTracker(RequestTracker): ... def _logIn(self, opener): ... """This method does nothing but say it's been called.""" ... print "_logIn() has been called." >>> request_tracker = NoLogInRequestTracker('http://example.com/') >>> request_tracker._opener _logIn() has been called. == Authentication Credentials == RT instances require that we log in to be able to export statuses for their tickets. The RequestTracker ExternalBugTracker class has a credentials property which returns a dict of credentials based on the hostname of the current remote RT instance. The default username and password for RT instances are 'guest' and 'guest'. The credentials property for an RT instance that we don't have specific credentials for will return the default credentials. >>> rt_one = RequestTracker('http://foobar.com') >>> rt_one.credentials {'user': 'guest', 'pass': 'guest'} However, if the RT instance is one for which we have a username and password, those credentials will be retrieved from the Launchpad configuration files. rt.example.com is known to Launchpad. >>> rt_two = RequestTracker('http://rt.example.com') >>> rt_two.credentials {'user': 'zaphod', 'pass': 'pangalacticgargleblaster'} == Status Conversion == The RequestTracker class can convert the default RT ticket statuses into Launchpad statuses: >>> rt = RequestTracker('http://example.com/') >>> rt.convertRemoteStatus('new').title 'New' >>> rt.convertRemoteStatus('open').title 'Confirmed' >>> rt.convertRemoteStatus('stalled').title 'Confirmed' >>> rt.convertRemoteStatus('rejected').title 'Invalid' >>> rt.convertRemoteStatus('resolved').title 'Fix Released' Passing a status which the RequestTracker instance can't understand will result in an UnknownRemoteStatusError being raised. >>> rt.convertRemoteStatus('spam').title Traceback (most recent call last): ... UnknownRemoteStatusError: spam == Importance Conversion == There is no obvious mapping from ticket priorities to importances. They are all imported as Unknown. No exception is raised, because they are all unknown. >>> rt.convertRemoteImportance('foo').title 'Unknown' == Initialisation == Calling initializeRemoteBugDB() on our RequestTracker instance and passing it a set of remote bug IDs will fetch those bug IDs from the server and file them in a local variable for later use. We use a test-oriented implementation of RequestTracker for the purposes of these tests, which allows us to not rely on a working network connection. >>> from lp.bugs.tests.externalbugtracker import ( ... TestRequestTracker) >>> rt = TestRequestTracker('http://example.com/') >>> rt.initializeRemoteBugDB([1585, 1586, 1587, 1588, 1589]) >>> sorted(rt.bugs.keys()) [1585, 1586, 1587, 1588, 1589] == Export Methods == There are two means by which we can export RT bug statuses: on a bug-by-bug basis and as a batch. When the number of bugs that need updating is less than a given bug RT instances's batch_query_threshold the bugs will be fetched one-at-a-time: >>> rt.batch_query_threshold 1 >>> rt.trace_calls = True >>> rt.initializeRemoteBugDB([1585]) CALLED urlopen('REST/1.0/ticket/1585/show') >>> rt.bugs.keys() [1585] If there are more than batch_query_threshold bugs to update then they are fetched as a batch: >>> rt.initializeRemoteBugDB([1585, 1586, 1587, 1588, 1589]) CALLED urlopen('REST/1.0/search/ticket/') >>> sorted(rt.bugs.keys()) [1585, 1586, 1587, 1588, 1589] If something goes wrong when we request a bug from the remote server a BugTrackerConnectError will be raised. We can demonstrate this by making our test RT instance simulate such a situation. >>> rt.simulate_bad_response = True >>> rt.initializeRemoteBugDB([1585]) Traceback (most recent call last): ... BugTrackerConnectError... This can also be demonstrated for importing bugs as a batch: >>> rt.initializeRemoteBugDB([1585, 1586, 1587, 1588, 1589]) Traceback (most recent call last): ... BugTrackerConnectError... >>> rt.simulate_bad_response = False == Updating Bug Watches == First, we create some bug watches to test with. Example.com hosts an RT instance which has several bugs that we wish to watch: >>> from lp.bugs.interfaces.bug import IBugSet >>> from lp.bugs.interfaces.bugwatch import IBugWatchSet >>> from lp.registry.interfaces.person import IPersonSet >>> from lp.bugs.tests.externalbugtracker import ( ... print_bugwatches) Launchpad.dev bug #10 is the same bug as reported in example.com bug #1585, so we add a watch against the remote bug. >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities >>> example_bug_tracker = new_bugtracker(BugTrackerType.RT) >>> example_bug = getUtility(IBugSet).get(10) >>> sample_person = getUtility(IPersonSet).getByEmail( ... 'test@canonical.com') >>> example_bugwatch = example_bug.addWatch( ... example_bug_tracker, '1585', ... getUtility(ILaunchpadCelebrities).janitor) >>> print_bugwatches(example_bug_tracker.watches) Remote bug 1585: None Our RequestTracker ExternalBugTracker can now process, and retrieve a remote status for, the bug watch that we have created. >>> transaction.commit() >>> from canonical.testing.layers import LaunchpadZopelessLayer >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster >>> txn = LaunchpadZopelessLayer.txn >>> bug_watch_updater = CheckwatchesMaster(txn) >>> rt = TestRequestTracker(example_bug_tracker.baseurl) >>> bug_watch_updater.updateBugWatches(rt, example_bug_tracker.watches) INFO:...:Updating 1 watches for 1 bugs on http://bugs.some.where >>> print_bugwatches(example_bug_tracker.watches) Remote bug 1585: new We now add some more watches against remote bugs in the example.com bug tracker with a variety of statuses. >>> print_bugwatches(example_bug_tracker.watches, ... rt.convertRemoteStatus) Remote bug 1585: New >>> remote_bugs = [ ... 1586, ... 1587, ... 1588, ... 1589, ... ] >>> bug_watch_set = getUtility(IBugWatchSet) >>> for remote_bug_id in remote_bugs: ... bug_watch = bug_watch_set.createBugWatch( ... bug=example_bug, owner=sample_person, ... bugtracker=example_bug_tracker, ... remotebug=str(remote_bug_id)) >>> rt.trace_calls = True >>> bug_watch_updater.updateBugWatches(rt, example_bug_tracker.watches) INFO:...:Updating 5 watches for 5 bugs on http://bugs.some.where CALLED urlopen(u'REST/1.0/search/ticket/') The bug statuses have now been imported from the Example.com bug tracker, so the bug watches should now have valid Launchpad bug statuses: >>> print_bugwatches(example_bug_tracker.watches, ... rt.convertRemoteStatus) Remote bug 1585: New Remote bug 1586: Confirmed Remote bug 1587: Confirmed Remote bug 1588: Fix Released Remote bug 1589: Invalid == Getting the remote product for a bug == It's possible to get the remote product for a remote RT bug using getRemoteProduct(). In the case of RT, what we refer to in Launchpad as a "remote product" is in fact the name of an RT ticket Queue. RT has no concept of products, only queues, so though there'e a terminology mismatch the meaning is essentially the same. >>> print rt.getRemoteProduct(1585) OpenSSL-Bugs If you try to get the remote product of a bug that doesn't exist you'll get a BugNotFound error. >>> print rt.getRemoteProduct('this-doesnt-exist') Traceback (most recent call last): ... BugNotFound: this-doesnt-exist If for some reason the RT instance doesn't return a Queue name for a bug, getRemoteProduct() will return None. >>> del rt.bugs[1589]['queue'] >>> print rt.getRemoteProduct(1589) None