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
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'))

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: