Merge lp:~matsubara/tarmac/bundle-lander-fixes into lp:~launchpad/tarmac/lp-tarmac

Proposed by Diogo Matsubara
Status: Merged
Approved by: Diogo Matsubara
Approved revision: 391
Merged at revision: 387
Proposed branch: lp:~matsubara/tarmac/bundle-lander-fixes
Merge into: lp:~launchpad/tarmac/lp-tarmac
Diff against target: 286 lines (+80/-61)
5 files modified
tarmac/bin/commands.py (+7/-11)
tarmac/plugins/bundlecommand.py (+11/-16)
tarmac/plugins/tests/test_bundlecommand.py (+41/-21)
tarmac/tests/mock.py (+4/-1)
tarmac/tests/test_commands.py (+17/-12)
To merge this branch: bzr merge lp:~matsubara/tarmac/bundle-lander-fixes
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+44040@code.launchpad.net

Commit message

Fixes issue with double notification in merge proposals and two notifications for each event. Add XXX pointing to bug 691563 as this is a temporary solution.

Description of the change

This branch fix the issue shown in https://code.launchpad.net/~matsubara/launchpad/test-ghost-update/+merge/43919 where there are duplicated comments and two notifications for the same event.

Due to the way the merge_branches() generator works, it yields twice, once for each pre and post commit hooks, so this branch fixes the duplicated comments by using a set() to collect the proposals the bundle lander will work on.

To fix the two notification for the same event problem, I moved an update_proposal_with_error() call in bin/commands.py to plugins/bundlecommand.py, so now it's the plugins responsibility t, in case o failure, update the merge proposal and uncommit and revert changes done. Since the plugin doesn't raise an error anymore in case of failure, I removed the test that was testing that and added more tests to test_bundlecommand.py to cover for its new responsibility.

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Cool, thank you.

In the abstract, I feel like we ought to keep the try: except handling in commands.py (cmd_bundle_merge). If a tarmac_bundle_merge hook raises an error, we should still inform the merge proposal about it. Our hook no longer raises the usual errors--but if it raises an unexpected error, isn't that even more important to report? And keeping the logic that reverts the branch in commands.py better matches the basic code structure. Moreover, I think we could change the exception we raise in the hook to have all the collected information that we want in the email, so that we could follow the usual tarmac pattern, letting cmd_bundle_merge handle the reversion, and still get the emails we want.

I'd like that change, but given your schedule and our schedule, that change requires time we don't have. We can put it in the category of "things to revisit when proposing a merge to trunk" or maybe "things for Gary or Benji or Ursula to look at next week." Maybe just an XXX with a quick explanation of the issue? A bug would be extra credit. :-)

Quick tweaks:

=== modified file 'tarmac/bin/commands.py'
    # proposal) tuple twice, once for each pre and pos commit hook.
pos -> post

=== modified file 'tarmac/plugins/bundlecommand.py'
    # Uncommit and revert changes up to the point where target
    # was before the merge.
Per my comments above, please add an additional comment explaining that we normally expect the tarmac command to do this, We are doing it here because of the emails, but we hope to change it in the future.

tarmac/tests/test_commands.py : maybe comment out the test, rather than removing it, per my comments above?

I'll approve this, so you can commit with just these small changes, but if you disagree with my comments about the larger future changes, please raise your concerns, here or on another channel (in which case you or I can summarize here afterwards).

Thank you

review: Approve
Revision history for this message
Diogo Matsubara (matsubara) wrote :

Hi Gary,

I filed bug 691563 for the XXX and added comments in the code. I fixed the typo as well and commented out the test that I had previously removed.

To answer your concerns and add some more of my own:

If tarmac_bundle_merge hook raises an unexpected error, the try/except block removed wouldn't be able do anything about it. It would be caught by the upper try/except and re-raised. Basically tarmac would crash and burn in that situation. I agree that keeping the logic to revert the branch in commands.py feels more correct than in the hook.

There's one thing not clear about your proposal though. If we change the exception raised in the hook to collect the information, put the try/except and revert code back in commands.py, who would be responsible for sending the email? Would that still be done in the hook code, but the proposal updates done outside of it? Is that the right understanding?

Thanks for the review.

Revision history for this message
Diogo Matsubara (matsubara) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tarmac/bin/commands.py'
2--- tarmac/bin/commands.py 2010-12-15 16:52:26 +0000
3+++ tarmac/bin/commands.py 2010-12-17 14:22:15 +0000
4@@ -269,24 +269,20 @@
5 # revno to restore to if bundle merge fails.
6 restore_revno = target.tree.branch.revno() + 1
7 results = target.merge_branches()
8- collected_proposals = []
9+ # Use a set to collect the proposals. We don't want
10+ # duplicates since results generator will return the (source,
11+ # proposal) tuple twice, once for each pre and post commit hook.
12+ collected_proposals = set()
13 while True:
14 try:
15 (hook, source, proposal) = results.next()
16- collected_proposals.append((source, proposal))
17+ collected_proposals.add((source, proposal))
18 self._fire_merge_hook(
19 results, hook, target, source, proposal)
20 except StopIteration:
21 break
22- try:
23- tarmac_hooks['tarmac_bundle_merge'].fire(
24- self, target, collected_proposals)
25- except TarmacMergeError, failure:
26- for source, proposal in collected_proposals:
27- target.update_proposal_with_error(proposal, failure)
28- # Uncommit and revert changes up to the point where target
29- # was before the merge.
30- target.uncommit_and_revert(restore_revno)
31+ tarmac_hooks['tarmac_bundle_merge'].fire(
32+ self, target, restore_revno, collected_proposals)
33
34 # This except is here because we need the else and can't have it
35 # without an except as well.
36
37=== modified file 'tarmac/plugins/bundlecommand.py'
38--- tarmac/plugins/bundlecommand.py 2010-12-16 03:58:15 +0000
39+++ tarmac/plugins/bundlecommand.py 2010-12-17 14:22:15 +0000
40@@ -128,7 +128,7 @@
41 smtp_connection = SMTPConnection(bzrlib.config.GlobalConfig())
42 self._smtp_connection = smtp_connection
43
44- def run(self, command, target, collected_proposals):
45+ def run(self, command, target, restore_revno, collected_proposals):
46 """Plugin for running the registered bundle verify command on target.
47
48 This is very similar to the Command plugin, but instead of running the
49@@ -169,19 +169,21 @@
50 if return_code != 0:
51 for source, proposal in collected_proposals:
52 self.do_failed(
53- start_time, end_time, proposal, output)
54- message = u'Test command "%s" failed.' % self.verify_command
55- comment = (
56- u'Bundle lander failed to land proposals: %s' %
57- collected_proposals)
58- raise VerifyCommandFailed(message, comment)
59+ target, start_time, end_time, proposal, output)
60+ # XXX matsubara 2010-12-17: The command code should be
61+ # handling this, but due to the way we want emails formatted
62+ # and sent, this is done here for now.
63+ # See bug 691563 for more details.
64+ # Uncommit and revert changes up to the point where target
65+ # was before the merge.
66+ target.uncommit_and_revert(restore_revno)
67 else:
68 self.logger.info('==========')
69 self.logger.info(output.read())
70 finally:
71 output.close()
72
73- def do_failed(self, start_time, end_time, proposal, output):
74+ def do_failed(self, target, start_time, end_time, proposal, output):
75 '''Perform failure tests.
76
77 In this case, the full failure is emailed to the proposal subscribers,
78@@ -235,14 +237,7 @@
79 'target': proposal.target_branch.display_name,
80 'msg': str(message)})
81
82- subject = u'Re: [Merge] %(source)s into %(target)s' % {
83- "source": proposal.source_branch.display_name,
84- "target": proposal.target_branch.display_name}
85-
86- proposal.createComment(subject=subject,
87- content=comment)
88- proposal.setStatus(status=u'Needs review')
89- proposal.lp_save()
90+ target.update_proposal(proposal, comment)
91
92 def get_subscribers_emails(self, proposal):
93 """Return a list of the proposal subscribers emails."""
94
95=== modified file 'tarmac/plugins/tests/test_bundlecommand.py'
96--- tarmac/plugins/tests/test_bundlecommand.py 2010-12-16 03:58:15 +0000
97+++ tarmac/plugins/tests/test_bundlecommand.py 2010-12-17 14:22:15 +0000
98@@ -11,7 +11,7 @@
99 from tarmac.bin.registry import CommandRegistry
100 from tarmac.plugins.bundlecommand import (
101 BundleCommand, TestResultFormatter, VerifyCommandFailed)
102-from tarmac.tests import TarmacTestCase
103+from tarmac.tests import BranchTestCase
104 from tarmac.tests.test_commands import FakeCommand
105 from tarmac.tests.mock import Thing, MockMergeProposal
106
107@@ -55,10 +55,6 @@
108
109 return TestResultFormatter(start_time, end_time, result, proposal)
110
111- def lp_save(self, *args, **kwargs):
112- """Dummy lp_save method."""
113- pass
114-
115
116 class TestTestResultFormatter(TestCase, Helpers):
117 """Tests for `TestResultFormatter`."""
118@@ -176,14 +172,15 @@
119 doctest.ELLIPSIS))
120
121
122-class BundleCommandTests(TarmacTestCase, Helpers):
123+class BundleCommandTests(BranchTestCase, Helpers):
124 """Test the BundleCommand."""
125
126 def setUp(self):
127 """Set up additional data we need for tests."""
128 super(BundleCommandTests, self).setUp()
129- self.collected_proposals = [
130- (Thing(),
131+ # Use a set here because that's the object passed to BundleCommand.
132+ self.collected_proposals = set(
133+ [(Thing(),
134 MockMergeProposal(
135 Thing(
136 display_name='lp:project/source',
137@@ -214,8 +211,8 @@
138 votes=[
139 Thing(reviewer=Thing(
140 preferred_email_address=Thing(
141- email='foo.bar@example.com')))])),
142- ]
143+ email='foo.bar@example.com')))]))]
144+ )
145 # Patch the plugin smtp_connection so emails are never actually sent.
146 email_log = []
147 smtp_connection = LoggingSMTPConnection(email_log)
148@@ -229,7 +226,7 @@
149 bundle_verify_command="/bin/true"),
150 tree=Thing(abspath=os.path.abspath))
151 self.plugin.run(
152- command=self.command, target=target,
153+ command=self.command, target=target, restore_revno=123,
154 collected_proposals=self.collected_proposals)
155
156 def test_run_failure(self):
157@@ -240,16 +237,28 @@
158 proposal1, proposal2 = self.collected_proposals
159 self.assertEqual(proposal1[1].queue_status, "Approved")
160 self.assertEqual(proposal2[1].queue_status, "Approved")
161-
162- target = Thing(config=Thing(
163- bundle_verify_command="/bin/false"),
164- tree=Thing(abspath=os.path.abspath))
165- self.assertRaises(VerifyCommandFailed,
166- self.plugin.run,
167- command=self.command, target=target,
168+ # Proposals have no comments.
169+ self.assertEqual(proposal1[1].all_comments, [])
170+ self.assertEqual(proposal2[1].all_comments, [])
171+
172+ target = self.branch1
173+ # Include the config to run the bundle verify command.
174+ setattr(target.config, 'bundle_verify_command', '/bin/false')
175+ # Set a fake method to target, so we can make sure it's called when
176+ # the plugin fails to run.
177+ def fake_uncommit_and_revert(restore_revno):
178+ setattr(target, 'fake_revno', restore_revno)
179+ target.uncommit_and_revert = fake_uncommit_and_revert
180+
181+ # Run the plugin.
182+ self.plugin.run(
183+ command=self.command, target=target, restore_revno=123,
184 collected_proposals=self.collected_proposals)
185
186 # There are two emails in the log. One for each proposal.
187+ self.plugin._smtp_connection._log.sort(
188+ key=lambda email: email.get('Subject'))
189+ self.assertTrue(len(self.plugin._smtp_connection._log) == 2)
190 email = self.plugin._smtp_connection._log.pop()
191 self.assertEqual(
192 email.get('Subject'),
193@@ -259,19 +268,30 @@
194 email.get('Subject'),
195 'Test results: lp:project/source => lp:project: FAILURE')
196
197- # Proposals status is updated.
198+ # Proposals statuses are updated.
199 proposal1, proposal2 = self.collected_proposals
200 self.assertEqual(proposal1[1].queue_status, "Needs review")
201 self.assertEqual(proposal2[1].queue_status, "Needs review")
202+ # Proposals have comments.
203+ self.assertEqual(len(proposal1[1].all_comments), 1)
204+ self.assertEqual(len(proposal2[1].all_comments), 1)
205+
206+ # Check the patched target.uncommit_and_revert() was run.
207+ self.assertEqual(target.fake_revno, 123)
208
209 def test_send_report_email(self):
210 """Test send_report_email()."""
211 formatter = self.make_formatter()
212
213- emails = self.plugin.get_subscribers_emails(
214- self.collected_proposals[0][1])
215+ proposals = [
216+ proposal for source, proposal in self.collected_proposals]
217+ proposals.sort(
218+ key=lambda proposal: proposal.source_branch.display_name)
219+ emails = self.plugin.get_subscribers_emails(proposals[0])
220+
221 self.plugin.send_report_email(
222 emails, formatter, 'some gzip content')
223+ self.assertTrue(len(self.plugin._smtp_connection._log) == 1)
224 email = self.plugin._smtp_connection._log.pop()
225 self.assertEqual(
226 email.get('To'), 'foo@example.com, foo.bar@example.com')
227
228=== modified file 'tarmac/tests/mock.py'
229--- tarmac/tests/mock.py 2010-12-16 03:58:15 +0000
230+++ tarmac/tests/mock.py 2010-12-17 14:22:15 +0000
231@@ -62,7 +62,7 @@
232 """A mock LP merge proposal."""
233
234 def __init__(self, source_branch, target_branch, status, votes=()):
235- self.createComment = self.lp_save
236+ self.all_comments = []
237 self.queue_status = status
238 self.source_branch = source_branch
239 self.target_branch = target_branch
240@@ -74,6 +74,9 @@
241 def setStatus(self, status="Needs review"):
242 self.queue_status = status
243
244+ def createComment(self, subject=None, content=None):
245+ self.all_comments.append((subject,content))
246+
247
248 class cmd_mock(TarmacCommand):
249 '''A mock command.'''
250
251=== modified file 'tarmac/tests/test_commands.py'
252--- tarmac/tests/test_commands.py 2010-12-16 03:58:15 +0000
253+++ tarmac/tests/test_commands.py 2010-12-17 14:22:15 +0000
254@@ -444,15 +444,20 @@
255 self.assertIsInstance(self.error, TarmacMergeError)
256 self.assertEqual(self.error.comment, plugin.long_message)
257
258- def test_tarmac_bundle_merge_hookpoint_errors_are_handled(self):
259- # Setup
260- self.approve_all_proposals()
261- plugin = self.install_merge_error_injecting_plugin('tarmac_bundle_merge')
262-
263- # Act
264- self.command.run(launchpad=self.launchpad)
265-
266- # Verify
267- self.assertTrue(plugin.has_run)
268- self.assertIsInstance(self.error, TarmacMergeError)
269- self.assertEqual(self.error.comment, plugin.long_message)
270+# XXX matsubara 2010-12-17: Test commented out because we're not handling
271+# errors in the command itself but in the plugin. This is a temporary hack
272+# and we want to change the way the bundle merge plugin and the command
273+# interacts in the near future. See bug 691563 for more details.
274+# def test_tarmac_bundle_merge_hookpoint_errors_are_handled(self):
275+# # Setup
276+# self.approve_all_proposals()
277+# plugin = self.install_merge_error_injecting_plugin('tarmac_bundle_merge')
278+#
279+# # Act
280+# self.command.run(launchpad=self.launchpad)
281+#
282+# # Verify
283+# self.assertTrue(plugin.has_run)
284+# self.assertIsInstance(self.error, TarmacMergeError)
285+# self.assertEqual(self.error.comment, plugin.long_message)
286+#

Subscribers

People subscribed via source and target branches

to all changes: