Merge lp:~ursinha/bzr-pqm/add-noqa-incr-lpland into lp:bzr-pqm

Proposed by Ursula Junque
Status: Merged
Approved by: John A Meinel
Approved revision: 81
Merged at revision: 71
Proposed branch: lp:~ursinha/bzr-pqm/add-noqa-incr-lpland
Merge into: lp:bzr-pqm
Diff against target: 448 lines (+284/-22)
4 files modified
__init__.py (+44/-9)
lpland.py (+61/-12)
tests/fakemethod.py (+21/-0)
tests/test_lpland.py (+158/-1)
To merge this branch: bzr merge lp:~ursinha/bzr-pqm/add-noqa-incr-lpland
Reviewer Review Type Date Requested Status
John A Meinel Approve
Review via email: mp+31703@code.launchpad.net

Description of the change

= Summary =

This branch is a fix for bug 602137, adding to bzr lp-land three options: --testfix, --no-qa and --incr. --no-qa option should add the [no-qa] tag to the commit message, --incremental option should add the [incr] tag, and --testfix, [testfix]. This enforces the QA policy of now allowing fixes to be committed without bugs unless explicitely pointed.

== Proposed fix ==

The implementation is pretty simple. A method checks if the --no-qa, --incremental or --testfix options are given to bzr lp-land, and combined with the bugs found in the mp decides if the merge can continue based on the QA policy:

 * If the mp has no linked bugs and no no-qa tag, ec2 land should fail because without bugs and no-qa tag that commit would be an orphaned one.
 * If the mp has bugs and the no-qa commit tag, that's ok. The bug #s and the no-qa tag will be added to the commit message. This will be properly parsed and handled by qa-tagger script after the branch landed.
 * If the mp has no linked bugs but has the no-qa commit tag, that's ok. The commit will be qa-untestable, therefore not orphaned.
 * If the mp has no linked bugs but has the incr commit tag, ec2 land should fail, because bugs are needed when the fix is incremental.
 * A commit cannot be no-qa and incr at the same time.

== Tests ==

I've tested the newly created methods by adding more tests to test_lpland.py. I'm also testing get_commit_message using a very basic implementation of a fakemethod, that mocks launchpadlib's responses.

bzr selftest pqm

== Demo and Q/A ==

I've tested by running the command in a real merge proposal in Launchpad, using the --dry-run option, to check if the commit message is correctly parsed and tagged by bzr lp-land. I landed my branch that does similar changes to ec2 land (https://code.edge.launchpad.net/~ursinha/launchpad/add-ec2land-rules-orphaned-branches-no-conflicts/+merge/31386) using it.

devel/add-ec2land-rules-orphaned-branches-no-conflicts $ bzr lp-land

Results should be as following:
* Both --incremental and --no-qa at the same time: should error.
* without --no-qa and nor linked bugs: should error.
* with --incremental and no linked bugs: should error.
* with --incremental and linked bugs: should work.
* with --no-qa and no linked bugs: should work.
* with --no-qa and linked bugs: should work.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Ursula Junque wrote:
> Ursula Junque has proposed merging lp:~ursinha/bzr-pqm/add-noqa-incr-lpland into lp:bzr-pqm.
>
> Requested reviews:
> Bzr-pqm-devel (bzr-pqm-devel)
> Related bugs:
> #602137 add bzr lp-land support to no-qa and incr QA tags
> https://bugs.launchpad.net/bugs/602137
>
>
> = Summary =
>
> This branch is a fix for bug 602137, adding to bzr lp-land three options: --testfix, --no-qa and --incr. --no-qa option should add the [no-qa] tag to the commit message, --incremental option should add the [incr] tag, and --testfix, [testfix]. This enforces the QA policy of now allowing fixes to be committed without bugs unless explicitely pointed.
>
> == Proposed fix ==
>
> The implementation is pretty simple. A method checks if the --no-qa, --incremental or --testfix options are given to bzr lp-land, and combined with the bugs found in the mp decides if the merge can continue based on the QA policy:
>
> * If the mp has no linked bugs and no no-qa tag, ec2 land should fail because without bugs and no-qa tag that commit would be an orphaned one.
> * If the mp has bugs and the no-qa commit tag, that's ok. The bug #s and the no-qa tag will be added to the commit message. This will be properly parsed and handled by qa-tagger script after the branch landed.
> * If the mp has no linked bugs but has the no-qa commit tag, that's ok. The commit will be qa-untestable, therefore not orphaned.
> * If the mp has no linked bugs but has the incr commit tag, ec2 land should fail, because bugs are needed when the fix is incremental.
> * A commit cannot be no-qa and incr at the same time.

As far as reviewing goes, I'm going to trust that you know the Launchpad
requirements more than I do. As such, I'm not really auditing this part.
I'll just look at the code itself.

Personally, I really don't like this notation:
+__metaclass__ = type

Certainly in bzrlib itself we always do:

class Foo(object):

instead.

I'm certainly inclined to be less strict in bzr-pqm, esp. as I think the
Launchpad standard way of doing it is with __metaclass__. (Also, I see
that test_lpland was using __metaclass__ as well.)

I'm going to just land this, but mostly I'm trusting that you've thought
the logic through. Certainly everything I've seen seems well thought out.

John
=:->
 merge: approve

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkxcPVQACgkQJdeBCYSNAAMDNQCcC6688GqhnisqHAjqVti2q3T/
L6kAnivByx3OiMqv8oDA4sV4ZTgosN0Z
=zgkD
-----END PGP SIGNATURE-----

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 2010-04-14 10:48:03 +0000
3+++ __init__.py 2010-08-03 22:01:20 +0000
4@@ -18,6 +18,7 @@
5
6 from bzrlib.commands import Command, register_command
7 from bzrlib.option import Option
8+from bzrlib.errors import BzrCommandError
9
10
11 version_info = (1, 4, 0, 'dev', 0)
12@@ -83,7 +84,7 @@
13
14 def run(self, location=None, message=None, public_location=None,
15 dry_run=False, submit_branch=None, ignore_local=False):
16- from bzrlib import errors, trace, bzrdir
17+ from bzrlib import trace, bzrdir
18 if __name__ != 'bzrlib.plugins.pqm':
19 trace.warning('The bzr-pqm plugin needs to be called'
20 ' "bzrlib.plugins.pqm" not "%s"\n'
21@@ -103,12 +104,12 @@
22 b.lock_read()
23 self.add_cleanup(b.unlock)
24 if relpath and not tree and location != '.':
25- raise errors.BzrCommandError(
26+ raise BzrCommandError(
27 'No working tree was found, but we were not given the '
28 'exact path to the branch.\n'
29 'We found a branch at: %s' % (b.base,))
30 if message is None:
31- raise errors.BzrCommandError(
32+ raise BzrCommandError(
33 'You must supply a commit message for the pqm to use.')
34 submit(b, message=message, dry_run=dry_run,
35 public_location=public_location,
36@@ -125,18 +126,53 @@
37
38 takes_args = ['location?']
39
40- takes_options = [Option(
41- 'dry-run', help='Display the PQM message instead of sending.')]
42+ takes_options = [
43+ Option('dry-run', help='Display the PQM message instead of sending.'),
44+ Option(
45+ 'testfix',
46+ help="This is a testfix (tags commit with [testfix])."),
47+ Option(
48+ 'no-qa',
49+ help="Does not require QA (tags commit with [no-qa])."),
50+ Option(
51+ 'incremental',
52+ help="Incremental to other bug fix (tags commit with [incr])."),
53+ ]
54
55- def run(self, location=None, dry_run=False):
56+ def run(self, location=None, dry_run=False, testfix=False,
57+ no_qa=False, incremental=False):
58 from bzrlib.plugins.pqm.lpland import Submitter
59 from bzrlib import branch as _mod_branch
60+ from bzrlib.plugins.pqm.lpland import (
61+ MissingReviewError, MissingBugsError, MissingBugsIncrementalError)
62+
63+
64+ if no_qa and incremental:
65+ raise BzrCommandError(
66+ "--no-qa and --incremental cannot be given at the same time.")
67+
68 branch = _mod_branch.Branch.open_containing('.')[0]
69 if dry_run:
70 outf = self.outf
71 else:
72 outf = None
73- submitter = Submitter(branch, location).run(outf)
74+ try:
75+ submitter = Submitter(branch, location, testfix, no_qa, incremental
76+ ).run(outf)
77+ except MissingReviewError:
78+ raise BzrCommandError(
79+ "Cannot land branches that haven't got approved code "
80+ "reviews. Get an 'Approved' vote so we can fill in the "
81+ "[r=REVIEWER] section.")
82+ except MissingBugsError:
83+ raise BzrCommandError(
84+ "Branch doesn't have linked bugs and doesn't have no-qa "
85+ "option set. Use --no-qa, or link the related bugs to the "
86+ "branch.")
87+ except MissingBugsIncrementalError:
88+ raise BzrCommandError(
89+ "--incremental option requires bugs linked to the branch. "
90+ "Link the bugs or remove the --incremental option.")
91
92
93 register_command(cmd_pqm_submit)
94@@ -147,8 +183,7 @@
95 from bzrlib.tests import TestLoader
96 from unittest import TestSuite
97
98- import test_lpland
99- import test_pqm_submit
100+ from tests import test_lpland, test_pqm_submit
101
102 loader = TestLoader()
103 return TestSuite([
104
105=== modified file 'lpland.py'
106--- lpland.py 2010-04-07 09:47:52 +0000
107+++ lpland.py 2010-08-03 22:01:20 +0000
108@@ -33,6 +33,14 @@
109 """Raised when we try to get a review message without enough reviewers."""
110
111
112+class MissingBugsError(Exception):
113+ """Merge proposal has no linked bugs and no [no-qa] tag."""
114+
115+
116+class MissingBugsIncrementalError(Exception):
117+ """Merge proposal has the [incr] tag but no linked bugs."""
118+
119+
120 class LaunchpadBranchLander:
121
122 name = 'launchpad-branch-lander'
123@@ -171,25 +179,31 @@
124 # XXX: JonathanLange 2009-09-24: No unit tests.
125 return branch.composePublicURL(scheme="bzr+ssh")
126
127- def get_commit_message(self, commit_text, testfix=False):
128+ def get_commit_message(self, commit_text, testfix=False, no_qa=False,
129+ incremental=False):
130 """Get the Launchpad-style commit message for a merge proposal."""
131 reviews = self.get_reviews()
132 bugs = self.get_bugs()
133- if testfix:
134- testfix = '[testfix]'
135- else:
136- testfix = ''
137- return '%s%s%s %s' % (
138- testfix,
139+
140+ tags = ''.join([
141+ get_testfix_clause(testfix),
142 get_reviewer_clause(reviews),
143 get_bugs_clause(bugs),
144- commit_text)
145+ get_qa_clause(bugs, no_qa,
146+ incremental),
147+ ])
148+
149+ return '%s %s' % (tags, commit_text)
150
151
152 class Submitter(object):
153
154- def __init__(self, branch, location):
155+ def __init__(self, branch, location, testfix=False, no_qa=False,
156+ incremental=False):
157 self.branch = branch
158+ self.testfix = testfix
159+ self.no_qa = no_qa
160+ self.incremental = incremental
161 self.config = self.branch.get_config()
162 self.mail_to = self.config.get_user_option('pqm_email')
163 if not self.mail_to:
164@@ -211,10 +225,11 @@
165 submission.check_public_branch()
166
167 @staticmethod
168- def set_message(submission, mp):
169+ def set_message(submission, mp, testfix, no_qa, incremental):
170 pqm_command = ''.join(submission.to_lines())
171 commit_message = mp.commit_message or ''
172- start_message = mp.get_commit_message(commit_message)
173+ start_message = mp.get_commit_message(commit_message, testfix, no_qa,
174+ incremental)
175 message = msgeditor.edit_commit_message(
176 'pqm command:\n%s' % pqm_command,
177 start_message=start_message).rstrip('\n')
178@@ -227,7 +242,8 @@
179 mp = self.lander.load_merge_proposal(self.location)
180 submission = self.submission(mp)
181 self.check_submission(submission)
182- self.set_message(submission, mp)
183+ self.set_message(submission, mp, self.testfix, self.no_qa,
184+ self.incremental)
185 email = submission.to_email(self.mail_from, self.mail_to)
186 if outf is not None:
187 outf.write(email.as_string())
188@@ -256,6 +272,39 @@
189 return '[bug=%s]' % ','.join(str(bug.id) for bug in bugs)
190
191
192+def get_testfix_clause(testfix=False):
193+ """Get the testfix clause."""
194+ if testfix:
195+ testfix_clause = '[testfix]'
196+ else:
197+ testfix_clause = ''
198+ return testfix_clause
199+
200+
201+def get_qa_clause(bugs, no_qa=False, incremental=False):
202+ """Check the no-qa and incremental options, getting the qa clause.
203+
204+ The qa clause will always be or no-qa, or incremental or no tags, never
205+ both at the same time.
206+ """
207+ qa_clause = ""
208+
209+ if not bugs and not no_qa and not incremental:
210+ raise MissingBugsError
211+
212+ if incremental and not bugs:
213+ raise MissingBugsIncrementalError
214+
215+ if incremental:
216+ qa_clause = '[incr]'
217+ elif no_qa:
218+ qa_clause = '[no-qa]'
219+ else:
220+ qa_clause = ''
221+
222+ return qa_clause
223+
224+
225 def get_reviewer_handle(reviewer):
226 """Get the handle for 'reviewer'.
227
228
229=== added directory 'tests'
230=== added file 'tests/__init__.py'
231=== added file 'tests/fakemethod.py'
232--- tests/fakemethod.py 1970-01-01 00:00:00 +0000
233+++ tests/fakemethod.py 2010-08-03 22:01:20 +0000
234@@ -0,0 +1,21 @@
235+__metaclass__ = type
236+
237+class FakeMethod:
238+ """Catch any function or method call, and record the fact."""
239+
240+ def __init__(self, result=None, failure=None):
241+ """Set up a fake function or method.
242+
243+ :param result: Value to return.
244+ :param failure: Exception to raise.
245+ """
246+ self.result = result
247+ self.failure = failure
248+
249+ def __call__(self, *args, **kwargs):
250+ """Catch an invocation to the method."""
251+
252+ if self.failure is None:
253+ return self.result
254+ else:
255+ raise self.failure
256
257=== renamed file 'test_lpland.py' => 'tests/test_lpland.py'
258--- test_lpland.py 2010-03-31 14:41:33 +0000
259+++ tests/test_lpland.py 2010-08-03 22:01:20 +0000
260@@ -13,7 +13,11 @@
261
262 from bzrlib.plugins.pqm.lpland import (
263 get_bazaar_host, get_bugs_clause, get_reviewer_clause,
264- get_reviewer_handle, MissingReviewError)
265+ get_reviewer_handle, get_testfix_clause, get_qa_clause,
266+ MissingReviewError, MissingBugsError, MissingBugsIncrementalError,
267+ MergeProposal)
268+
269+from fakemethod import FakeMethod
270
271
272 class FakeBug:
273@@ -48,6 +52,16 @@
274 self.network = network
275
276
277+class FakeLPMergeProposal:
278+ """Fake launchpadlib MergeProposal object.
279+
280+ Only used for the purposes of testing.
281+ """
282+
283+ def __init__(self, root=None):
284+ self._root = root
285+
286+
287 class TestBugsClaused(unittest.TestCase):
288 """Tests for `get_bugs_clause`."""
289
290@@ -70,6 +84,64 @@
291 self.assertEqual('[bug=20,45]', bugs_clause)
292
293
294+class TestGetTestfixClause(unittest.TestCase):
295+ """Tests for `get_testfix_clause`"""
296+
297+ def test_no_testfix(self):
298+ testfix = False
299+ self.assertEqual('', get_testfix_clause(testfix))
300+
301+ def test_is_testfix(self):
302+ testfix = True
303+ self.assertEqual('[testfix]', get_testfix_clause(testfix))
304+
305+
306+class TestGetQaClause(unittest.TestCase):
307+ """Tests for `get_qa_clause`"""
308+
309+ def test_no_bugs_no_option_given(self):
310+ bugs = None
311+ no_qa = False
312+ incr = False
313+ self.assertRaises(MissingBugsError, get_qa_clause, bugs, no_qa,
314+ incr)
315+
316+ def test_bugs_noqa_option_given(self):
317+ bug1 = FakeBug(20)
318+ no_qa = True
319+ incr = False
320+ self.assertEqual('[no-qa]',
321+ get_qa_clause([bug1], no_qa, incr))
322+
323+ def test_no_bugs_noqa_option_given(self):
324+ bugs = None
325+ no_qa = True
326+ incr = False
327+ self.assertEqual('[no-qa]',
328+ get_qa_clause(bugs, no_qa, incr))
329+
330+ def test_bugs_no_option_given(self):
331+ bug1 = FakeBug(20)
332+ no_qa = False
333+ incr = False
334+ self.assertEqual('',
335+ get_qa_clause([bug1], no_qa, incr))
336+
337+ def test_bugs_incr_option_given(self):
338+ bug1 = FakeBug(20)
339+ no_qa = False
340+ incr = True
341+ self.assertEqual('[incr]',
342+ get_qa_clause([bug1], no_qa, incr))
343+
344+ def test_no_bugs_incr_option_given(self):
345+ bugs = None
346+ no_qa = False
347+ incr = True
348+ self.assertRaises(MissingBugsIncrementalError,
349+ get_qa_clause, bugs, no_qa, incr)
350+
351+
352 class TestGetReviewerHandle(unittest.TestCase):
353 """Tests for `get_reviewer_handle`."""
354
355@@ -96,6 +168,91 @@
356 self.assertEqual('foo', get_reviewer_handle(person))
357
358
359+class TestGetCommitMessage(unittest.TestCase):
360+
361+ def setUp(self):
362+ self.mp = MergeProposal(FakeLPMergeProposal())
363+ self.fake_bug = FakeBug(20)
364+ self.fake_person = self.makePerson('foo')
365+
366+ def makePerson(self, name):
367+ return FakePerson(name, [])
368+
369+ def test_commit_with_bugs(self):
370+ incr = False
371+ no_qa = False
372+ testfix = False
373+
374+ self.mp.get_bugs = FakeMethod([self.fake_bug])
375+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
376+
377+ self.assertEqual("[r=foo][ui=none][bug=20] Foobaring the sbrubble.",
378+ self.mp.get_commit_message("Foobaring the sbrubble.",
379+ testfix, no_qa, incr))
380+
381+ def test_commit_no_bugs_no_noqa(self):
382+ incr = False
383+ no_qa = False
384+ testfix = False
385+
386+ self.mp.get_bugs = FakeMethod([])
387+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
388+
389+ self.assertRaises(MissingBugsError, self.mp.get_commit_message,
390+ testfix, no_qa, incr)
391+
392+ def test_commit_no_bugs_with_noqa(self):
393+ incr = False
394+ no_qa = True
395+ testfix = False
396+
397+ self.mp.get_bugs = FakeMethod([])
398+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
399+
400+ self.assertEqual("[r=foo][ui=none][no-qa] Foobaring the sbrubble.",
401+ self.mp.get_commit_message("Foobaring the sbrubble.",
402+ testfix, no_qa, incr))
403+
404+ def test_commit_bugs_with_noqa(self):
405+ incr = False
406+ no_qa = True
407+ testfix = False
408+
409+ self.mp.get_bugs = FakeMethod([self.fake_bug])
410+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
411+
412+ self.assertEqual(
413+ "[r=foo][ui=none][bug=20][no-qa] Foobaring the sbrubble.",
414+ self.mp.get_commit_message("Foobaring the sbrubble.",
415+ testfix, no_qa, incr))
416+
417+ def test_commit_bugs_with_incr(self):
418+ incr = True
419+ no_qa = False
420+ testfix = False
421+
422+ self.mp.get_bugs = FakeMethod([self.fake_bug])
423+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
424+
425+ self.assertEqual(
426+ "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
427+ self.mp.get_commit_message("Foobaring the sbrubble.",
428+ testfix, no_qa, incr))
429+
430+ def test_commit_no_bugs_with_incr(self):
431+ incr = True
432+ no_qa = False
433+ testfix = False
434+
435+ self.mp.get_bugs = FakeMethod([self.fake_bug])
436+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
437+
438+ self.assertEqual(
439+ "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
440+ self.mp.get_commit_message("Foobaring the sbrubble.",
441+ testfix, no_qa, incr))
442+
443+
444 class TestGetReviewerClause(unittest.TestCase):
445 """Tests for `get_reviewer_clause`."""
446
447
448=== renamed file 'test_pqm_submit.py' => 'tests/test_pqm_submit.py'

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: