Merge lp:~abentley/bzr-pqm/fix-lp-land into lp:bzr-pqm

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
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.make_submission_email to simplify testing.

To post a comment you must log in.
lp:~abentley/bzr-pqm/fix-lp-land updated
89. By Aaron Bentley

Cleanup.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Not much to add, nice cleanup!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '__init__.py'
--- __init__.py 2011-12-19 11:04:03 +0000
+++ __init__.py 2012-02-02 17:34:20 +0000
@@ -150,11 +150,9 @@
150 def run(self, location=None, dry_run=False, testfix=False,150 def run(self, location=None, dry_run=False, testfix=False,
151 no_qa=False, incremental=False, rollback=None):151 no_qa=False, incremental=False, rollback=None):
152 from bzrlib.plugins.pqm.lpland import Submitter152 from bzrlib.plugins.pqm.lpland import Submitter
153 from bzrlib import branch as _mod_branch
154 from bzrlib.plugins.pqm.lpland import (153 from bzrlib.plugins.pqm.lpland import (
155 MissingReviewError, MissingBugsError, MissingBugsIncrementalError)154 MissingReviewError, MissingBugsError, MissingBugsIncrementalError)
156155
157 branch = _mod_branch.Branch.open_containing('.')[0]
158 if dry_run:156 if dry_run:
159 outf = self.outf157 outf = self.outf
160 else:158 else:
@@ -162,8 +160,9 @@
162 if rollback and (no_qa or incremental):160 if rollback and (no_qa or incremental):
163 print "--rollback option used. Ignoring --no-qa and --incremental."161 print "--rollback option used. Ignoring --no-qa and --incremental."
164 try:162 try:
165 submitter = Submitter(branch, location, testfix, no_qa,163 submitter = Submitter.from_cmdline(
166 incremental, rollback=rollback).run(outf)164 location, testfix, no_qa, incremental,
165 rollback=rollback).run(outf)
167 except MissingReviewError:166 except MissingReviewError:
168 raise BzrCommandError(167 raise BzrCommandError(
169 "Cannot land branches that haven't got approved code "168 "Cannot land branches that haven't got approved code "
170169
=== modified file 'lpland.py'
--- lpland.py 2011-12-16 20:21:06 +0000
+++ lpland.py 2012-02-02 17:34:20 +0000
@@ -198,39 +198,57 @@
198198
199199
200class Submitter(object):200class Submitter(object):
201 """Class that submits a to PQM from a merge proposal."""
201202
202 def __init__(self, branch, location, testfix=False, no_qa=False,203 def __init__(self, branch, mp, testfix=False, no_qa=False,
203 incremental=False, rollback=None):204 incremental=False, rollback=None):
204 self.branch = branch205 self.branch = branch
206 self.mp = mp
205 self.testfix = testfix207 self.testfix = testfix
206 self.no_qa = no_qa208 self.no_qa = no_qa
207 self.incremental = incremental209 self.incremental = incremental
208 self.rollback = rollback210 self.rollback = rollback
209 self.config = self.branch.get_config()211 self.config = pqm_submit.get_stacked_config(self.branch, None)
210 self.mail_from = self.config.get_user_option('pqm_user_email')212 self.mail_from = pqm_submit.get_mail_from(self.config)
211 if not self.mail_from:213
212 self.mail_from = self.config.username()214 @classmethod
213 self.lander = LaunchpadBranchLander.load()215 def from_cmdline(cls, location, testfix, no_qa, incremental, rollback):
214 self.location = location216 """Factory that returns a Submitter from commandline arguments."""
215217 from bzrlib import branch as _mod_branch
216 def submission(self, mp):218 branch = _mod_branch.Branch.open_containing('.')[0]
219 lander = LaunchpadBranchLander.load()
220 if location is None:
221 mp = lander.get_merge_proposal_from_branch(branch)
222 else:
223 mp = lander.load_merge_proposal(location)
224 return cls(branch, mp, testfix, no_qa, incremental, rollback)
225
226 def submission(self):
227 """Create a PQMSubmission for this Submitter."""
217 submission = pqm_submit.PQMSubmission(self.branch,228 submission = pqm_submit.PQMSubmission(self.branch,
218 mp.source_branch, mp.target_branch, '')229 self.mp.source_branch, self.mp.target_branch, '')
219 return submission230 return submission
220231
221 @staticmethod232 @staticmethod
222 def check_submission(submission):233 def check_submission(submission):
234 """Ensure the Submission is reasonable."""
223 submission.check_tree()235 submission.check_tree()
224 submission.check_public_branch()236 submission.check_public_branch()
225237
226 def set_message(self, submission, mp):238 def set_message(self, submission):
227 pqm_command = ''.join(submission.to_lines())239 """Set the message of the Submission.
228 commit_message = mp.commit_message or ''240
229 start_message = mp.get_commit_message(commit_message, self.testfix,241 This starts with automatic generation from merge proposal and branch
230 self.no_qa, self.incremental, rollback=self.rollback)242 properties, then allows the user to edit the values.
243 """
244 commit_message = self.mp.commit_message or ''
245 start_message = self.mp.get_commit_message(commit_message,
246 self.testfix, self.no_qa, self.incremental,
247 rollback=self.rollback)
231 prompt = ('Proposed commit message:\n%s\nEdit before sending'248 prompt = ('Proposed commit message:\n%s\nEdit before sending'
232 % start_message)249 % start_message)
233 if commit_message == '' or ui.ui_factory.get_boolean(prompt):250 if commit_message == '' or ui.ui_factory.get_boolean(prompt):
251 pqm_command = ''.join(submission.to_lines())
234 unicode_message = msgeditor.edit_commit_message(252 unicode_message = msgeditor.edit_commit_message(
235 'pqm command:\n%s' % pqm_command,253 'pqm command:\n%s' % pqm_command,
236 start_message=start_message).rstrip('\n')254 start_message=start_message).rstrip('\n')
@@ -239,18 +257,17 @@
239 message = start_message257 message = start_message
240 submission.message = message258 submission.message = message
241259
242 def run(self, outf):260 def make_submission_email(self, submission, sign=True):
243 if self.location is None:261 mail_to = pqm_submit.pqm_email(self.config, self.mp.target_branch)
244 mp = self.lander.get_merge_proposal_from_branch(self.branch)
245 else:
246 mp = self.lander.load_merge_proposal(self.location)
247 submission = self.submission(mp)
248 self.check_submission(submission)
249 self.set_message(submission, mp)
250 mail_to = pqm_submit.pqm_email(self.config, mp.target_branch)
251 if not mail_to:262 if not mail_to:
252 raise pqm_submit.NoPQMSubmissionAddress(self.branch)263 raise pqm_submit.NoPQMSubmissionAddress(self.branch)
253 email = submission.to_email(self.mail_from, mail_to)264 return submission.to_email(self.mail_from, mail_to, sign=sign)
265
266 def run(self, outf, sign=True):
267 submission = self.submission()
268 self.check_submission(submission)
269 self.set_message(submission)
270 email = self.make_submission_email(submission, sign=sign)
254 if outf is not None:271 if outf is not None:
255 outf.write(email.as_string())272 outf.write(email.as_string())
256 else:273 else:
257274
=== modified file 'pqm_submit.py'
--- pqm_submit.py 2012-01-20 15:32:43 +0000
+++ pqm_submit.py 2012-02-02 17:34:20 +0000
@@ -219,7 +219,16 @@
219 return value219 return value
220 return None220 return None
221221
222 get = _get_user_option222 def get(self, option_name):
223 """Return an option exactly as bzrlib.config.Stack would.
224
225 Since Stack allows the environment to override 'email', this uses the
226 same logic.
227 """
228 if option_name == 'email':
229 return self.username()
230 else:
231 return self._get_user_option(option_name)
223232
224 def _get_user_id(self):233 def _get_user_id(self):
225 for source in self._sources:234 for source in self._sources:
@@ -245,9 +254,20 @@
245 return mail_to.encode('utf8') # same here254 return mail_to.encode('utf8') # same here
246255
247256
248def submit(branch, message, dry_run=False, public_location=None,257def get_stacked_config(branch=None, public_location=None):
249 submit_location=None, tree=None, ignore_local=False):258 """Return the relevant stacked config.
250 """Submit the given branch to the pqm."""259
260 If the branch is supplied, a branch stacked config is returned.
261 Otherwise, if the public location is supplied, a stacked location config
262 is returned.
263 Otherwise, a global config is returned.
264
265 For bzr versions earlier than 2.5, pqm_submit.StackedConfig is used. For
266 later versions, the standard stacked config is used.
267
268 :param branch: The branch to retrieve the config for.
269 :param public_location: The public location to retrieve the config for.
270 """
251 if bzrlib_version < (2, 5):271 if bzrlib_version < (2, 5):
252 config = StackedConfig()272 config = StackedConfig()
253 if branch:273 if branch:
@@ -263,16 +283,31 @@
263 config = _mod_config.LocationStack(public_location)283 config = _mod_config.LocationStack(public_location)
264 else:284 else:
265 config = _mod_config.GlobalStack()285 config = _mod_config.GlobalStack()
266286 return config
287
288
289def get_mail_from(config):
290 """Return the email id that the email is from.
291
292 :param config: The config to use for determining the from address.
293 """
294 mail_from = config.get('pqm_user_email')
295 if not mail_from:
296 mail_from = config.get('email')
297 mail_from = mail_from.encode('utf8') # Make sure this isn't unicode
298 return mail_from
299
300
301def submit(branch, message, dry_run=False, public_location=None,
302 submit_location=None, tree=None, ignore_local=False):
303 """Submit the given branch to the pqm."""
304 config = get_stacked_config(branch, public_location)
267 submission = PQMSubmission(305 submission = PQMSubmission(
268 source_branch=branch, public_location=public_location, message=message,306 source_branch=branch, public_location=public_location, message=message,
269 submit_location=submit_location,307 submit_location=submit_location,
270 tree=tree)308 tree=tree)
271309
272 mail_from = config.get('pqm_user_email')310 mail_from = get_mail_from(config)
273 if not mail_from:
274 mail_from = config.get('email')
275 mail_from = mail_from.encode('utf8') # Make sure this isn't unicode
276 mail_to = pqm_email(config, submit_location)311 mail_to = pqm_email(config, submit_location)
277 if not mail_to:312 if not mail_to:
278 raise NoPQMSubmissionAddress(branch)313 raise NoPQMSubmissionAddress(branch)
279314
=== modified file 'tests/test_lpland.py'
--- tests/test_lpland.py 2011-02-16 00:01:50 +0000
+++ tests/test_lpland.py 2012-02-02 17:34:20 +0000
@@ -5,17 +5,27 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from cStringIO import StringIO
9from textwrap import dedent
8import unittest10import unittest
911
10from launchpadlib.uris import (12from bzrlib import (
11 DEV_SERVICE_ROOT, EDGE_SERVICE_ROOT, LPNET_SERVICE_ROOT,13 errors,
12 STAGING_SERVICE_ROOT)14 ui,
1315)
14from bzrlib.plugins.pqm.lpland import (16from bzrlib.plugins.pqm.lpland import (
15 get_bugs_clause, get_reviewer_clause,17 get_bugs_clause,
16 get_reviewer_handle, get_testfix_clause, get_qa_clause,18 get_reviewer_clause,
17 MissingReviewError, MissingBugsError, MissingBugsIncrementalError,19 get_reviewer_handle,
18 MergeProposal)20 get_testfix_clause,
21 get_qa_clause,
22 MissingReviewError,
23 MissingBugsError,
24 MissingBugsIncrementalError,
25 MergeProposal,
26 Submitter,
27)
28from bzrlib.tests import TestCaseWithTransport
1929
20from fakemethod import FakeMethod30from fakemethod import FakeMethod
2131
@@ -36,7 +46,7 @@
36 Only used for the purposes of testing.46 Only used for the purposes of testing.
37 """47 """
3848
39 def __init__(self, name, irc_handles):49 def __init__(self, name='jrandom', irc_handles=()):
40 self.name = name50 self.name = name
41 self.irc_nicknames = list(irc_handles)51 self.irc_nicknames = list(irc_handles)
4252
@@ -52,6 +62,30 @@
52 self.network = network62 self.network = network
5363
5464
65class FakeBranch:
66
67 def __init__(self, location):
68 self.location = location
69 self.linked_bugs = [FakeBug(5)]
70
71 def composePublicURL(self, scheme):
72 return self.location
73
74
75class FakeComment:
76
77 def __init__(self):
78 self.vote = 'Approve'
79
80
81class FakeVote:
82
83 def __init__(self):
84 self.comment = FakeComment()
85 self.review_type = None
86 self.reviewer = FakePerson()
87
88
55class FakeLPMergeProposal:89class FakeLPMergeProposal:
56 """Fake launchpadlib MergeProposal object.90 """Fake launchpadlib MergeProposal object.
5791
@@ -60,6 +94,10 @@
6094
61 def __init__(self, root=None):95 def __init__(self, root=None):
62 self._root = root96 self._root = root
97 self.source_branch = FakeBranch('lp_source')
98 self.target_branch = FakeBranch('lp_target')
99 self.commit_message = 'Message1'
100 self.votes = [FakeVote()]
63101
64102
65class TestBugsClaused(unittest.TestCase):103class TestBugsClaused(unittest.TestCase):
@@ -361,3 +399,75 @@
361 # If the merge proposal hasn't been approved by anyone, we cannot399 # If the merge proposal hasn't been approved by anyone, we cannot
362 # generate a valid clause.400 # generate a valid clause.
363 self.assertRaises(MissingReviewError, self.get_reviewer_clause, {})401 self.assertRaises(MissingReviewError, self.get_reviewer_clause, {})
402
403
404class TestSubmitter(TestCaseWithTransport):
405
406 def make_submitter(self):
407 """Return a Submitter for testing."""
408 b = self.make_branch('source')
409 b.get_config().set_user_option('pqm_email', 'PQM <pqm@example.com>')
410 lp_source = self.make_branch('lp_source')
411 mp = MergeProposal(FakeLPMergeProposal())
412 return Submitter(b, mp)
413
414 def test_submission(self):
415 """Creating a submission produces the expected result."""
416 submitter = self.make_submitter()
417 submission = submitter.submission()
418 self.assertEqual(submitter.branch, submission.source_branch)
419 self.assertEqual('lp_source', submission.public_location)
420 self.assertEqual('lp_target', submission.submit_location)
421 self.assertEqual('', submission.message)
422
423 def test_check_submission(self):
424 """Checking the submission returns in the comment case."""
425 submitter = self.make_submitter()
426 submitter.check_submission(submitter.submission())
427
428 def test_check_submission_public_branch(self):
429 """Checking the submission raises if the public branch is outdated."""
430 submitter = self.make_submitter()
431 tree = submitter.branch.create_checkout('tree', lightweight=True)
432 tree.commit('message')
433 self.assertRaises(errors.PublicBranchOutOfDate,
434 submitter.check_submission, submitter.submission())
435
436 def test_set_message(self):
437 """Setting the message produces one in the correct format."""
438 submitter = self.make_submitter()
439 submission = submitter.submission()
440 ui.ui_factory = ui.CannedInputUIFactory([False])
441 submitter.set_message(submission)
442 self.assertEqual('[r=jrandom][bug=5] Message1', submission.message)
443
444 def test_make_submission_email(self):
445 """Creating a submission email works."""
446 submitter = self.make_submitter()
447 submission = submitter.submission()
448 submission.message = 'Hello!'
449 target = self.make_branch('lp_target')
450 email = submitter.make_submission_email(submission, sign=False)
451 self.assertEqual('PQM <pqm@example.com>', email['To'])
452 self.assertEqual('jrandom@example.com', email['From'])
453 self.assertEqual('Hello!', email['Subject'])
454 self.assertContainsRe(email['User-Agent'], '^Bazaar')
455 self.assertEqual('star-merge lp_source lp_target\n', email._body)
456
457 def test_run(self):
458 """End-to-end test of Submitter.run."""
459 submitter = self.make_submitter()
460 ui.ui_factory = ui.CannedInputUIFactory([False])
461 outf = StringIO()
462 submitter.run(outf, sign=False)
463 self.assertContainsRe(outf.getvalue(), dedent("""\
464 MIME-Version: 1.0
465 Content-Type: text/plain; charset="us-ascii"
466 Content-Transfer-Encoding: 7bit
467 From: jrandom@example.com
468 Subject: \[r=jrandom\]\[bug=5\] Message1
469 To: PQM <pqm@example.com>
470 User-Agent: Bazaar (.*)
471
472 star-merge lp_source lp_target
473 """))
364474
=== modified file 'tests/test_pqm_submit.py'
--- tests/test_pqm_submit.py 2010-08-06 16:52:30 +0000
+++ tests/test_pqm_submit.py 2012-02-02 17:34:20 +0000
@@ -471,3 +471,11 @@
471 call[1:3])471 call[1:3])
472 self.assertContainsRe(call[3], EMAIL)472 self.assertContainsRe(call[3], EMAIL)
473473
474
475class TestConfig(TestCaseWithMemoryTransport):
476
477 def test_email_from_environ(self):
478 """The config can get the email from the environ."""
479 branch = self.make_branch('foo')
480 config = pqm_submit.get_stacked_config(branch, None)
481 self.assertEqual('jrandom@example.com', config.get('email'))

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: