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:
|
|||
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!