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
=== modified file 'lib/devscripts/autoland.py'
--- lib/devscripts/autoland.py 2010-03-05 21:47:29 +0000
+++ lib/devscripts/autoland.py 2010-07-30 15:13:50 +0000
@@ -12,6 +12,14 @@
12 """Raised when we try to get a review message without enough reviewers."""12 """Raised when we try to get a review message without enough reviewers."""
1313
1414
15class MissingBugsError(Exception):
16 """Merge proposal has no linked bugs and no [no-qa] tag."""
17
18
19class MissingBugsIncrementalError(Exception):
20 """Merge proposal has the [incr] tag but no linked bugs."""
21
22
15class LaunchpadBranchLander:23class LaunchpadBranchLander:
1624
17 name = 'launchpad-branch-lander'25 name = 'launchpad-branch-lander'
@@ -152,19 +160,54 @@
152 # URL. Do it ourselves.160 # URL. Do it ourselves.
153 return URI(scheme='bzr+ssh', host=host, path='/' + branch.unique_name)161 return URI(scheme='bzr+ssh', host=host, path='/' + branch.unique_name)
154162
155 def get_commit_message(self, commit_text, testfix=False):163 def get_commit_message(self, commit_text, testfix=False, no_qa=False,
164 incremental=False):
156 """Get the Launchpad-style commit message for a merge proposal."""165 """Get the Launchpad-style commit message for a merge proposal."""
157 reviews = self.get_reviews()166 reviews = self.get_reviews()
158 bugs = self.get_bugs()167 bugs = self.get_bugs()
159 if testfix:168
160 testfix = '[testfix]'169 tags = ''.join([
161 else:170 get_testfix_clause(testfix),
162 testfix = ''
163 return '%s%s%s %s' % (
164 testfix,
165 get_reviewer_clause(reviews),171 get_reviewer_clause(reviews),
166 get_bugs_clause(bugs),172 get_bugs_clause(bugs),
167 commit_text)173 get_qa_clause(bugs, no_qa,
174 incremental),
175 ])
176
177 return '%s %s' % (tags, commit_text)
178
179
180def get_testfix_clause(testfix=False):
181 """Get the testfix clause."""
182 if testfix:
183 testfix_clause = '[testfix]'
184 else:
185 testfix_clause = ''
186 return testfix_clause
187
188
189def get_qa_clause(bugs, no_qa=False, incremental=False):
190 """Check the no-qa and incremental options, getting the qa clause.
191
192 The qa clause will always be or no-qa, or incremental or no tags, never both at
193 the same time.
194 """
195 qa_clause = ""
196
197 if not bugs and not no_qa and not incremental:
198 raise MissingBugsError
199
200 if incremental and not bugs:
201 raise MissingBugsIncrementalError
202
203 if incremental:
204 qa_clause = '[incr]'
205 elif no_qa:
206 qa_clause = '[no-qa]'
207 else:
208 qa_clause = ''
209
210 return qa_clause
168211
169212
170def get_email(person):213def get_email(person):
171214
=== modified file 'lib/devscripts/ec2test/builtins.py'
--- lib/devscripts/ec2test/builtins.py 2010-04-13 19:32:04 +0000
+++ lib/devscripts/ec2test/builtins.py 2010-07-30 15:13:50 +0000
@@ -321,7 +321,13 @@
321 Option('print-commit', help="Print the full commit message."),321 Option('print-commit', help="Print the full commit message."),
322 Option(322 Option(
323 'testfix',323 'testfix',
324 help="Include the [testfix] prefix in the commit message."),324 help="This is a testfix (tags commit with [testfix])."),
325 Option(
326 'no-qa',
327 help="Does not require QA (tags commit with [no-qa])."),
328 Option(
329 'incremental',
330 help="Incremental to other bug fix (tags commit with [incr])."),
325 Option(331 Option(
326 'commit-text', short_name='s', type=str,332 'commit-text', short_name='s', type=str,
327 help=(333 help=(
@@ -355,10 +361,12 @@
355 def run(self, merge_proposal=None, machine=None,361 def run(self, merge_proposal=None, machine=None,
356 instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False,362 instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False,
357 debug=False, commit_text=None, dry_run=False, testfix=False,363 debug=False, commit_text=None, dry_run=False, testfix=False,
358 print_commit=False, force=False, attached=False):364 no_qa=False, incremental=False, print_commit=False, force=False,
365 attached=False):
359 try:366 try:
360 from devscripts.autoland import (367 from devscripts.autoland import (
361 LaunchpadBranchLander, MissingReviewError)368 LaunchpadBranchLander, MissingReviewError, MissingBugsError,
369 MissingBugsIncrementalError)
362 except ImportError:370 except ImportError:
363 self.outf.write(371 self.outf.write(
364 "***************************************************\n\n"372 "***************************************************\n\n"
@@ -374,6 +382,9 @@
374 if print_commit and dry_run:382 if print_commit and dry_run:
375 raise BzrCommandError(383 raise BzrCommandError(
376 "Cannot specify --print-commit and --dry-run.")384 "Cannot specify --print-commit and --dry-run.")
385 if no_qa and incremental:
386 raise BzrCommandError(
387 "--no-qa and --incremental cannot be given at the same time.")
377 lander = LaunchpadBranchLander.load()388 lander = LaunchpadBranchLander.load()
378389
379 if merge_proposal is None:390 if merge_proposal is None:
@@ -396,12 +407,23 @@
396 "Commit text not specified. Use --commit-text, or specify a "407 "Commit text not specified. Use --commit-text, or specify a "
397 "message on the merge proposal.")408 "message on the merge proposal.")
398 try:409 try:
399 commit_message = mp.get_commit_message(commit_text, testfix)410 commit_message = mp.get_commit_message(
411 commit_text, testfix, no_qa, incremental)
400 except MissingReviewError:412 except MissingReviewError:
401 raise BzrCommandError(413 raise BzrCommandError(
402 "Cannot land branches that haven't got approved code "414 "Cannot land branches that haven't got approved code "
403 "reviews. Get an 'Approved' vote so we can fill in the "415 "reviews. Get an 'Approved' vote so we can fill in the "
404 "[r=REVIEWER] section.")416 "[r=REVIEWER] section.")
417 except MissingBugsError:
418 raise BzrCommandError(
419 "Branch doesn't have linked bugs and doesn't have no-qa "
420 "option set. Use --no-qa, or link the related bugs to the "
421 "branch.")
422 except MissingBugsIncrementalError:
423 raise BzrCommandError(
424 "--incremental option requires bugs linked to the branch. "
425 "Link the bugs or remove the --incremental option.")
426
405 if print_commit:427 if print_commit:
406 print commit_message428 print commit_message
407 return429 return
408430
=== modified file 'lib/devscripts/tests/test_autoland.py'
--- lib/devscripts/tests/test_autoland.py 2009-10-08 23:22:16 +0000
+++ lib/devscripts/tests/test_autoland.py 2010-07-30 15:13:50 +0000
@@ -6,12 +6,17 @@
6__metaclass__ = type6__metaclass__ = type
77
8import unittest8import unittest
9import re
910
10from launchpadlib.launchpad import EDGE_SERVICE_ROOT, STAGING_SERVICE_ROOT11from launchpadlib.launchpad import EDGE_SERVICE_ROOT, STAGING_SERVICE_ROOT
1112
13from lp.testing.fakemethod import FakeMethod
14
12from devscripts.autoland import (15from devscripts.autoland import (
13 get_bazaar_host, get_bugs_clause, get_reviewer_clause,16 get_bazaar_host, get_bugs_clause, get_reviewer_clause,
14 get_reviewer_handle, MissingReviewError)17 get_reviewer_handle, get_qa_clause, get_testfix_clause,
18 MissingReviewError, MissingBugsError, MissingBugsIncrementalError,
19 MergeProposal)
1520
1621
17class FakeBug:22class FakeBug:
@@ -46,6 +51,64 @@
46 self.network = network51 self.network = network
4752
4853
54class FakeLPMergeProposal:
55 """Fake launchpadlib MergeProposal object.
56
57 Only used for the purposes of testing.
58 """
59
60 def __init__(self, root=None):
61 self._root = root
62
63
64class TestPQMRegexAcceptance(unittest.TestCase):
65 """Tests if the generated commit message is accepted by PQM regexes."""
66
67 def setUp(self):
68 # PQM regexes; might need update once in a while
69 self.devel_open_re = ("(?is)^\s*(:?\[testfix\])?\[(?:"
70 "release-critical=[^\]]+|rs?=[^\]]+)\]\[ui=(?:.+)\]")
71 self.dbdevel_normal_re = ("(?is)^\s*(:?\[testfix\])?\[(?:"
72 "release-critical|rs?=[^\]]+)\]")
73
74 self.mp = MergeProposal(FakeLPMergeProposal())
75 self.fake_bug = FakeBug(20)
76 self.fake_person = FakePerson('foo', [])
77 self.mp.get_bugs = FakeMethod([self.fake_bug])
78 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
79
80 def assertRegexpMatches(self, text, expected_regexp, msg=None):
81 """Fail the test unless the text matches the regular expression.
82
83 Method default in Python 2.7. Can be removed as soon as LP goes 2.7.
84 """
85 if isinstance(expected_regexp, basestring):
86 expected_regexp = re.compile(expected_regexp)
87 if not expected_regexp.search(text):
88 msg = msg or "Regexp didn't match"
89 msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern,
90 text)
91 raise self.failureException(msg)
92
93 def _test_commit_message_match(self, incr, no_qa, testfix):
94 commit_message = self.mp.get_commit_message("Foobaring the sbrubble.",
95 testfix, no_qa, incr)
96 self.assertRegexpMatches(commit_message, self.devel_open_re)
97 self.assertRegexpMatches(commit_message, self.dbdevel_normal_re)
98
99 def test_testfix_match(self):
100 self._test_commit_message_match(incr=False, no_qa=False, testfix=True)
101
102 def test_regular_match(self):
103 self._test_commit_message_match(incr=False, no_qa=False, testfix=False)
104
105 def test_noqa_match(self):
106 self._test_commit_message_match(incr=False, no_qa=True, testfix=False)
107
108 def test_incr_match(self):
109 self._test_commit_message_match(incr=True, no_qa=False, testfix=False)
110
111
49class TestBugsClaused(unittest.TestCase):112class TestBugsClaused(unittest.TestCase):
50 """Tests for `get_bugs_clause`."""113 """Tests for `get_bugs_clause`."""
51114
@@ -68,6 +131,149 @@
68 self.assertEqual('[bug=20,45]', bugs_clause)131 self.assertEqual('[bug=20,45]', bugs_clause)
69132
70133
134class TestGetCommitMessage(unittest.TestCase):
135
136 def setUp(self):
137 self.mp = MergeProposal(FakeLPMergeProposal())
138 self.fake_bug = FakeBug(20)
139 self.fake_person = self.makePerson('foo')
140
141 def makePerson(self, name):
142 return FakePerson(name, [])
143
144 def test_commit_with_bugs(self):
145 incr = False
146 no_qa = False
147 testfix = False
148
149 self.mp.get_bugs = FakeMethod([self.fake_bug])
150 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
151
152 self.assertEqual("[r=foo][ui=none][bug=20] Foobaring the sbrubble.",
153 self.mp.get_commit_message("Foobaring the sbrubble.",
154 testfix, no_qa, incr))
155
156 def test_commit_no_bugs_no_noqa(self):
157 incr = False
158 no_qa = False
159 testfix = False
160
161 self.mp.get_bugs = FakeMethod([])
162 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
163
164 self.assertRaises(MissingBugsError, self.mp.get_commit_message,
165 testfix, no_qa, incr)
166
167 def test_commit_no_bugs_with_noqa(self):
168 incr = False
169 no_qa = True
170 testfix = False
171
172 self.mp.get_bugs = FakeMethod([])
173 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
174
175 self.assertEqual("[r=foo][ui=none][no-qa] Foobaring the sbrubble.",
176 self.mp.get_commit_message("Foobaring the sbrubble.",
177 testfix, no_qa, incr))
178
179 def test_commit_bugs_with_noqa(self):
180 incr = False
181 no_qa = True
182 testfix = False
183
184 self.mp.get_bugs = FakeMethod([self.fake_bug])
185 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
186
187 self.assertEqual(
188 "[r=foo][ui=none][bug=20][no-qa] Foobaring the sbrubble.",
189 self.mp.get_commit_message("Foobaring the sbrubble.",
190 testfix, no_qa, incr))
191
192 def test_commit_bugs_with_incr(self):
193 incr = True
194 no_qa = False
195 testfix = False
196
197 self.mp.get_bugs = FakeMethod([self.fake_bug])
198 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
199
200 self.assertEqual(
201 "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
202 self.mp.get_commit_message("Foobaring the sbrubble.",
203 testfix, no_qa, incr))
204
205 def test_commit_no_bugs_with_incr(self):
206 incr = True
207 no_qa = False
208 testfix = False
209
210 self.mp.get_bugs = FakeMethod([self.fake_bug])
211 self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
212
213 self.assertEqual(
214 "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
215 self.mp.get_commit_message("Foobaring the sbrubble.",
216 testfix, no_qa, incr))
217
218
219class TestGetTestfixClause(unittest.TestCase):
220 """Tests for `get_testfix_clause`"""
221
222 def test_no_testfix(self):
223 testfix = False
224 self.assertEqual('', get_testfix_clause(testfix))
225
226 def test_is_testfix(self):
227 testfix = True
228 self.assertEqual('[testfix]', get_testfix_clause(testfix))
229
230
231class TestGetQaClause(unittest.TestCase):
232 """Tests for `get_qa_clause`"""
233
234 def test_no_bugs_no_option_given(self):
235 bugs = None
236 no_qa = False
237 incr = False
238 self.assertRaises(MissingBugsError, get_qa_clause, bugs, no_qa,
239 incr)
240
241 def test_bugs_noqa_option_given(self):
242 bug1 = FakeBug(20)
243 no_qa = True
244 incr = False
245 self.assertEqual('[no-qa]',
246 get_qa_clause([bug1], no_qa, incr))
247
248 def test_no_bugs_noqa_option_given(self):
249 bugs = None
250 no_qa = True
251 incr = False
252 self.assertEqual('[no-qa]',
253 get_qa_clause(bugs, no_qa, incr))
254
255 def test_bugs_no_option_given(self):
256 bug1 = FakeBug(20)
257 no_qa = False
258 incr = False
259 self.assertEqual('',
260 get_qa_clause([bug1], no_qa, incr))
261
262 def test_bugs_incr_option_given(self):
263 bug1 = FakeBug(20)
264 no_qa = False
265 incr = True
266 self.assertEqual('[incr]',
267 get_qa_clause([bug1], no_qa, incr))
268
269 def test_no_bugs_incr_option_given(self):
270 bugs = None
271 no_qa = False
272 incr = True
273 self.assertRaises(MissingBugsIncrementalError,
274 get_qa_clause, bugs, no_qa, incr)
275
276
71class TestGetReviewerHandle(unittest.TestCase):277class TestGetReviewerHandle(unittest.TestCase):
72 """Tests for `get_reviewer_handle`."""278 """Tests for `get_reviewer_handle`."""
73279