~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
= ExternalBugTracker: Roundup =

This covers the implementation of the ExternalBugTracker class for Roundup
bugwatches.


== Basics ==

The ExternalBugTracker descendant class which implements methods for updating
bug watches on Roundup bug trackers is externalbugtracker.Roundup, which
implements IExternalBugTracker.

    >>> from lp.bugs.externalbugtracker import Roundup
    >>> 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,
    ...     Roundup('http://example.com'))
    True


== Status Conversion ==

The basic Roundup bug statuses (i.e. those available by default in new
Roundup instances) map to Launchpad bug statuses.

Roundup.convertRemoteStatus() handles the conversion.

    >>> roundup = Roundup('http://example.com/')
    >>> roundup.convertRemoteStatus('1').title
    'New'
    >>> roundup.convertRemoteStatus('2').title
    'Confirmed'
    >>> roundup.convertRemoteStatus('3').title
    'Incomplete'
    >>> roundup.convertRemoteStatus('4').title
    'Incomplete'
    >>> roundup.convertRemoteStatus('5').title
    'In Progress'
    >>> roundup.convertRemoteStatus('6').title
    'In Progress'
    >>> roundup.convertRemoteStatus('7').title
    'Fix Committed'
    >>> roundup.convertRemoteStatus('8').title
    'Fix Released'

Some Roundup trackers are set up to use multiple fields (columns in
Roundup terminology) to represent bug statuses. We store multiple
values by joining them with colons. The Roundup class knows how many
fields are expected for a particular remote host (for those that we
support), and will generate an error when we have more or less field
values compared to the expected number of fields.

    >>> roundup.convertRemoteStatus('1:2')
    Traceback (most recent call last):
      ...
    UnknownRemoteStatusError: 1 field(s) expected, got 2: 1:2

If the status isn't something that our Roundup ExternalBugTracker can
understand an UnknownRemoteStatusError will be raised.

    >>> roundup.convertRemoteStatus('eggs').title
    Traceback (most recent call last):
      ...
    UnknownRemoteStatusError: Unrecognized value for field 1 (status): eggs


== Initialization ==

Calling initializeRemoteBugDB() on our Roundup 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 for the purposes of these tests, which
overrides ExternalBugTracker.urlopen() so that we don't have to rely on a
working network connection.

    >>> from lp.bugs.tests.externalbugtracker import (
    ...     TestRoundup, print_bugwatches)
    >>> roundup = TestRoundup(u'http://test.roundup/')
    >>> roundup.initializeRemoteBugDB([1])
    >>> sorted(roundup.bugs.keys())
    [1]


== Export Methods ==

There are two means by which we can export Roundup 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 roundupker's batch_query_threshold the bugs will be
fetched one-at-a-time:

    >>> roundup.batch_query_threshold
    10

    >>> roundup.trace_calls = True
    >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
    CALLED urlopen(u'http://test.roundup/issue?...&id=6')
    CALLED urlopen(u'http://test.roundup/issue?...&id=7')
    CALLED urlopen(u'http://test.roundup/issue?...&id=8')
    CALLED urlopen(u'http://test.roundup/issue?...&id=9')
    CALLED urlopen(u'http://test.roundup/issue?...&id=10')

If there are more than batch_query_threshold bugs to update then they are
fetched as a batch:

    >>> roundup.batch_query_threshold = 4
    >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
    CALLED urlopen(u'http://test.roundup/issue?...@startwith=0')


== Updating Bug Watches ==

First, we create some bug watches to test with:

    >>> from lp.bugs.interfaces.bug import IBugSet
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')

    >>> example_bug_tracker = new_bugtracker(BugTrackerType.ROUNDUP)

    >>> from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
    >>> example_bug = getUtility(IBugSet).get(10)
    >>> example_bugwatch = example_bug.addWatch(
    ...     example_bug_tracker, '1',
    ...     getUtility(ILaunchpadCelebrities).janitor)


Collect the Example.com watches:

    >>> print_bugwatches(example_bug_tracker.watches)
    Remote bug 1: None

And have a Roundup instance process them:

    >>> transaction.commit()

    >>> from canonical.testing.layers import LaunchpadZopelessLayer
    >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
    >>> txn = LaunchpadZopelessLayer.txn
    >>> bug_watch_updater = CheckwatchesMaster(txn)
    >>> roundup = TestRoundup(example_bug_tracker.baseurl)
    >>> bug_watch_updater.updateBugWatches(
    ...     roundup, example_bug_tracker.watches)
    INFO:...:Updating 1 watches for 1 bugs on http://bugs.some.where
    >>> print_bugwatches(example_bug_tracker.watches)
    Remote bug 1: 1

We'll add some more watches now.

    >>> from lp.bugs.interfaces.bugwatch import IBugWatchSet
    >>> print_bugwatches(example_bug_tracker.watches,
    ...     roundup.convertRemoteStatus)
    Remote bug 1: New

    >>> remote_bugs = [
    ...     (2, 'Confirmed'),
    ...     (3, 'Incomplete'),
    ...     (4, 'Incomplete'),
    ...     (5, 'In Progress'),
    ...     (9, 'In Progress'),
    ...     (10, 'Fix Committed'),
    ...     (11, 'Fix Released'),
    ...     (12, 'Incomplete'),
    ...     (13, 'Incomplete'),
    ...     (14, 'In Progress')
    ... ]

    >>> bug_watch_set = getUtility(IBugWatchSet)
    >>> for remote_bug_id, remote_status in remote_bugs:
    ...     bug_watch = bug_watch_set.createBugWatch(
    ...         bug=example_bug, owner=sample_person,
    ...         bugtracker=example_bug_tracker,
    ...         remotebug=str(remote_bug_id))

    >>> roundup.trace_calls = True
    >>> bug_watch_updater.updateBugWatches(
    ...     roundup, example_bug_tracker.watches)
    INFO:...:Updating 11 watches for 11 bugs on http://bugs.some.where
    CALLED urlopen(u'http://.../issue?...@startwith=0')

    >>> print_bugwatches(example_bug_tracker.watches,
    ...     roundup.convertRemoteStatus)
    Remote bug 1: New
    Remote bug 2: Confirmed
    Remote bug 3: Incomplete
    Remote bug 4: Incomplete
    Remote bug 5: In Progress
    Remote bug 9: In Progress
    Remote bug 10: Fix Committed
    Remote bug 11: Fix Released
    Remote bug 12: Incomplete
    Remote bug 13: Incomplete
    Remote bug 14: In Progress