Merge lp:~ursinha/launchpad/add-ec2land-rules-orphaned-branches-no-conflicts into lp:launchpad

Proposed by Ursula Junque
Status: Merged
Approved by: Ursula Junque
Approved revision: not available
Merge reported by: Ursula Junque
Merged at revision: not available
Proposed branch: lp:~ursinha/launchpad/add-ec2land-rules-orphaned-branches-no-conflicts
Merge into: lp:launchpad
Diff against target: 387 lines (+284/-13)
3 files modified
lib/devscripts/autoland.py (+51/-8)
lib/devscripts/ec2test/builtins.py (+26/-4)
lib/devscripts/tests/test_autoland.py (+207/-1)
To merge this branch: bzr merge lp:~ursinha/launchpad/add-ec2land-rules-orphaned-branches-no-conflicts
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+31386@code.launchpad.net

Commit message

[r=henninge][ui=none][bug=601995] Adds the --incremental and --no-qa options to the ec2 land script, as part of the orphaned branches story. Also adds tests to the previous implemented methods.

Description of the change

This is the same set of changes as approved in https://code.edge.launchpad.net/~ursinha/launchpad/add-ec2land-rules-orphaned-branches/+merge/29255, with all suggestions applied. I had trouble solving a merge conflict in that one, so created this branch with all conflicts solved, tested and ready to land.

= Summary =

This branch is a fix for bug 601995, adding to ec2 land two options: --no-qa and --incr. --no-qa option should add the [no-qa] tag to the commit message, and --incr option should add the [incr] tag. 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 or the --incr options are passed to ec2 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 method by adding more tests to test_autoland.py.

./bin/test -vvt devscripts.*

It's not possible to test get_commit_message directly because it all depends on launchpadlib.

== 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 ec2 land.

Considering staging is down for some time now, I've tested with this mp: https://code.edge.launchpad.net/~ursinha/qa-tagger/find_linked_branches-bug-591856/+merge/28025:

$ ec2 land https://code.edge.launchpad.net/~ursinha/qa-tagger/find_linked_branches-bug-591856/+merge/28025 --force --commit-text "Testing ec2 land" --dry-run [combinations below]

Results should be as following:
* Both --incr and --no-qa at the same time: should error.
* without --no-qa and nor linked bugs: should error.
* with --incr and no linked bugs: should error.
* with --incr 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
Henning Eggers (henninge) wrote :

I compared the diffs and they are essentially identical. ;-)

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/devscripts/autoland.py'
2--- lib/devscripts/autoland.py 2010-03-05 21:47:29 +0000
3+++ lib/devscripts/autoland.py 2010-07-30 15:13:50 +0000
4@@ -12,6 +12,14 @@
5 """Raised when we try to get a review message without enough reviewers."""
6
7
8+class MissingBugsError(Exception):
9+ """Merge proposal has no linked bugs and no [no-qa] tag."""
10+
11+
12+class MissingBugsIncrementalError(Exception):
13+ """Merge proposal has the [incr] tag but no linked bugs."""
14+
15+
16 class LaunchpadBranchLander:
17
18 name = 'launchpad-branch-lander'
19@@ -152,19 +160,54 @@
20 # URL. Do it ourselves.
21 return URI(scheme='bzr+ssh', host=host, path='/' + branch.unique_name)
22
23- def get_commit_message(self, commit_text, testfix=False):
24+ def get_commit_message(self, commit_text, testfix=False, no_qa=False,
25+ incremental=False):
26 """Get the Launchpad-style commit message for a merge proposal."""
27 reviews = self.get_reviews()
28 bugs = self.get_bugs()
29- if testfix:
30- testfix = '[testfix]'
31- else:
32- testfix = ''
33- return '%s%s%s %s' % (
34- testfix,
35+
36+ tags = ''.join([
37+ get_testfix_clause(testfix),
38 get_reviewer_clause(reviews),
39 get_bugs_clause(bugs),
40- commit_text)
41+ get_qa_clause(bugs, no_qa,
42+ incremental),
43+ ])
44+
45+ return '%s %s' % (tags, commit_text)
46+
47+
48+def get_testfix_clause(testfix=False):
49+ """Get the testfix clause."""
50+ if testfix:
51+ testfix_clause = '[testfix]'
52+ else:
53+ testfix_clause = ''
54+ return testfix_clause
55+
56+
57+def get_qa_clause(bugs, no_qa=False, incremental=False):
58+ """Check the no-qa and incremental options, getting the qa clause.
59+
60+ The qa clause will always be or no-qa, or incremental or no tags, never both at
61+ the same time.
62+ """
63+ qa_clause = ""
64+
65+ if not bugs and not no_qa and not incremental:
66+ raise MissingBugsError
67+
68+ if incremental and not bugs:
69+ raise MissingBugsIncrementalError
70+
71+ if incremental:
72+ qa_clause = '[incr]'
73+ elif no_qa:
74+ qa_clause = '[no-qa]'
75+ else:
76+ qa_clause = ''
77+
78+ return qa_clause
79
80
81 def get_email(person):
82
83=== modified file 'lib/devscripts/ec2test/builtins.py'
84--- lib/devscripts/ec2test/builtins.py 2010-04-13 19:32:04 +0000
85+++ lib/devscripts/ec2test/builtins.py 2010-07-30 15:13:50 +0000
86@@ -321,7 +321,13 @@
87 Option('print-commit', help="Print the full commit message."),
88 Option(
89 'testfix',
90- help="Include the [testfix] prefix in the commit message."),
91+ help="This is a testfix (tags commit with [testfix])."),
92+ Option(
93+ 'no-qa',
94+ help="Does not require QA (tags commit with [no-qa])."),
95+ Option(
96+ 'incremental',
97+ help="Incremental to other bug fix (tags commit with [incr])."),
98 Option(
99 'commit-text', short_name='s', type=str,
100 help=(
101@@ -355,10 +361,12 @@
102 def run(self, merge_proposal=None, machine=None,
103 instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False,
104 debug=False, commit_text=None, dry_run=False, testfix=False,
105- print_commit=False, force=False, attached=False):
106+ no_qa=False, incremental=False, print_commit=False, force=False,
107+ attached=False):
108 try:
109 from devscripts.autoland import (
110- LaunchpadBranchLander, MissingReviewError)
111+ LaunchpadBranchLander, MissingReviewError, MissingBugsError,
112+ MissingBugsIncrementalError)
113 except ImportError:
114 self.outf.write(
115 "***************************************************\n\n"
116@@ -374,6 +382,9 @@
117 if print_commit and dry_run:
118 raise BzrCommandError(
119 "Cannot specify --print-commit and --dry-run.")
120+ if no_qa and incremental:
121+ raise BzrCommandError(
122+ "--no-qa and --incremental cannot be given at the same time.")
123 lander = LaunchpadBranchLander.load()
124
125 if merge_proposal is None:
126@@ -396,12 +407,23 @@
127 "Commit text not specified. Use --commit-text, or specify a "
128 "message on the merge proposal.")
129 try:
130- commit_message = mp.get_commit_message(commit_text, testfix)
131+ commit_message = mp.get_commit_message(
132+ commit_text, testfix, no_qa, incremental)
133 except MissingReviewError:
134 raise BzrCommandError(
135 "Cannot land branches that haven't got approved code "
136 "reviews. Get an 'Approved' vote so we can fill in the "
137 "[r=REVIEWER] section.")
138+ except MissingBugsError:
139+ raise BzrCommandError(
140+ "Branch doesn't have linked bugs and doesn't have no-qa "
141+ "option set. Use --no-qa, or link the related bugs to the "
142+ "branch.")
143+ except MissingBugsIncrementalError:
144+ raise BzrCommandError(
145+ "--incremental option requires bugs linked to the branch. "
146+ "Link the bugs or remove the --incremental option.")
147+
148 if print_commit:
149 print commit_message
150 return
151
152=== modified file 'lib/devscripts/tests/test_autoland.py'
153--- lib/devscripts/tests/test_autoland.py 2009-10-08 23:22:16 +0000
154+++ lib/devscripts/tests/test_autoland.py 2010-07-30 15:13:50 +0000
155@@ -6,12 +6,17 @@
156 __metaclass__ = type
157
158 import unittest
159+import re
160
161 from launchpadlib.launchpad import EDGE_SERVICE_ROOT, STAGING_SERVICE_ROOT
162
163+from lp.testing.fakemethod import FakeMethod
164+
165 from devscripts.autoland import (
166 get_bazaar_host, get_bugs_clause, get_reviewer_clause,
167- get_reviewer_handle, MissingReviewError)
168+ get_reviewer_handle, get_qa_clause, get_testfix_clause,
169+ MissingReviewError, MissingBugsError, MissingBugsIncrementalError,
170+ MergeProposal)
171
172
173 class FakeBug:
174@@ -46,6 +51,64 @@
175 self.network = network
176
177
178+class FakeLPMergeProposal:
179+ """Fake launchpadlib MergeProposal object.
180+
181+ Only used for the purposes of testing.
182+ """
183+
184+ def __init__(self, root=None):
185+ self._root = root
186+
187+
188+class TestPQMRegexAcceptance(unittest.TestCase):
189+ """Tests if the generated commit message is accepted by PQM regexes."""
190+
191+ def setUp(self):
192+ # PQM regexes; might need update once in a while
193+ self.devel_open_re = ("(?is)^\s*(:?\[testfix\])?\[(?:"
194+ "release-critical=[^\]]+|rs?=[^\]]+)\]\[ui=(?:.+)\]")
195+ self.dbdevel_normal_re = ("(?is)^\s*(:?\[testfix\])?\[(?:"
196+ "release-critical|rs?=[^\]]+)\]")
197+
198+ self.mp = MergeProposal(FakeLPMergeProposal())
199+ self.fake_bug = FakeBug(20)
200+ self.fake_person = FakePerson('foo', [])
201+ self.mp.get_bugs = FakeMethod([self.fake_bug])
202+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
203+
204+ def assertRegexpMatches(self, text, expected_regexp, msg=None):
205+ """Fail the test unless the text matches the regular expression.
206+
207+ Method default in Python 2.7. Can be removed as soon as LP goes 2.7.
208+ """
209+ if isinstance(expected_regexp, basestring):
210+ expected_regexp = re.compile(expected_regexp)
211+ if not expected_regexp.search(text):
212+ msg = msg or "Regexp didn't match"
213+ msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern,
214+ text)
215+ raise self.failureException(msg)
216+
217+ def _test_commit_message_match(self, incr, no_qa, testfix):
218+ commit_message = self.mp.get_commit_message("Foobaring the sbrubble.",
219+ testfix, no_qa, incr)
220+ self.assertRegexpMatches(commit_message, self.devel_open_re)
221+ self.assertRegexpMatches(commit_message, self.dbdevel_normal_re)
222+
223+ def test_testfix_match(self):
224+ self._test_commit_message_match(incr=False, no_qa=False, testfix=True)
225+
226+ def test_regular_match(self):
227+ self._test_commit_message_match(incr=False, no_qa=False, testfix=False)
228+
229+ def test_noqa_match(self):
230+ self._test_commit_message_match(incr=False, no_qa=True, testfix=False)
231+
232+ def test_incr_match(self):
233+ self._test_commit_message_match(incr=True, no_qa=False, testfix=False)
234+
235+
236 class TestBugsClaused(unittest.TestCase):
237 """Tests for `get_bugs_clause`."""
238
239@@ -68,6 +131,149 @@
240 self.assertEqual('[bug=20,45]', bugs_clause)
241
242
243+class TestGetCommitMessage(unittest.TestCase):
244+
245+ def setUp(self):
246+ self.mp = MergeProposal(FakeLPMergeProposal())
247+ self.fake_bug = FakeBug(20)
248+ self.fake_person = self.makePerson('foo')
249+
250+ def makePerson(self, name):
251+ return FakePerson(name, [])
252+
253+ def test_commit_with_bugs(self):
254+ incr = False
255+ no_qa = False
256+ testfix = False
257+
258+ self.mp.get_bugs = FakeMethod([self.fake_bug])
259+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
260+
261+ self.assertEqual("[r=foo][ui=none][bug=20] Foobaring the sbrubble.",
262+ self.mp.get_commit_message("Foobaring the sbrubble.",
263+ testfix, no_qa, incr))
264+
265+ def test_commit_no_bugs_no_noqa(self):
266+ incr = False
267+ no_qa = False
268+ testfix = False
269+
270+ self.mp.get_bugs = FakeMethod([])
271+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
272+
273+ self.assertRaises(MissingBugsError, self.mp.get_commit_message,
274+ testfix, no_qa, incr)
275+
276+ def test_commit_no_bugs_with_noqa(self):
277+ incr = False
278+ no_qa = True
279+ testfix = False
280+
281+ self.mp.get_bugs = FakeMethod([])
282+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
283+
284+ self.assertEqual("[r=foo][ui=none][no-qa] Foobaring the sbrubble.",
285+ self.mp.get_commit_message("Foobaring the sbrubble.",
286+ testfix, no_qa, incr))
287+
288+ def test_commit_bugs_with_noqa(self):
289+ incr = False
290+ no_qa = True
291+ testfix = False
292+
293+ self.mp.get_bugs = FakeMethod([self.fake_bug])
294+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
295+
296+ self.assertEqual(
297+ "[r=foo][ui=none][bug=20][no-qa] Foobaring the sbrubble.",
298+ self.mp.get_commit_message("Foobaring the sbrubble.",
299+ testfix, no_qa, incr))
300+
301+ def test_commit_bugs_with_incr(self):
302+ incr = True
303+ no_qa = False
304+ testfix = False
305+
306+ self.mp.get_bugs = FakeMethod([self.fake_bug])
307+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
308+
309+ self.assertEqual(
310+ "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
311+ self.mp.get_commit_message("Foobaring the sbrubble.",
312+ testfix, no_qa, incr))
313+
314+ def test_commit_no_bugs_with_incr(self):
315+ incr = True
316+ no_qa = False
317+ testfix = False
318+
319+ self.mp.get_bugs = FakeMethod([self.fake_bug])
320+ self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
321+
322+ self.assertEqual(
323+ "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
324+ self.mp.get_commit_message("Foobaring the sbrubble.",
325+ testfix, no_qa, incr))
326+
327+
328+class TestGetTestfixClause(unittest.TestCase):
329+ """Tests for `get_testfix_clause`"""
330+
331+ def test_no_testfix(self):
332+ testfix = False
333+ self.assertEqual('', get_testfix_clause(testfix))
334+
335+ def test_is_testfix(self):
336+ testfix = True
337+ self.assertEqual('[testfix]', get_testfix_clause(testfix))
338+
339+
340+class TestGetQaClause(unittest.TestCase):
341+ """Tests for `get_qa_clause`"""
342+
343+ def test_no_bugs_no_option_given(self):
344+ bugs = None
345+ no_qa = False
346+ incr = False
347+ self.assertRaises(MissingBugsError, get_qa_clause, bugs, no_qa,
348+ incr)
349+
350+ def test_bugs_noqa_option_given(self):
351+ bug1 = FakeBug(20)
352+ no_qa = True
353+ incr = False
354+ self.assertEqual('[no-qa]',
355+ get_qa_clause([bug1], no_qa, incr))
356+
357+ def test_no_bugs_noqa_option_given(self):
358+ bugs = None
359+ no_qa = True
360+ incr = False
361+ self.assertEqual('[no-qa]',
362+ get_qa_clause(bugs, no_qa, incr))
363+
364+ def test_bugs_no_option_given(self):
365+ bug1 = FakeBug(20)
366+ no_qa = False
367+ incr = False
368+ self.assertEqual('',
369+ get_qa_clause([bug1], no_qa, incr))
370+
371+ def test_bugs_incr_option_given(self):
372+ bug1 = FakeBug(20)
373+ no_qa = False
374+ incr = True
375+ self.assertEqual('[incr]',
376+ get_qa_clause([bug1], no_qa, incr))
377+
378+ def test_no_bugs_incr_option_given(self):
379+ bugs = None
380+ no_qa = False
381+ incr = True
382+ self.assertRaises(MissingBugsIncrementalError,
383+ get_qa_clause, bugs, no_qa, incr)
384+
385+
386 class TestGetReviewerHandle(unittest.TestCase):
387 """Tests for `get_reviewer_handle`."""
388