Merge lp:~abentley/bzr-pqm/fix-lp-land into lp:bzr-pqm
- fix-lp-land
- Merge into devel
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Merged at revision: | 85 |
Proposed branch: | lp:~abentley/bzr-pqm/fix-lp-land |
Merge into: | lp:bzr-pqm |
Diff against target: |
398 lines (+216/-47) 5 files modified
__init__.py (+3/-4) lpland.py (+42/-25) pqm_submit.py (+44/-9) tests/test_lpland.py (+119/-9) tests/test_pqm_submit.py (+8/-0) |
To merge this branch: | bzr merge lp:~abentley/bzr-pqm/fix-lp-land |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij (community) | Approve | ||
Review via email: mp+91318@code.launchpad.net |
Commit message
Fix lp-land.
Description of the change
Recent changes to config handling broke lp-land. This breakage was not detected because lp-land was not sufficiently tested.
This branch fixes the breakage by using an implementation of config.Stack as the input to pqm_email.
It increases the test coverage of lp-land to reduce the chance of similar problems in the future.
It changes the behaviour of ConfigStack.get so that it supports environment variables such as BZR_EMAIL and EMAIL.
It extracts get_stacked_config and get_mail_from to increase code re-use.
It moves most functionality out of Submitter.__init__ into the new from_cmdline factory method, to make testing easier.
It extracts Submitter.
To post a comment you must log in.
lp:~abentley/bzr-pqm/fix-lp-land
updated
- 89. By Aaron Bentley
-
Cleanup.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '__init__.py' |
2 | --- __init__.py 2011-12-19 11:04:03 +0000 |
3 | +++ __init__.py 2012-02-02 17:34:20 +0000 |
4 | @@ -150,11 +150,9 @@ |
5 | def run(self, location=None, dry_run=False, testfix=False, |
6 | no_qa=False, incremental=False, rollback=None): |
7 | from bzrlib.plugins.pqm.lpland import Submitter |
8 | - from bzrlib import branch as _mod_branch |
9 | from bzrlib.plugins.pqm.lpland import ( |
10 | MissingReviewError, MissingBugsError, MissingBugsIncrementalError) |
11 | |
12 | - branch = _mod_branch.Branch.open_containing('.')[0] |
13 | if dry_run: |
14 | outf = self.outf |
15 | else: |
16 | @@ -162,8 +160,9 @@ |
17 | if rollback and (no_qa or incremental): |
18 | print "--rollback option used. Ignoring --no-qa and --incremental." |
19 | try: |
20 | - submitter = Submitter(branch, location, testfix, no_qa, |
21 | - incremental, rollback=rollback).run(outf) |
22 | + submitter = Submitter.from_cmdline( |
23 | + location, testfix, no_qa, incremental, |
24 | + rollback=rollback).run(outf) |
25 | except MissingReviewError: |
26 | raise BzrCommandError( |
27 | "Cannot land branches that haven't got approved code " |
28 | |
29 | === modified file 'lpland.py' |
30 | --- lpland.py 2011-12-16 20:21:06 +0000 |
31 | +++ lpland.py 2012-02-02 17:34:20 +0000 |
32 | @@ -198,39 +198,57 @@ |
33 | |
34 | |
35 | class Submitter(object): |
36 | + """Class that submits a to PQM from a merge proposal.""" |
37 | |
38 | - def __init__(self, branch, location, testfix=False, no_qa=False, |
39 | + def __init__(self, branch, mp, testfix=False, no_qa=False, |
40 | incremental=False, rollback=None): |
41 | self.branch = branch |
42 | + self.mp = mp |
43 | self.testfix = testfix |
44 | self.no_qa = no_qa |
45 | self.incremental = incremental |
46 | self.rollback = rollback |
47 | - self.config = self.branch.get_config() |
48 | - self.mail_from = self.config.get_user_option('pqm_user_email') |
49 | - if not self.mail_from: |
50 | - self.mail_from = self.config.username() |
51 | - self.lander = LaunchpadBranchLander.load() |
52 | - self.location = location |
53 | - |
54 | - def submission(self, mp): |
55 | + self.config = pqm_submit.get_stacked_config(self.branch, None) |
56 | + self.mail_from = pqm_submit.get_mail_from(self.config) |
57 | + |
58 | + @classmethod |
59 | + def from_cmdline(cls, location, testfix, no_qa, incremental, rollback): |
60 | + """Factory that returns a Submitter from commandline arguments.""" |
61 | + from bzrlib import branch as _mod_branch |
62 | + branch = _mod_branch.Branch.open_containing('.')[0] |
63 | + lander = LaunchpadBranchLander.load() |
64 | + if location is None: |
65 | + mp = lander.get_merge_proposal_from_branch(branch) |
66 | + else: |
67 | + mp = lander.load_merge_proposal(location) |
68 | + return cls(branch, mp, testfix, no_qa, incremental, rollback) |
69 | + |
70 | + def submission(self): |
71 | + """Create a PQMSubmission for this Submitter.""" |
72 | submission = pqm_submit.PQMSubmission(self.branch, |
73 | - mp.source_branch, mp.target_branch, '') |
74 | + self.mp.source_branch, self.mp.target_branch, '') |
75 | return submission |
76 | |
77 | @staticmethod |
78 | def check_submission(submission): |
79 | + """Ensure the Submission is reasonable.""" |
80 | submission.check_tree() |
81 | submission.check_public_branch() |
82 | |
83 | - def set_message(self, submission, mp): |
84 | - pqm_command = ''.join(submission.to_lines()) |
85 | - commit_message = mp.commit_message or '' |
86 | - start_message = mp.get_commit_message(commit_message, self.testfix, |
87 | - self.no_qa, self.incremental, rollback=self.rollback) |
88 | + def set_message(self, submission): |
89 | + """Set the message of the Submission. |
90 | + |
91 | + This starts with automatic generation from merge proposal and branch |
92 | + properties, then allows the user to edit the values. |
93 | + """ |
94 | + commit_message = self.mp.commit_message or '' |
95 | + start_message = self.mp.get_commit_message(commit_message, |
96 | + self.testfix, self.no_qa, self.incremental, |
97 | + rollback=self.rollback) |
98 | prompt = ('Proposed commit message:\n%s\nEdit before sending' |
99 | % start_message) |
100 | if commit_message == '' or ui.ui_factory.get_boolean(prompt): |
101 | + pqm_command = ''.join(submission.to_lines()) |
102 | unicode_message = msgeditor.edit_commit_message( |
103 | 'pqm command:\n%s' % pqm_command, |
104 | start_message=start_message).rstrip('\n') |
105 | @@ -239,18 +257,17 @@ |
106 | message = start_message |
107 | submission.message = message |
108 | |
109 | - def run(self, outf): |
110 | - if self.location is None: |
111 | - mp = self.lander.get_merge_proposal_from_branch(self.branch) |
112 | - else: |
113 | - mp = self.lander.load_merge_proposal(self.location) |
114 | - submission = self.submission(mp) |
115 | - self.check_submission(submission) |
116 | - self.set_message(submission, mp) |
117 | - mail_to = pqm_submit.pqm_email(self.config, mp.target_branch) |
118 | + def make_submission_email(self, submission, sign=True): |
119 | + mail_to = pqm_submit.pqm_email(self.config, self.mp.target_branch) |
120 | if not mail_to: |
121 | raise pqm_submit.NoPQMSubmissionAddress(self.branch) |
122 | - email = submission.to_email(self.mail_from, mail_to) |
123 | + return submission.to_email(self.mail_from, mail_to, sign=sign) |
124 | + |
125 | + def run(self, outf, sign=True): |
126 | + submission = self.submission() |
127 | + self.check_submission(submission) |
128 | + self.set_message(submission) |
129 | + email = self.make_submission_email(submission, sign=sign) |
130 | if outf is not None: |
131 | outf.write(email.as_string()) |
132 | else: |
133 | |
134 | === modified file 'pqm_submit.py' |
135 | --- pqm_submit.py 2012-01-20 15:32:43 +0000 |
136 | +++ pqm_submit.py 2012-02-02 17:34:20 +0000 |
137 | @@ -219,7 +219,16 @@ |
138 | return value |
139 | return None |
140 | |
141 | - get = _get_user_option |
142 | + def get(self, option_name): |
143 | + """Return an option exactly as bzrlib.config.Stack would. |
144 | + |
145 | + Since Stack allows the environment to override 'email', this uses the |
146 | + same logic. |
147 | + """ |
148 | + if option_name == 'email': |
149 | + return self.username() |
150 | + else: |
151 | + return self._get_user_option(option_name) |
152 | |
153 | def _get_user_id(self): |
154 | for source in self._sources: |
155 | @@ -245,9 +254,20 @@ |
156 | return mail_to.encode('utf8') # same here |
157 | |
158 | |
159 | -def submit(branch, message, dry_run=False, public_location=None, |
160 | - submit_location=None, tree=None, ignore_local=False): |
161 | - """Submit the given branch to the pqm.""" |
162 | +def get_stacked_config(branch=None, public_location=None): |
163 | + """Return the relevant stacked config. |
164 | + |
165 | + If the branch is supplied, a branch stacked config is returned. |
166 | + Otherwise, if the public location is supplied, a stacked location config |
167 | + is returned. |
168 | + Otherwise, a global config is returned. |
169 | + |
170 | + For bzr versions earlier than 2.5, pqm_submit.StackedConfig is used. For |
171 | + later versions, the standard stacked config is used. |
172 | + |
173 | + :param branch: The branch to retrieve the config for. |
174 | + :param public_location: The public location to retrieve the config for. |
175 | + """ |
176 | if bzrlib_version < (2, 5): |
177 | config = StackedConfig() |
178 | if branch: |
179 | @@ -263,16 +283,31 @@ |
180 | config = _mod_config.LocationStack(public_location) |
181 | else: |
182 | config = _mod_config.GlobalStack() |
183 | - |
184 | + return config |
185 | + |
186 | + |
187 | +def get_mail_from(config): |
188 | + """Return the email id that the email is from. |
189 | + |
190 | + :param config: The config to use for determining the from address. |
191 | + """ |
192 | + mail_from = config.get('pqm_user_email') |
193 | + if not mail_from: |
194 | + mail_from = config.get('email') |
195 | + mail_from = mail_from.encode('utf8') # Make sure this isn't unicode |
196 | + return mail_from |
197 | + |
198 | + |
199 | +def submit(branch, message, dry_run=False, public_location=None, |
200 | + submit_location=None, tree=None, ignore_local=False): |
201 | + """Submit the given branch to the pqm.""" |
202 | + config = get_stacked_config(branch, public_location) |
203 | submission = PQMSubmission( |
204 | source_branch=branch, public_location=public_location, message=message, |
205 | submit_location=submit_location, |
206 | tree=tree) |
207 | |
208 | - mail_from = config.get('pqm_user_email') |
209 | - if not mail_from: |
210 | - mail_from = config.get('email') |
211 | - mail_from = mail_from.encode('utf8') # Make sure this isn't unicode |
212 | + mail_from = get_mail_from(config) |
213 | mail_to = pqm_email(config, submit_location) |
214 | if not mail_to: |
215 | raise NoPQMSubmissionAddress(branch) |
216 | |
217 | === modified file 'tests/test_lpland.py' |
218 | --- tests/test_lpland.py 2011-02-16 00:01:50 +0000 |
219 | +++ tests/test_lpland.py 2012-02-02 17:34:20 +0000 |
220 | @@ -5,17 +5,27 @@ |
221 | |
222 | __metaclass__ = type |
223 | |
224 | +from cStringIO import StringIO |
225 | +from textwrap import dedent |
226 | import unittest |
227 | |
228 | -from launchpadlib.uris import ( |
229 | - DEV_SERVICE_ROOT, EDGE_SERVICE_ROOT, LPNET_SERVICE_ROOT, |
230 | - STAGING_SERVICE_ROOT) |
231 | - |
232 | +from bzrlib import ( |
233 | + errors, |
234 | + ui, |
235 | +) |
236 | from bzrlib.plugins.pqm.lpland import ( |
237 | - get_bugs_clause, get_reviewer_clause, |
238 | - get_reviewer_handle, get_testfix_clause, get_qa_clause, |
239 | - MissingReviewError, MissingBugsError, MissingBugsIncrementalError, |
240 | - MergeProposal) |
241 | + get_bugs_clause, |
242 | + get_reviewer_clause, |
243 | + get_reviewer_handle, |
244 | + get_testfix_clause, |
245 | + get_qa_clause, |
246 | + MissingReviewError, |
247 | + MissingBugsError, |
248 | + MissingBugsIncrementalError, |
249 | + MergeProposal, |
250 | + Submitter, |
251 | +) |
252 | +from bzrlib.tests import TestCaseWithTransport |
253 | |
254 | from fakemethod import FakeMethod |
255 | |
256 | @@ -36,7 +46,7 @@ |
257 | Only used for the purposes of testing. |
258 | """ |
259 | |
260 | - def __init__(self, name, irc_handles): |
261 | + def __init__(self, name='jrandom', irc_handles=()): |
262 | self.name = name |
263 | self.irc_nicknames = list(irc_handles) |
264 | |
265 | @@ -52,6 +62,30 @@ |
266 | self.network = network |
267 | |
268 | |
269 | +class FakeBranch: |
270 | + |
271 | + def __init__(self, location): |
272 | + self.location = location |
273 | + self.linked_bugs = [FakeBug(5)] |
274 | + |
275 | + def composePublicURL(self, scheme): |
276 | + return self.location |
277 | + |
278 | + |
279 | +class FakeComment: |
280 | + |
281 | + def __init__(self): |
282 | + self.vote = 'Approve' |
283 | + |
284 | + |
285 | +class FakeVote: |
286 | + |
287 | + def __init__(self): |
288 | + self.comment = FakeComment() |
289 | + self.review_type = None |
290 | + self.reviewer = FakePerson() |
291 | + |
292 | + |
293 | class FakeLPMergeProposal: |
294 | """Fake launchpadlib MergeProposal object. |
295 | |
296 | @@ -60,6 +94,10 @@ |
297 | |
298 | def __init__(self, root=None): |
299 | self._root = root |
300 | + self.source_branch = FakeBranch('lp_source') |
301 | + self.target_branch = FakeBranch('lp_target') |
302 | + self.commit_message = 'Message1' |
303 | + self.votes = [FakeVote()] |
304 | |
305 | |
306 | class TestBugsClaused(unittest.TestCase): |
307 | @@ -361,3 +399,75 @@ |
308 | # If the merge proposal hasn't been approved by anyone, we cannot |
309 | # generate a valid clause. |
310 | self.assertRaises(MissingReviewError, self.get_reviewer_clause, {}) |
311 | + |
312 | + |
313 | +class TestSubmitter(TestCaseWithTransport): |
314 | + |
315 | + def make_submitter(self): |
316 | + """Return a Submitter for testing.""" |
317 | + b = self.make_branch('source') |
318 | + b.get_config().set_user_option('pqm_email', 'PQM <pqm@example.com>') |
319 | + lp_source = self.make_branch('lp_source') |
320 | + mp = MergeProposal(FakeLPMergeProposal()) |
321 | + return Submitter(b, mp) |
322 | + |
323 | + def test_submission(self): |
324 | + """Creating a submission produces the expected result.""" |
325 | + submitter = self.make_submitter() |
326 | + submission = submitter.submission() |
327 | + self.assertEqual(submitter.branch, submission.source_branch) |
328 | + self.assertEqual('lp_source', submission.public_location) |
329 | + self.assertEqual('lp_target', submission.submit_location) |
330 | + self.assertEqual('', submission.message) |
331 | + |
332 | + def test_check_submission(self): |
333 | + """Checking the submission returns in the comment case.""" |
334 | + submitter = self.make_submitter() |
335 | + submitter.check_submission(submitter.submission()) |
336 | + |
337 | + def test_check_submission_public_branch(self): |
338 | + """Checking the submission raises if the public branch is outdated.""" |
339 | + submitter = self.make_submitter() |
340 | + tree = submitter.branch.create_checkout('tree', lightweight=True) |
341 | + tree.commit('message') |
342 | + self.assertRaises(errors.PublicBranchOutOfDate, |
343 | + submitter.check_submission, submitter.submission()) |
344 | + |
345 | + def test_set_message(self): |
346 | + """Setting the message produces one in the correct format.""" |
347 | + submitter = self.make_submitter() |
348 | + submission = submitter.submission() |
349 | + ui.ui_factory = ui.CannedInputUIFactory([False]) |
350 | + submitter.set_message(submission) |
351 | + self.assertEqual('[r=jrandom][bug=5] Message1', submission.message) |
352 | + |
353 | + def test_make_submission_email(self): |
354 | + """Creating a submission email works.""" |
355 | + submitter = self.make_submitter() |
356 | + submission = submitter.submission() |
357 | + submission.message = 'Hello!' |
358 | + target = self.make_branch('lp_target') |
359 | + email = submitter.make_submission_email(submission, sign=False) |
360 | + self.assertEqual('PQM <pqm@example.com>', email['To']) |
361 | + self.assertEqual('jrandom@example.com', email['From']) |
362 | + self.assertEqual('Hello!', email['Subject']) |
363 | + self.assertContainsRe(email['User-Agent'], '^Bazaar') |
364 | + self.assertEqual('star-merge lp_source lp_target\n', email._body) |
365 | + |
366 | + def test_run(self): |
367 | + """End-to-end test of Submitter.run.""" |
368 | + submitter = self.make_submitter() |
369 | + ui.ui_factory = ui.CannedInputUIFactory([False]) |
370 | + outf = StringIO() |
371 | + submitter.run(outf, sign=False) |
372 | + self.assertContainsRe(outf.getvalue(), dedent("""\ |
373 | + MIME-Version: 1.0 |
374 | + Content-Type: text/plain; charset="us-ascii" |
375 | + Content-Transfer-Encoding: 7bit |
376 | + From: jrandom@example.com |
377 | + Subject: \[r=jrandom\]\[bug=5\] Message1 |
378 | + To: PQM <pqm@example.com> |
379 | + User-Agent: Bazaar (.*) |
380 | + |
381 | + star-merge lp_source lp_target |
382 | + """)) |
383 | |
384 | === modified file 'tests/test_pqm_submit.py' |
385 | --- tests/test_pqm_submit.py 2010-08-06 16:52:30 +0000 |
386 | +++ tests/test_pqm_submit.py 2012-02-02 17:34:20 +0000 |
387 | @@ -471,3 +471,11 @@ |
388 | call[1:3]) |
389 | self.assertContainsRe(call[3], EMAIL) |
390 | |
391 | + |
392 | +class TestConfig(TestCaseWithMemoryTransport): |
393 | + |
394 | + def test_email_from_environ(self): |
395 | + """The config can get the email from the environ.""" |
396 | + branch = self.make_branch('foo') |
397 | + config = pqm_submit.get_stacked_config(branch, None) |
398 | + self.assertEqual('jrandom@example.com', config.get('email')) |
Not much to add, nice cleanup!