~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
= 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.
    <urllib2.OpenerDirector...>


== 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