Merge lp:~cjwatson/launchpad/consistent-bmp-events into lp:launchpad
- consistent-bmp-events
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17807 | ||||
Proposed branch: | lp:~cjwatson/launchpad/consistent-bmp-events | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
863 lines (+268/-96) 17 files modified
lib/lp/code/browser/branchmergeproposal.py (+1/-12) lib/lp/code/browser/tests/test_branchmergeproposal.py (+140/-6) lib/lp/code/configure.zcml (+2/-2) lib/lp/code/event/branchmergeproposal.py (+1/-14) lib/lp/code/interfaces/event.py (+0/-10) lib/lp/code/mail/codehandler.py (+9/-7) lib/lp/code/mail/tests/test_codehandler.py (+21/-1) lib/lp/code/model/branchmergeproposal.py (+0/-4) lib/lp/code/model/branchmergeproposaljob.py (+1/-0) lib/lp/code/model/tests/test_branchmergeproposal.py (+7/-7) lib/lp/code/model/tests/test_codereviewcomment.py (+3/-2) lib/lp/code/model/tests/test_codereviewkarma.py (+12/-5) lib/lp/code/model/tests/test_gitrepository.py (+17/-3) lib/lp/code/subscribers/branchmergeproposal.py (+13/-8) lib/lp/code/subscribers/karma.py (+15/-6) lib/lp/codehosting/scanner/tests/test_mergedetection.py (+10/-2) lib/lp/testing/__init__.py (+16/-7) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/consistent-bmp-events | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+273325@code.launchpad.net |
Commit message
Remove BranchMergeProp
Description of the change
I'd like to hook merge proposal webhooks into the Zope events that are already generated for several kinds of changes to merge proposals, but this is complicated by unnecessarily-
To get this right, I exhaustively checked all the possible calling paths to BranchMergeProp
BranchMergeProp
This also has the useful side-effect of sending mail notifications for transitions to SUPERSEDED, per bug 716169. I made this consistent and added tests for it.
William Grant (wgrant) : | # |
Colin Watson (cjwatson) : | # |
Preview Diff
1 | === modified file 'lib/lp/code/browser/branchmergeproposal.py' |
2 | --- lib/lp/code/browser/branchmergeproposal.py 2015-07-21 16:30:15 +0000 |
3 | +++ lib/lp/code/browser/branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
4 | @@ -161,18 +161,6 @@ |
5 | return decorator |
6 | |
7 | |
8 | -def update_and_notify(func): |
9 | - """Decorate an action to update from a form and send a notification.""" |
10 | - |
11 | - @notify |
12 | - def decorator(view, action, data): |
13 | - result = func(view, action, data) |
14 | - form.applyChanges( |
15 | - view.context, view.form_fields, data, view.adapters) |
16 | - return result |
17 | - return decorator |
18 | - |
19 | - |
20 | class BranchMergeCandidateView(LaunchpadView): |
21 | """Provides a small fragment of landing targets""" |
22 | |
23 | @@ -1028,6 +1016,7 @@ |
24 | return dict(item for item in items if item[1] is not UNSET) |
25 | |
26 | @action('Resubmit', name='resubmit') |
27 | + @notify |
28 | def resubmit_action(self, action, data): |
29 | """Resubmit this proposal.""" |
30 | try: |
31 | |
32 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' |
33 | --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-10-01 10:25:19 +0000 |
34 | +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
35 | @@ -12,6 +12,7 @@ |
36 | ) |
37 | from difflib import unified_diff |
38 | |
39 | +from lazr.lifecycle.event import ObjectModifiedEvent |
40 | from lazr.restful.interfaces import IJSONRequestCache |
41 | import pytz |
42 | import simplejson |
43 | @@ -20,11 +21,16 @@ |
44 | Tag, |
45 | ) |
46 | from testtools.matchers import ( |
47 | + MatchesListwise, |
48 | MatchesRegex, |
49 | + MatchesStructure, |
50 | Not, |
51 | ) |
52 | import transaction |
53 | -from zope.component import getMultiAdapter |
54 | +from zope.component import ( |
55 | + getMultiAdapter, |
56 | + getUtility, |
57 | + ) |
58 | from zope.security.interfaces import Unauthorized |
59 | from zope.security.proxy import removeSecurityProxy |
60 | |
61 | @@ -47,6 +53,11 @@ |
62 | BranchMergeProposalStatus, |
63 | CodeReviewVote, |
64 | ) |
65 | +from lp.code.interfaces.branchmergeproposal import ( |
66 | + IBranchMergeProposal, |
67 | + IMergeProposalNeedsReviewEmailJobSource, |
68 | + IMergeProposalUpdatedEmailJobSource, |
69 | + ) |
70 | from lp.code.model.diff import PreviewDiff |
71 | from lp.code.tests.helpers import ( |
72 | add_revision_to_branch, |
73 | @@ -66,6 +77,7 @@ |
74 | from lp.services.webapp.servers import LaunchpadTestRequest |
75 | from lp.testing import ( |
76 | BrowserTestCase, |
77 | + EventRecorder, |
78 | feature_flags, |
79 | login_person, |
80 | monkey_patch, |
81 | @@ -168,6 +180,27 @@ |
82 | browser = self.getViewBrowser(bmp.merge_source, '+index') |
83 | self.assertThat(browser.contents, HTMLContains(revision_number)) |
84 | |
85 | + def test_notifies_modification(self): |
86 | + bmp = self.makeBranchMergeProposal() |
87 | + registrant = bmp.registrant |
88 | + login_person(registrant) |
89 | + browser = self.getViewBrowser(bmp, '+merged', user=bmp.registrant) |
90 | + browser.getControl(self.merged_revision_text).value = str( |
91 | + self.arbitrary_revisions[2]) |
92 | + with EventRecorder(propagate=True) as recorder: |
93 | + browser.getControl('Mark as Merged').click() |
94 | + login_person(registrant) |
95 | + events = [ |
96 | + event for event in recorder.events |
97 | + if isinstance(event, ObjectModifiedEvent) and |
98 | + IBranchMergeProposal.providedBy(event.object)] |
99 | + self.assertThat(events, MatchesListwise([ |
100 | + MatchesStructure( |
101 | + object_before_modification=MatchesStructure.byEquality( |
102 | + queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS), |
103 | + object=MatchesStructure.byEquality( |
104 | + queue_status=BranchMergeProposalStatus.MERGED))])) |
105 | + |
106 | |
107 | class TestBranchMergeProposalMergedViewBzr( |
108 | TestBranchMergeProposalMergedViewMixin, BrowserTestCase): |
109 | @@ -918,6 +951,50 @@ |
110 | simplejson.loads(view.form_result)) |
111 | |
112 | |
113 | +class TestBranchMergeProposalRequestReviewViewMixin: |
114 | + """Test `BranchMergeProposalRequestReviewView`.""" |
115 | + |
116 | + layer = DatabaseFunctionalLayer |
117 | + |
118 | + def test_notifies_modification(self): |
119 | + bmp = self.makeBranchMergeProposal() |
120 | + registrant = bmp.registrant |
121 | + self.factory.makePerson(name='test-reviewer') |
122 | + login_person(registrant) |
123 | + browser = self.getViewBrowser( |
124 | + bmp, '+request-review', user=bmp.registrant) |
125 | + browser.getControl('Reviewer').value = 'test-reviewer' |
126 | + with EventRecorder(propagate=True) as recorder: |
127 | + browser.getControl('Request Review').click() |
128 | + login_person(registrant) |
129 | + events = [ |
130 | + event for event in recorder.events |
131 | + if isinstance(event, ObjectModifiedEvent) and |
132 | + IBranchMergeProposal.providedBy(event.object)] |
133 | + self.assertThat(events, MatchesListwise([ |
134 | + MatchesStructure( |
135 | + object_before_modification=MatchesStructure.byEquality( |
136 | + queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS), |
137 | + object=MatchesStructure.byEquality( |
138 | + queue_status=BranchMergeProposalStatus.NEEDS_REVIEW))])) |
139 | + |
140 | + |
141 | +class TestBranchMergeProposalRequestReviewViewBzr( |
142 | + TestBranchMergeProposalRequestReviewViewMixin, BrowserTestCase): |
143 | + """Test `BranchMergeProposalRequestReviewView` for Bazaar.""" |
144 | + |
145 | + def makeBranchMergeProposal(self): |
146 | + return self.factory.makeBranchMergeProposal() |
147 | + |
148 | + |
149 | +class TestBranchMergeProposalRequestReviewViewGit( |
150 | + TestBranchMergeProposalRequestReviewViewMixin, BrowserTestCase): |
151 | + """Test `BranchMergeProposalRequestReviewView` for Git.""" |
152 | + |
153 | + def makeBranchMergeProposal(self): |
154 | + return self.factory.makeBranchMergeProposalForGit() |
155 | + |
156 | + |
157 | class TestBranchMergeProposalResubmitViewMixin: |
158 | """Test BranchMergeProposalResubmitView.""" |
159 | |
160 | @@ -998,13 +1075,54 @@ |
161 | ' <a href=.*>a similar merge proposal</a> is already active.')) |
162 | self.assertEqual(BrowserNotificationLevel.ERROR, notification.level) |
163 | |
164 | + def test_notifies_modification(self): |
165 | + view = self.createView() |
166 | + with EventRecorder(propagate=True) as recorder: |
167 | + self.resubmitDefault(view) |
168 | + events = [ |
169 | + event for event in recorder.events |
170 | + if isinstance(event, ObjectModifiedEvent) and |
171 | + IBranchMergeProposal.providedBy(event.object)] |
172 | + self.assertThat(events, MatchesListwise([ |
173 | + MatchesStructure( |
174 | + object_before_modification=MatchesStructure.byEquality( |
175 | + queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS), |
176 | + object=MatchesStructure.byEquality( |
177 | + queue_status=BranchMergeProposalStatus.SUPERSEDED))])) |
178 | + |
179 | + def test_mail_jobs(self): |
180 | + """Resubmitting sends mail about the changed old MP and the new MP.""" |
181 | + view = self.createView() |
182 | + new_proposal = self.resubmitDefault(view) |
183 | + updated_job_source = getUtility(IMergeProposalUpdatedEmailJobSource) |
184 | + [updated_job] = list( |
185 | + removeSecurityProxy(updated_job_source).iterReady()) |
186 | + self.assertThat(updated_job, MatchesStructure.byEquality( |
187 | + branch_merge_proposal=view.context, |
188 | + metadata={ |
189 | + 'delta_text': ' Status: Work in progress => Superseded', |
190 | + 'editor': view.context.registrant.name, |
191 | + })) |
192 | + needs_review_job_source = getUtility( |
193 | + IMergeProposalNeedsReviewEmailJobSource) |
194 | + [needs_review_job] = list( |
195 | + removeSecurityProxy(needs_review_job_source).iterReady()) |
196 | + self.assertThat(needs_review_job, MatchesStructure.byEquality( |
197 | + branch_merge_proposal=new_proposal, |
198 | + metadata={})) |
199 | + |
200 | |
201 | class TestBranchMergeProposalResubmitViewBzr( |
202 | TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory): |
203 | """Test BranchMergeProposalResubmitView for Bazaar.""" |
204 | |
205 | def _makeBranchMergeProposal(self): |
206 | - return self.factory.makeBranchMergeProposal() |
207 | + bmp = self.factory.makeBranchMergeProposal() |
208 | + # Pretend to have a revision so that |
209 | + # BranchMergeProposalJobDerived.iterReady will consider this |
210 | + # proposal. |
211 | + removeSecurityProxy(bmp.source_branch).revision_count = 1 |
212 | + return bmp |
213 | |
214 | @staticmethod |
215 | def _getFormValues(source_branch, target_branch, prerequisite_branch, |
216 | @@ -1366,8 +1484,8 @@ |
217 | self.assertThat(browser.contents, HTMLContains(expected_meta)) |
218 | |
219 | |
220 | -class TestBranchMergeProposalChangeStatusOptions(TestCaseWithFactory): |
221 | - """Test the status vocabulary generated for then +edit-status view.""" |
222 | +class TestBranchMergeProposalChangeStatusView(TestCaseWithFactory): |
223 | + """Test the +edit-status view.""" |
224 | |
225 | layer = DatabaseFunctionalLayer |
226 | |
227 | @@ -1378,10 +1496,10 @@ |
228 | self.proposal = self.factory.makeBranchMergeProposal( |
229 | registrant=self.user) |
230 | |
231 | - def _createView(self): |
232 | + def _createView(self, form=None): |
233 | # Construct the view and initialize it. |
234 | view = BranchMergeProposalChangeStatusView( |
235 | - self.proposal, LaunchpadTestRequest()) |
236 | + self.proposal, LaunchpadTestRequest(form=form)) |
237 | view.initialize() |
238 | return view |
239 | |
240 | @@ -1454,6 +1572,22 @@ |
241 | self.assertAllStatusesAvailable( |
242 | user=self.proposal.target_branch.owner) |
243 | |
244 | + def test_notifies_modification(self): |
245 | + view = self._createView(form={'revno': '1'}) |
246 | + with EventRecorder(propagate=True) as recorder: |
247 | + view.update_action.success( |
248 | + {'queue_status': BranchMergeProposalStatus.NEEDS_REVIEW}) |
249 | + events = [ |
250 | + event for event in recorder.events |
251 | + if isinstance(event, ObjectModifiedEvent) and |
252 | + IBranchMergeProposal.providedBy(event.object)] |
253 | + self.assertThat(events, MatchesListwise([ |
254 | + MatchesStructure( |
255 | + object_before_modification=MatchesStructure.byEquality( |
256 | + queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS), |
257 | + object=MatchesStructure.byEquality( |
258 | + queue_status=BranchMergeProposalStatus.NEEDS_REVIEW))])) |
259 | + |
260 | |
261 | class TestCommentAttachmentRendering(TestCaseWithFactory): |
262 | """Test diff attachments are rendered correctly.""" |
263 | |
264 | === modified file 'lib/lp/code/configure.zcml' |
265 | --- lib/lp/code/configure.zcml 2015-09-24 13:44:02 +0000 |
266 | +++ lib/lp/code/configure.zcml 2015-10-06 15:19:46 +0000 |
267 | @@ -295,8 +295,8 @@ |
268 | handler="lp.code.subscribers.karma.branch_merge_proposed"/> |
269 | <subscriber |
270 | for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal |
271 | - lp.code.interfaces.event.IBranchMergeProposalStatusChangeEvent" |
272 | - handler="lp.code.subscribers.karma.branch_merge_status_changed"/> |
273 | + lazr.lifecycle.interfaces.IObjectModifiedEvent" |
274 | + handler="lp.code.subscribers.karma.branch_merge_modified"/> |
275 | |
276 | <!-- hierarchy --> |
277 | |
278 | |
279 | === modified file 'lib/lp/code/event/branchmergeproposal.py' |
280 | --- lib/lp/code/event/branchmergeproposal.py 2015-07-08 16:05:11 +0000 |
281 | +++ lib/lp/code/event/branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
282 | @@ -1,4 +1,4 @@ |
283 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
284 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
285 | # GNU Affero General Public License version 3 (see the file LICENSE). |
286 | |
287 | """Event implementation classes for branch merge proposal events.""" |
288 | @@ -6,7 +6,6 @@ |
289 | __metaclass__ = type |
290 | __all__ = [ |
291 | 'BranchMergeProposalNeedsReviewEvent', |
292 | - 'BranchMergeProposalStatusChangeEvent', |
293 | 'NewBranchMergeProposalEvent', |
294 | 'NewCodeReviewCommentEvent', |
295 | 'ReviewerNominatedEvent', |
296 | @@ -17,24 +16,12 @@ |
297 | |
298 | from lp.code.interfaces.event import ( |
299 | IBranchMergeProposalNeedsReviewEvent, |
300 | - IBranchMergeProposalStatusChangeEvent, |
301 | INewBranchMergeProposalEvent, |
302 | INewCodeReviewCommentEvent, |
303 | IReviewerNominatedEvent, |
304 | ) |
305 | |
306 | |
307 | -@implementer(IBranchMergeProposalStatusChangeEvent) |
308 | -class BranchMergeProposalStatusChangeEvent(ObjectEvent): |
309 | - """See `IBranchMergeProposalStatusChangeEvent`.""" |
310 | - |
311 | - def __init__(self, proposal, user, from_state, to_state): |
312 | - ObjectEvent.__init__(self, proposal) |
313 | - self.user = user |
314 | - self.from_state = from_state |
315 | - self.to_state = to_state |
316 | - |
317 | - |
318 | @implementer(INewBranchMergeProposalEvent) |
319 | class NewBranchMergeProposalEvent(ObjectEvent): |
320 | """A new merge has been proposed.""" |
321 | |
322 | === modified file 'lib/lp/code/interfaces/event.py' |
323 | --- lib/lp/code/interfaces/event.py 2015-06-06 08:49:54 +0000 |
324 | +++ lib/lp/code/interfaces/event.py 2015-10-06 15:19:46 +0000 |
325 | @@ -5,7 +5,6 @@ |
326 | |
327 | __metaclass__ = type |
328 | __all__ = [ |
329 | - 'IBranchMergeProposalStatusChangeEvent', |
330 | 'IBranchMergeProposalNeedsReviewEvent', |
331 | 'IGitRefsUpdatedEvent', |
332 | 'INewBranchMergeProposalEvent', |
333 | @@ -15,15 +14,6 @@ |
334 | |
335 | |
336 | from zope.component.interfaces import IObjectEvent |
337 | -from zope.interface import Attribute |
338 | - |
339 | - |
340 | -class IBranchMergeProposalStatusChangeEvent(IObjectEvent): |
341 | - """A merge proposal has changed state.""" |
342 | - |
343 | - user = Attribute("The user who updated the proposal.") |
344 | - from_state = Attribute("The previous queue_status.") |
345 | - to_state = Attribute("The updated queue_status.") |
346 | |
347 | |
348 | class INewBranchMergeProposalEvent(IObjectEvent): |
349 | |
350 | === modified file 'lib/lp/code/mail/codehandler.py' |
351 | --- lib/lp/code/mail/codehandler.py 2015-09-28 17:38:45 +0000 |
352 | +++ lib/lp/code/mail/codehandler.py 2015-10-06 15:19:46 +0000 |
353 | @@ -1,4 +1,4 @@ |
354 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
355 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
356 | # GNU Affero General Public License version 3 (see the file LICENSE). |
357 | |
358 | __metaclass__ = type |
359 | @@ -14,6 +14,7 @@ |
360 | from zope.interface import implementer |
361 | from zope.security.interfaces import Unauthorized |
362 | |
363 | +from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta |
364 | from lp.code.enums import CodeReviewVote |
365 | from lp.code.errors import UserNotBranchReviewer |
366 | from lp.code.interfaces.branchmergeproposal import IBranchMergeProposalGetter |
367 | @@ -258,12 +259,13 @@ |
368 | def processCommands(self, context, commands): |
369 | """Process the various merge proposal commands against the context.""" |
370 | processing_errors = [] |
371 | - |
372 | - for command in commands: |
373 | - try: |
374 | - command.execute(context) |
375 | - except EmailProcessingError as error: |
376 | - processing_errors.append((error, command)) |
377 | + with BranchMergeProposalNoPreviewDiffDelta.monitor( |
378 | + context.merge_proposal): |
379 | + for command in commands: |
380 | + try: |
381 | + command.execute(context) |
382 | + except EmailProcessingError as error: |
383 | + processing_errors.append((error, command)) |
384 | |
385 | if len(processing_errors) > 0: |
386 | errors, commands = zip(*processing_errors) |
387 | |
388 | === modified file 'lib/lp/code/mail/tests/test_codehandler.py' |
389 | --- lib/lp/code/mail/tests/test_codehandler.py 2015-07-21 09:04:01 +0000 |
390 | +++ lib/lp/code/mail/tests/test_codehandler.py 2015-10-06 15:19:46 +0000 |
391 | @@ -1,4 +1,4 @@ |
392 | -# Copyright 2009-2014 Canonical Ltd. This software is licensed under the |
393 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
394 | # GNU Affero General Public License version 3 (see the file LICENSE). |
395 | |
396 | """Testing the CodeHandler.""" |
397 | @@ -7,6 +7,7 @@ |
398 | |
399 | from textwrap import dedent |
400 | |
401 | +from lazr.lifecycle.event import ObjectModifiedEvent |
402 | from storm.store import Store |
403 | import transaction |
404 | from zope.security.management import setSecurityPolicy |
405 | @@ -17,6 +18,7 @@ |
406 | CodeReviewNotificationLevel, |
407 | CodeReviewVote, |
408 | ) |
409 | +from lp.code.event.branchmergeproposal import NewCodeReviewCommentEvent |
410 | from lp.code.mail.codehandler import ( |
411 | AddReviewerEmailCommand, |
412 | CodeEmailCommands, |
413 | @@ -425,6 +427,24 @@ |
414 | mail['from']) |
415 | self.assertEqual(0, bmp.all_comments.count()) |
416 | |
417 | + def test_notifies_modification(self): |
418 | + """Changes to the merge proposal itself trigger events.""" |
419 | + mail = self.factory.makeSignedMessage(body=' merge approved') |
420 | + bmp = self.factory.makeBranchMergeProposal() |
421 | + email_addr = bmp.address |
422 | + switch_dbuser(config.processmail.dbuser) |
423 | + login_person(bmp.merge_target.owner) |
424 | + _, events = self.assertNotifies( |
425 | + [ObjectModifiedEvent, NewCodeReviewCommentEvent], False, |
426 | + self.code_handler.process, mail, email_addr, None) |
427 | + self.assertEqual(bmp, events[0].object) |
428 | + self.assertEqual( |
429 | + BranchMergeProposalStatus.WORK_IN_PROGRESS, |
430 | + events[0].object_before_modification.queue_status) |
431 | + self.assertEqual( |
432 | + BranchMergeProposalStatus.CODE_APPROVED, |
433 | + events[0].object.queue_status) |
434 | + |
435 | |
436 | class TestVoteEmailCommand(TestCase): |
437 | """Test the vote and tag processing of the VoteEmailCommand.""" |
438 | |
439 | === modified file 'lib/lp/code/model/branchmergeproposal.py' |
440 | --- lib/lp/code/model/branchmergeproposal.py 2015-07-08 16:05:11 +0000 |
441 | +++ lib/lp/code/model/branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
442 | @@ -58,7 +58,6 @@ |
443 | ) |
444 | from lp.code.event.branchmergeproposal import ( |
445 | BranchMergeProposalNeedsReviewEvent, |
446 | - BranchMergeProposalStatusChangeEvent, |
447 | NewCodeReviewCommentEvent, |
448 | ReviewerNominatedEvent, |
449 | ) |
450 | @@ -577,7 +576,6 @@ |
451 | _date_reviewed=None): |
452 | """Set the proposal to next_state.""" |
453 | # Check the reviewer can review the code for the target branch. |
454 | - old_state = self.queue_status |
455 | if not self.merge_target.isPersonTrustedReviewer(reviewer): |
456 | raise UserNotBranchReviewer |
457 | # Check the current state of the proposal. |
458 | @@ -589,8 +587,6 @@ |
459 | self.date_reviewed = _date_reviewed |
460 | # Record the reviewed revision id |
461 | self.reviewed_revision_id = revision_id |
462 | - notify(BranchMergeProposalStatusChangeEvent( |
463 | - self, reviewer, old_state, next_state)) |
464 | |
465 | def approveBranch(self, reviewer, revision_id, _date_reviewed=None): |
466 | """See `IBranchMergeProposal`.""" |
467 | |
468 | === modified file 'lib/lp/code/model/branchmergeproposaljob.py' |
469 | --- lib/lp/code/model/branchmergeproposaljob.py 2015-08-03 09:52:29 +0000 |
470 | +++ lib/lp/code/model/branchmergeproposaljob.py 2015-10-06 15:19:46 +0000 |
471 | @@ -284,6 +284,7 @@ |
472 | Or(Branch.next_mirror_time == None, |
473 | Branch.branch_type != BranchType.HOSTED)), |
474 | BranchMergeProposal.source_git_repository != None))) |
475 | + jobs = jobs.config(distinct=True) |
476 | return (klass(job) for job in jobs) |
477 | |
478 | def getOopsVars(self): |
479 | |
480 | === modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py' |
481 | --- lib/lp/code/model/tests/test_branchmergeproposal.py 2015-09-28 17:38:45 +0000 |
482 | +++ lib/lp/code/model/tests/test_branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
483 | @@ -1,4 +1,4 @@ |
484 | -# Copyright 2009-2014 Canonical Ltd. This software is licensed under the |
485 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
486 | # GNU Affero General Public License version 3 (see the file LICENSE). |
487 | |
488 | """Tests for BranchMergeProposals.""" |
489 | @@ -514,7 +514,7 @@ |
490 | commenter = self.factory.makePerson() |
491 | login_person(commenter) |
492 | result, events = self.assertNotifies( |
493 | - NewCodeReviewCommentEvent, |
494 | + NewCodeReviewCommentEvent, False, |
495 | merge_proposal.createComment, |
496 | owner=commenter, |
497 | subject='A review.') |
498 | @@ -661,7 +661,7 @@ |
499 | registrant = self.factory.makePerson() |
500 | result, events = self.assertNotifies( |
501 | [NewBranchMergeProposalEvent, |
502 | - BranchMergeProposalNeedsReviewEvent], |
503 | + BranchMergeProposalNeedsReviewEvent], False, |
504 | source_branch.addLandingTarget, registrant, target_branch, |
505 | needs_review=True) |
506 | self.assertEqual(result, events[0].object) |
507 | @@ -674,7 +674,7 @@ |
508 | product=source_branch.product) |
509 | registrant = self.factory.makePerson() |
510 | result, events = self.assertNotifies( |
511 | - [NewBranchMergeProposalEvent], |
512 | + [NewBranchMergeProposalEvent], False, |
513 | source_branch.addLandingTarget, registrant, target_branch) |
514 | self.assertEqual(result, events[0].object) |
515 | |
516 | @@ -685,7 +685,7 @@ |
517 | set_state=BranchMergeProposalStatus.WORK_IN_PROGRESS) |
518 | with person_logged_in(bmp.registrant): |
519 | self.assertNotifies( |
520 | - [BranchMergeProposalNeedsReviewEvent], |
521 | + [BranchMergeProposalNeedsReviewEvent], False, |
522 | bmp.setStatus, BranchMergeProposalStatus.NEEDS_REVIEW) |
523 | |
524 | def test_needs_review_no_op(self): |
525 | @@ -1285,7 +1285,7 @@ |
526 | bmp = self.factory.makeBranchMergeProposal() |
527 | login_person(bmp.target_branch.owner) |
528 | self.assertNotifies( |
529 | - ObjectModifiedEvent, notify_modified, bmp, bmp.markAsMerged, |
530 | + ObjectModifiedEvent, False, notify_modified, bmp, bmp.markAsMerged, |
531 | merge_reporter=bmp.target_branch.owner) |
532 | self.assertEqual(BranchMergeProposalStatus.MERGED, bmp.queue_status) |
533 | self.assertEqual(bmp.target_branch.owner, bmp.merge_reporter) |
534 | @@ -1305,7 +1305,7 @@ |
535 | login_person(merge_proposal.source_branch.owner) |
536 | reviewer = self.factory.makePerson() |
537 | result, events = self.assertNotifies( |
538 | - ReviewerNominatedEvent, |
539 | + ReviewerNominatedEvent, False, |
540 | merge_proposal.nominateReviewer, |
541 | reviewer=reviewer, |
542 | registrant=merge_proposal.source_branch.owner) |
543 | |
544 | === modified file 'lib/lp/code/model/tests/test_codereviewcomment.py' |
545 | --- lib/lp/code/model/tests/test_codereviewcomment.py 2012-01-15 10:43:27 +0000 |
546 | +++ lib/lp/code/model/tests/test_codereviewcomment.py 2015-10-06 15:19:46 +0000 |
547 | @@ -1,4 +1,4 @@ |
548 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
549 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
550 | # GNU Affero General Public License version 3 (see the file LICENSE). |
551 | |
552 | """Unit tests for CodeReviewComment""" |
553 | @@ -107,7 +107,8 @@ |
554 | """Creating a CodeReviewComment should trigger a notification.""" |
555 | message = self.factory.makeMessage() |
556 | self.assertNotifies( |
557 | - NewCodeReviewCommentEvent, self.bmp.createCommentFromMessage, |
558 | + NewCodeReviewCommentEvent, False, |
559 | + self.bmp.createCommentFromMessage, |
560 | message, None, None, original_email=None, _validate=False) |
561 | |
562 | |
563 | |
564 | === modified file 'lib/lp/code/model/tests/test_codereviewkarma.py' |
565 | --- lib/lp/code/model/tests/test_codereviewkarma.py 2011-12-30 07:38:46 +0000 |
566 | +++ lib/lp/code/model/tests/test_codereviewkarma.py 2015-10-06 15:19:46 +0000 |
567 | @@ -1,10 +1,11 @@ |
568 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
569 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
570 | # GNU Affero General Public License version 3 (see the file LICENSE). |
571 | |
572 | """Tests for karma allocated for code reviews.""" |
573 | |
574 | __metaclass__ = type |
575 | |
576 | +from lp.code.interfaces.branchmergeproposal import notify_modified |
577 | from lp.registry.interfaces.karma import IKarmaAssignedEvent |
578 | from lp.registry.interfaces.person import IPerson |
579 | from lp.testing import ( |
580 | @@ -113,7 +114,9 @@ |
581 | proposal = self.factory.makeBranchMergeProposal() |
582 | reviewer = proposal.target_branch.owner |
583 | self.karma_events = [] |
584 | - proposal.approveBranch(reviewer, "A rev id.") |
585 | + login_person(reviewer) |
586 | + notify_modified( |
587 | + proposal, proposal.approveBranch, reviewer, "A rev id.") |
588 | self.assertOneKarmaEvent(reviewer, 'branchmergeapproved') |
589 | |
590 | def test_approvingOwnCodeReview(self): |
591 | @@ -123,7 +126,9 @@ |
592 | proposal = self.factory.makeBranchMergeProposal( |
593 | target_branch=target_branch, registrant=reviewer) |
594 | self.karma_events = [] |
595 | - proposal.approveBranch(reviewer, "A rev id.") |
596 | + login_person(reviewer) |
597 | + notify_modified( |
598 | + proposal, proposal.approveBranch, reviewer, "A rev id.") |
599 | self.assertOneKarmaEvent(reviewer, 'branchmergeapprovedown') |
600 | |
601 | def test_rejectedCodeReview(self): |
602 | @@ -132,7 +137,8 @@ |
603 | proposal = self.factory.makeBranchMergeProposal() |
604 | reviewer = proposal.target_branch.owner |
605 | self.karma_events = [] |
606 | - proposal.rejectBranch(reviewer, "A rev id.") |
607 | + login_person(reviewer) |
608 | + notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.") |
609 | self.assertOneKarmaEvent(reviewer, 'branchmergerejected') |
610 | |
611 | def test_rejectedOwnCodeReview(self): |
612 | @@ -144,5 +150,6 @@ |
613 | proposal = self.factory.makeBranchMergeProposal( |
614 | target_branch=target_branch, registrant=reviewer) |
615 | self.karma_events = [] |
616 | - proposal.rejectBranch(reviewer, "A rev id.") |
617 | + login_person(reviewer) |
618 | + notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.") |
619 | self.assertOneKarmaEvent(reviewer, 'branchmergerejectedown') |
620 | |
621 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
622 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-09-02 16:54:24 +0000 |
623 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-10-06 15:19:46 +0000 |
624 | @@ -51,6 +51,7 @@ |
625 | GitRepositoryExists, |
626 | GitTargetError, |
627 | ) |
628 | +from lp.code.event.git import GitRefsUpdatedEvent |
629 | from lp.code.interfaces.branchmergeproposal import ( |
630 | BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES, |
631 | ) |
632 | @@ -1864,7 +1865,7 @@ |
633 | hosting_client.detectMerges = FakeMethod( |
634 | result={source_1.commit_sha1: u"0" * 40}) |
635 | self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient)) |
636 | - repository.createOrUpdateRefs({ |
637 | + refs_info = { |
638 | u"refs/heads/target-1": { |
639 | u"sha1": u"0" * 40, |
640 | u"type": GitObjectType.COMMIT, |
641 | @@ -1872,8 +1873,12 @@ |
642 | u"refs/heads/target-2": { |
643 | u"sha1": u"1" * 40, |
644 | u"type": GitObjectType.COMMIT, |
645 | - } |
646 | - }) |
647 | + }, |
648 | + } |
649 | + expected_events = [ |
650 | + ObjectModifiedEvent, ObjectModifiedEvent, GitRefsUpdatedEvent] |
651 | + _, events = self.assertNotifies( |
652 | + expected_events, True, repository.createOrUpdateRefs, refs_info) |
653 | expected_args = [ |
654 | (repository.getInternalPath(), target_1.commit_sha1, |
655 | set([source_1.commit_sha1, source_2.commit_sha1])), |
656 | @@ -1888,6 +1893,15 @@ |
657 | BranchMergeProposalStatus.WORK_IN_PROGRESS, bmp2.queue_status) |
658 | self.assertEqual(BranchMergeProposalStatus.MERGED, bmp3.queue_status) |
659 | self.assertEqual(u"0" * 40, bmp3.merged_revision_id) |
660 | + # The two ObjectModifiedEvents indicate the queue_status changes. |
661 | + self.assertContentEqual( |
662 | + [bmp1, bmp3], [event.object for event in events[:2]]) |
663 | + self.assertContentEqual( |
664 | + [(BranchMergeProposalStatus.WORK_IN_PROGRESS, |
665 | + BranchMergeProposalStatus.MERGED)], |
666 | + set((event.object_before_modification.queue_status, |
667 | + event.object.queue_status) |
668 | + for event in events[:2])) |
669 | |
670 | |
671 | class TestGitRepositorySet(TestCaseWithFactory): |
672 | |
673 | === modified file 'lib/lp/code/subscribers/branchmergeproposal.py' |
674 | --- lib/lp/code/subscribers/branchmergeproposal.py 2013-04-09 09:47:58 +0000 |
675 | +++ lib/lp/code/subscribers/branchmergeproposal.py 2015-10-06 15:19:46 +0000 |
676 | @@ -1,4 +1,4 @@ |
677 | -# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
678 | +# Copyright 2010-2015 Canonical Ltd. This software is licensed under the |
679 | # GNU Affero General Public License version 3 (see the file LICENSE). |
680 | |
681 | """Event subscribers for branch merge proposals.""" |
682 | @@ -49,14 +49,19 @@ |
683 | from_person = None |
684 | else: |
685 | from_person = IPerson(event.user) |
686 | - # If the merge proposal was work in progress, then we don't want to send |
687 | - # out an email as the needs review email will cover that. |
688 | + # If the merge proposal was work in progress and is now needs review, |
689 | + # then we don't want to send out an email as the needs review email will |
690 | + # cover that. |
691 | old_status = event.object_before_modification.queue_status |
692 | - if old_status == BranchMergeProposalStatus.WORK_IN_PROGRESS: |
693 | - # Unless the new status is merged. If this occurs we really should |
694 | - # send out an email. |
695 | - if merge_proposal.queue_status != BranchMergeProposalStatus.MERGED: |
696 | - return |
697 | + new_status = merge_proposal.queue_status |
698 | + |
699 | + in_progress_states = ( |
700 | + BranchMergeProposalStatus.WORK_IN_PROGRESS, |
701 | + BranchMergeProposalStatus.NEEDS_REVIEW) |
702 | + |
703 | + if (old_status == BranchMergeProposalStatus.WORK_IN_PROGRESS and |
704 | + new_status in in_progress_states): |
705 | + return |
706 | # Create a delta of the changes. If there are no changes to report, then |
707 | # we're done. |
708 | delta = BranchMergeProposalNoPreviewDiffDelta.construct( |
709 | |
710 | === modified file 'lib/lp/code/subscribers/karma.py' |
711 | --- lib/lp/code/subscribers/karma.py 2015-04-22 16:11:40 +0000 |
712 | +++ lib/lp/code/subscribers/karma.py 2015-10-06 15:19:46 +0000 |
713 | @@ -3,6 +3,8 @@ |
714 | |
715 | """Assign karma for code domain activity.""" |
716 | |
717 | +from zope.principalregistry.principalregistry import UnauthenticatedPrincipal |
718 | + |
719 | from lp.code.enums import BranchMergeProposalStatus |
720 | from lp.registry.interfaces.person import IPerson |
721 | from lp.services.database.sqlbase import block_implicit_flushes |
722 | @@ -43,26 +45,33 @@ |
723 | |
724 | |
725 | @block_implicit_flushes |
726 | -def branch_merge_status_changed(proposal, event): |
727 | - """Assign karma to the user who approved the merge.""" |
728 | +def branch_merge_modified(proposal, event): |
729 | + """Assign karma to the user who approved or rejected the merge.""" |
730 | + if event.user is None or isinstance(event.user, UnauthenticatedPrincipal): |
731 | + # Some modification events have no associated user context. In |
732 | + # these cases there's no karma to assign. |
733 | + return |
734 | + |
735 | if proposal.source_git_repository is not None: |
736 | target = proposal.source_git_repository.namespace |
737 | else: |
738 | target = proposal.source_branch.target |
739 | user = IPerson(event.user) |
740 | + old_status = event.object_before_modification.queue_status |
741 | + new_status = proposal.queue_status |
742 | |
743 | in_progress_states = ( |
744 | BranchMergeProposalStatus.WORK_IN_PROGRESS, |
745 | BranchMergeProposalStatus.NEEDS_REVIEW) |
746 | |
747 | - if ((event.to_state == BranchMergeProposalStatus.CODE_APPROVED) and |
748 | - (event.from_state in (in_progress_states))): |
749 | + if ((new_status == BranchMergeProposalStatus.CODE_APPROVED) and |
750 | + (old_status in (in_progress_states))): |
751 | if user == proposal.registrant: |
752 | target.assignKarma(user, 'branchmergeapprovedown') |
753 | else: |
754 | target.assignKarma(user, 'branchmergeapproved') |
755 | - elif ((event.to_state == BranchMergeProposalStatus.REJECTED) and |
756 | - (event.from_state in (in_progress_states))): |
757 | + elif ((new_status == BranchMergeProposalStatus.REJECTED) and |
758 | + (old_status in (in_progress_states))): |
759 | if user == proposal.registrant: |
760 | target.assignKarma(user, 'branchmergerejectedown') |
761 | else: |
762 | |
763 | === modified file 'lib/lp/codehosting/scanner/tests/test_mergedetection.py' |
764 | --- lib/lp/codehosting/scanner/tests/test_mergedetection.py 2013-06-20 05:50:00 +0000 |
765 | +++ lib/lp/codehosting/scanner/tests/test_mergedetection.py 2015-10-06 15:19:46 +0000 |
766 | @@ -1,4 +1,4 @@ |
767 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
768 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
769 | # GNU Affero General Public License version 3 (see the file LICENSE). |
770 | |
771 | """Tests for the scanner's merge detection.""" |
772 | @@ -8,6 +8,7 @@ |
773 | import logging |
774 | |
775 | from bzrlib.revision import NULL_REVISION |
776 | +from lazr.lifecycle.event import ObjectModifiedEvent |
777 | import transaction |
778 | from zope.component import getUtility |
779 | from zope.event import notify |
780 | @@ -271,7 +272,8 @@ |
781 | self.assertNotEqual( |
782 | BranchLifecycleStatus.MERGED, |
783 | proposal.source_branch.lifecycle_status) |
784 | - mergedetection.merge_detected( |
785 | + _, [event] = self.assertNotifies( |
786 | + [ObjectModifiedEvent], True, mergedetection.merge_detected, |
787 | logging.getLogger(), |
788 | proposal.source_branch, proposal.target_branch, proposal) |
789 | self.assertEqual( |
790 | @@ -279,6 +281,12 @@ |
791 | self.assertEqual( |
792 | BranchLifecycleStatus.MERGED, |
793 | proposal.source_branch.lifecycle_status) |
794 | + self.assertEqual(proposal, event.object) |
795 | + self.assertEqual( |
796 | + BranchMergeProposalStatus.WORK_IN_PROGRESS, |
797 | + event.object_before_modification.queue_status) |
798 | + self.assertEqual( |
799 | + BranchMergeProposalStatus.MERGED, event.object.queue_status) |
800 | job = IStore(proposal).find( |
801 | BranchMergeProposalJob, |
802 | BranchMergeProposalJob.branch_merge_proposal == proposal, |
803 | |
804 | === modified file 'lib/lp/testing/__init__.py' |
805 | --- lib/lp/testing/__init__.py 2015-09-30 00:38:30 +0000 |
806 | +++ lib/lp/testing/__init__.py 2015-10-06 15:19:46 +0000 |
807 | @@ -530,11 +530,14 @@ |
808 | from lp.testing.matchers import Provides |
809 | self.assertThat(obj, Provides(interface)) |
810 | |
811 | - def assertNotifies(self, event_types, callable_obj, *args, **kwargs): |
812 | + def assertNotifies(self, event_types, propagate, callable_obj, |
813 | + *args, **kwargs): |
814 | """Assert that a callable performs a given notification. |
815 | |
816 | :param event_type: One or more event types that notification is |
817 | expected for. |
818 | + :param propagate: If True, propagate events to their normal |
819 | + subscribers. |
820 | :param callable_obj: The callable to call. |
821 | :param *args: The arguments to pass to the callable. |
822 | :param **kwargs: The keyword arguments to pass to the callable. |
823 | @@ -543,7 +546,7 @@ |
824 | """ |
825 | if not isinstance(event_types, (list, tuple)): |
826 | event_types = [event_types] |
827 | - with EventRecorder() as recorder: |
828 | + with EventRecorder(propagate=propagate) as recorder: |
829 | result = callable_obj(*args, **kwargs) |
830 | if len(recorder.events) == 0: |
831 | raise AssertionError('No notification was performed.') |
832 | @@ -1241,21 +1244,27 @@ |
833 | class EventRecorder: |
834 | """Intercept and record Zope events. |
835 | |
836 | - This prevents the events from propagating to their normal subscribers. |
837 | - The recorded events can be accessed via the 'events' list. |
838 | + This prevents the events from propagating to their normal subscribers, |
839 | + unless `propagate=True` is passed to the constructor. The recorded |
840 | + events can be accessed via the 'events' list. |
841 | """ |
842 | |
843 | - def __init__(self): |
844 | + def __init__(self, propagate=False): |
845 | + self.propagate = propagate |
846 | self.events = [] |
847 | self.old_subscribers = None |
848 | + self.new_subscribers = None |
849 | |
850 | def __enter__(self): |
851 | self.old_subscribers = zope.event.subscribers[:] |
852 | - zope.event.subscribers[:] = [self.events.append] |
853 | + if not self.propagate: |
854 | + zope.event.subscribers[:] = [] |
855 | + zope.event.subscribers.append(self.events.append) |
856 | + self.new_subscribers = zope.event.subscribers[:] |
857 | return self |
858 | |
859 | def __exit__(self, exc_type, exc_value, traceback): |
860 | - assert zope.event.subscribers == [self.events.append], ( |
861 | + assert zope.event.subscribers == self.new_subscribers, ( |
862 | 'Subscriber list has been changed while running!') |
863 | zope.event.subscribers[:] = self.old_subscribers |
864 |