Merge lp:~cjwatson/launchpad/consistent-bmp-events into lp:launchpad

Proposed by Colin Watson
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
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+273325@code.launchpad.net

Commit message

Remove BranchMergeProposalStatusChangeEvent and consistently generate ObjectModifiedEvent for all changes to merge proposal attributes.

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-chaotic conditions for when those events are generated. This removes the special-purpose BranchMergeProposalStatusChangeEvent, and instead simply consistently generates ObjectModifiedEvent for all changes to merge proposal attributes.

To get this right, I exhaustively checked all the possible calling paths to BranchMergeProposal._transitionToState. A couple of them (BranchMergeProposal:+resubmit and CodeHandler) failed to be consistent with other paths in terms of whether they generated ObjectModifiedEvent, which wasn't a practical problem before but would be now; and most of the rest were untested. I fixed all this up. The one relevant webservice entry point (BranchMergeProposal.setStatus) of course gets this right automatically thanks to lazr.restful.

BranchMergeProposalNeedsReviewEvent continues to exist even though it's sometimes accompanied by an almost equivalent ObjectModifiedEvent, because it can also be generated when a merge proposal is created.

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2015-07-21 16:30:15 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -161,18 +161,6 @@
161 return decorator161 return decorator
162162
163163
164def update_and_notify(func):
165 """Decorate an action to update from a form and send a notification."""
166
167 @notify
168 def decorator(view, action, data):
169 result = func(view, action, data)
170 form.applyChanges(
171 view.context, view.form_fields, data, view.adapters)
172 return result
173 return decorator
174
175
176class BranchMergeCandidateView(LaunchpadView):164class BranchMergeCandidateView(LaunchpadView):
177 """Provides a small fragment of landing targets"""165 """Provides a small fragment of landing targets"""
178166
@@ -1028,6 +1016,7 @@
1028 return dict(item for item in items if item[1] is not UNSET)1016 return dict(item for item in items if item[1] is not UNSET)
10291017
1030 @action('Resubmit', name='resubmit')1018 @action('Resubmit', name='resubmit')
1019 @notify
1031 def resubmit_action(self, action, data):1020 def resubmit_action(self, action, data):
1032 """Resubmit this proposal."""1021 """Resubmit this proposal."""
1033 try:1022 try:
10341023
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-10-01 10:25:19 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -12,6 +12,7 @@
12 )12 )
13from difflib import unified_diff13from difflib import unified_diff
1414
15from lazr.lifecycle.event import ObjectModifiedEvent
15from lazr.restful.interfaces import IJSONRequestCache16from lazr.restful.interfaces import IJSONRequestCache
16import pytz17import pytz
17import simplejson18import simplejson
@@ -20,11 +21,16 @@
20 Tag,21 Tag,
21 )22 )
22from testtools.matchers import (23from testtools.matchers import (
24 MatchesListwise,
23 MatchesRegex,25 MatchesRegex,
26 MatchesStructure,
24 Not,27 Not,
25 )28 )
26import transaction29import transaction
27from zope.component import getMultiAdapter30from zope.component import (
31 getMultiAdapter,
32 getUtility,
33 )
28from zope.security.interfaces import Unauthorized34from zope.security.interfaces import Unauthorized
29from zope.security.proxy import removeSecurityProxy35from zope.security.proxy import removeSecurityProxy
3036
@@ -47,6 +53,11 @@
47 BranchMergeProposalStatus,53 BranchMergeProposalStatus,
48 CodeReviewVote,54 CodeReviewVote,
49 )55 )
56from lp.code.interfaces.branchmergeproposal import (
57 IBranchMergeProposal,
58 IMergeProposalNeedsReviewEmailJobSource,
59 IMergeProposalUpdatedEmailJobSource,
60 )
50from lp.code.model.diff import PreviewDiff61from lp.code.model.diff import PreviewDiff
51from lp.code.tests.helpers import (62from lp.code.tests.helpers import (
52 add_revision_to_branch,63 add_revision_to_branch,
@@ -66,6 +77,7 @@
66from lp.services.webapp.servers import LaunchpadTestRequest77from lp.services.webapp.servers import LaunchpadTestRequest
67from lp.testing import (78from lp.testing import (
68 BrowserTestCase,79 BrowserTestCase,
80 EventRecorder,
69 feature_flags,81 feature_flags,
70 login_person,82 login_person,
71 monkey_patch,83 monkey_patch,
@@ -168,6 +180,27 @@
168 browser = self.getViewBrowser(bmp.merge_source, '+index')180 browser = self.getViewBrowser(bmp.merge_source, '+index')
169 self.assertThat(browser.contents, HTMLContains(revision_number))181 self.assertThat(browser.contents, HTMLContains(revision_number))
170182
183 def test_notifies_modification(self):
184 bmp = self.makeBranchMergeProposal()
185 registrant = bmp.registrant
186 login_person(registrant)
187 browser = self.getViewBrowser(bmp, '+merged', user=bmp.registrant)
188 browser.getControl(self.merged_revision_text).value = str(
189 self.arbitrary_revisions[2])
190 with EventRecorder(propagate=True) as recorder:
191 browser.getControl('Mark as Merged').click()
192 login_person(registrant)
193 events = [
194 event for event in recorder.events
195 if isinstance(event, ObjectModifiedEvent) and
196 IBranchMergeProposal.providedBy(event.object)]
197 self.assertThat(events, MatchesListwise([
198 MatchesStructure(
199 object_before_modification=MatchesStructure.byEquality(
200 queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS),
201 object=MatchesStructure.byEquality(
202 queue_status=BranchMergeProposalStatus.MERGED))]))
203
171204
172class TestBranchMergeProposalMergedViewBzr(205class TestBranchMergeProposalMergedViewBzr(
173 TestBranchMergeProposalMergedViewMixin, BrowserTestCase):206 TestBranchMergeProposalMergedViewMixin, BrowserTestCase):
@@ -918,6 +951,50 @@
918 simplejson.loads(view.form_result))951 simplejson.loads(view.form_result))
919952
920953
954class TestBranchMergeProposalRequestReviewViewMixin:
955 """Test `BranchMergeProposalRequestReviewView`."""
956
957 layer = DatabaseFunctionalLayer
958
959 def test_notifies_modification(self):
960 bmp = self.makeBranchMergeProposal()
961 registrant = bmp.registrant
962 self.factory.makePerson(name='test-reviewer')
963 login_person(registrant)
964 browser = self.getViewBrowser(
965 bmp, '+request-review', user=bmp.registrant)
966 browser.getControl('Reviewer').value = 'test-reviewer'
967 with EventRecorder(propagate=True) as recorder:
968 browser.getControl('Request Review').click()
969 login_person(registrant)
970 events = [
971 event for event in recorder.events
972 if isinstance(event, ObjectModifiedEvent) and
973 IBranchMergeProposal.providedBy(event.object)]
974 self.assertThat(events, MatchesListwise([
975 MatchesStructure(
976 object_before_modification=MatchesStructure.byEquality(
977 queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS),
978 object=MatchesStructure.byEquality(
979 queue_status=BranchMergeProposalStatus.NEEDS_REVIEW))]))
980
981
982class TestBranchMergeProposalRequestReviewViewBzr(
983 TestBranchMergeProposalRequestReviewViewMixin, BrowserTestCase):
984 """Test `BranchMergeProposalRequestReviewView` for Bazaar."""
985
986 def makeBranchMergeProposal(self):
987 return self.factory.makeBranchMergeProposal()
988
989
990class TestBranchMergeProposalRequestReviewViewGit(
991 TestBranchMergeProposalRequestReviewViewMixin, BrowserTestCase):
992 """Test `BranchMergeProposalRequestReviewView` for Git."""
993
994 def makeBranchMergeProposal(self):
995 return self.factory.makeBranchMergeProposalForGit()
996
997
921class TestBranchMergeProposalResubmitViewMixin:998class TestBranchMergeProposalResubmitViewMixin:
922 """Test BranchMergeProposalResubmitView."""999 """Test BranchMergeProposalResubmitView."""
9231000
@@ -998,13 +1075,54 @@
998 ' <a href=.*>a similar merge proposal</a> is already active.'))1075 ' <a href=.*>a similar merge proposal</a> is already active.'))
999 self.assertEqual(BrowserNotificationLevel.ERROR, notification.level)1076 self.assertEqual(BrowserNotificationLevel.ERROR, notification.level)
10001077
1078 def test_notifies_modification(self):
1079 view = self.createView()
1080 with EventRecorder(propagate=True) as recorder:
1081 self.resubmitDefault(view)
1082 events = [
1083 event for event in recorder.events
1084 if isinstance(event, ObjectModifiedEvent) and
1085 IBranchMergeProposal.providedBy(event.object)]
1086 self.assertThat(events, MatchesListwise([
1087 MatchesStructure(
1088 object_before_modification=MatchesStructure.byEquality(
1089 queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS),
1090 object=MatchesStructure.byEquality(
1091 queue_status=BranchMergeProposalStatus.SUPERSEDED))]))
1092
1093 def test_mail_jobs(self):
1094 """Resubmitting sends mail about the changed old MP and the new MP."""
1095 view = self.createView()
1096 new_proposal = self.resubmitDefault(view)
1097 updated_job_source = getUtility(IMergeProposalUpdatedEmailJobSource)
1098 [updated_job] = list(
1099 removeSecurityProxy(updated_job_source).iterReady())
1100 self.assertThat(updated_job, MatchesStructure.byEquality(
1101 branch_merge_proposal=view.context,
1102 metadata={
1103 'delta_text': ' Status: Work in progress => Superseded',
1104 'editor': view.context.registrant.name,
1105 }))
1106 needs_review_job_source = getUtility(
1107 IMergeProposalNeedsReviewEmailJobSource)
1108 [needs_review_job] = list(
1109 removeSecurityProxy(needs_review_job_source).iterReady())
1110 self.assertThat(needs_review_job, MatchesStructure.byEquality(
1111 branch_merge_proposal=new_proposal,
1112 metadata={}))
1113
10011114
1002class TestBranchMergeProposalResubmitViewBzr(1115class TestBranchMergeProposalResubmitViewBzr(
1003 TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):1116 TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
1004 """Test BranchMergeProposalResubmitView for Bazaar."""1117 """Test BranchMergeProposalResubmitView for Bazaar."""
10051118
1006 def _makeBranchMergeProposal(self):1119 def _makeBranchMergeProposal(self):
1007 return self.factory.makeBranchMergeProposal()1120 bmp = self.factory.makeBranchMergeProposal()
1121 # Pretend to have a revision so that
1122 # BranchMergeProposalJobDerived.iterReady will consider this
1123 # proposal.
1124 removeSecurityProxy(bmp.source_branch).revision_count = 1
1125 return bmp
10081126
1009 @staticmethod1127 @staticmethod
1010 def _getFormValues(source_branch, target_branch, prerequisite_branch,1128 def _getFormValues(source_branch, target_branch, prerequisite_branch,
@@ -1366,8 +1484,8 @@
1366 self.assertThat(browser.contents, HTMLContains(expected_meta))1484 self.assertThat(browser.contents, HTMLContains(expected_meta))
13671485
13681486
1369class TestBranchMergeProposalChangeStatusOptions(TestCaseWithFactory):1487class TestBranchMergeProposalChangeStatusView(TestCaseWithFactory):
1370 """Test the status vocabulary generated for then +edit-status view."""1488 """Test the +edit-status view."""
13711489
1372 layer = DatabaseFunctionalLayer1490 layer = DatabaseFunctionalLayer
13731491
@@ -1378,10 +1496,10 @@
1378 self.proposal = self.factory.makeBranchMergeProposal(1496 self.proposal = self.factory.makeBranchMergeProposal(
1379 registrant=self.user)1497 registrant=self.user)
13801498
1381 def _createView(self):1499 def _createView(self, form=None):
1382 # Construct the view and initialize it.1500 # Construct the view and initialize it.
1383 view = BranchMergeProposalChangeStatusView(1501 view = BranchMergeProposalChangeStatusView(
1384 self.proposal, LaunchpadTestRequest())1502 self.proposal, LaunchpadTestRequest(form=form))
1385 view.initialize()1503 view.initialize()
1386 return view1504 return view
13871505
@@ -1454,6 +1572,22 @@
1454 self.assertAllStatusesAvailable(1572 self.assertAllStatusesAvailable(
1455 user=self.proposal.target_branch.owner)1573 user=self.proposal.target_branch.owner)
14561574
1575 def test_notifies_modification(self):
1576 view = self._createView(form={'revno': '1'})
1577 with EventRecorder(propagate=True) as recorder:
1578 view.update_action.success(
1579 {'queue_status': BranchMergeProposalStatus.NEEDS_REVIEW})
1580 events = [
1581 event for event in recorder.events
1582 if isinstance(event, ObjectModifiedEvent) and
1583 IBranchMergeProposal.providedBy(event.object)]
1584 self.assertThat(events, MatchesListwise([
1585 MatchesStructure(
1586 object_before_modification=MatchesStructure.byEquality(
1587 queue_status=BranchMergeProposalStatus.WORK_IN_PROGRESS),
1588 object=MatchesStructure.byEquality(
1589 queue_status=BranchMergeProposalStatus.NEEDS_REVIEW))]))
1590
14571591
1458class TestCommentAttachmentRendering(TestCaseWithFactory):1592class TestCommentAttachmentRendering(TestCaseWithFactory):
1459 """Test diff attachments are rendered correctly."""1593 """Test diff attachments are rendered correctly."""
14601594
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-09-24 13:44:02 +0000
+++ lib/lp/code/configure.zcml 2015-10-06 15:19:46 +0000
@@ -295,8 +295,8 @@
295 handler="lp.code.subscribers.karma.branch_merge_proposed"/>295 handler="lp.code.subscribers.karma.branch_merge_proposed"/>
296 <subscriber296 <subscriber
297 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal297 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal
298 lp.code.interfaces.event.IBranchMergeProposalStatusChangeEvent"298 lazr.lifecycle.interfaces.IObjectModifiedEvent"
299 handler="lp.code.subscribers.karma.branch_merge_status_changed"/>299 handler="lp.code.subscribers.karma.branch_merge_modified"/>
300300
301 <!-- hierarchy -->301 <!-- hierarchy -->
302302
303303
=== modified file 'lib/lp/code/event/branchmergeproposal.py'
--- lib/lp/code/event/branchmergeproposal.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/event/branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Event implementation classes for branch merge proposal events."""4"""Event implementation classes for branch merge proposal events."""
@@ -6,7 +6,6 @@
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'BranchMergeProposalNeedsReviewEvent',8 'BranchMergeProposalNeedsReviewEvent',
9 'BranchMergeProposalStatusChangeEvent',
10 'NewBranchMergeProposalEvent',9 'NewBranchMergeProposalEvent',
11 'NewCodeReviewCommentEvent',10 'NewCodeReviewCommentEvent',
12 'ReviewerNominatedEvent',11 'ReviewerNominatedEvent',
@@ -17,24 +16,12 @@
1716
18from lp.code.interfaces.event import (17from lp.code.interfaces.event import (
19 IBranchMergeProposalNeedsReviewEvent,18 IBranchMergeProposalNeedsReviewEvent,
20 IBranchMergeProposalStatusChangeEvent,
21 INewBranchMergeProposalEvent,19 INewBranchMergeProposalEvent,
22 INewCodeReviewCommentEvent,20 INewCodeReviewCommentEvent,
23 IReviewerNominatedEvent,21 IReviewerNominatedEvent,
24 )22 )
2523
2624
27@implementer(IBranchMergeProposalStatusChangeEvent)
28class BranchMergeProposalStatusChangeEvent(ObjectEvent):
29 """See `IBranchMergeProposalStatusChangeEvent`."""
30
31 def __init__(self, proposal, user, from_state, to_state):
32 ObjectEvent.__init__(self, proposal)
33 self.user = user
34 self.from_state = from_state
35 self.to_state = to_state
36
37
38@implementer(INewBranchMergeProposalEvent)25@implementer(INewBranchMergeProposalEvent)
39class NewBranchMergeProposalEvent(ObjectEvent):26class NewBranchMergeProposalEvent(ObjectEvent):
40 """A new merge has been proposed."""27 """A new merge has been proposed."""
4128
=== modified file 'lib/lp/code/interfaces/event.py'
--- lib/lp/code/interfaces/event.py 2015-06-06 08:49:54 +0000
+++ lib/lp/code/interfaces/event.py 2015-10-06 15:19:46 +0000
@@ -5,7 +5,6 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'IBranchMergeProposalStatusChangeEvent',
9 'IBranchMergeProposalNeedsReviewEvent',8 'IBranchMergeProposalNeedsReviewEvent',
10 'IGitRefsUpdatedEvent',9 'IGitRefsUpdatedEvent',
11 'INewBranchMergeProposalEvent',10 'INewBranchMergeProposalEvent',
@@ -15,15 +14,6 @@
1514
1615
17from zope.component.interfaces import IObjectEvent16from zope.component.interfaces import IObjectEvent
18from zope.interface import Attribute
19
20
21class IBranchMergeProposalStatusChangeEvent(IObjectEvent):
22 """A merge proposal has changed state."""
23
24 user = Attribute("The user who updated the proposal.")
25 from_state = Attribute("The previous queue_status.")
26 to_state = Attribute("The updated queue_status.")
2717
2818
29class INewBranchMergeProposalEvent(IObjectEvent):19class INewBranchMergeProposalEvent(IObjectEvent):
3020
=== modified file 'lib/lp/code/mail/codehandler.py'
--- lib/lp/code/mail/codehandler.py 2015-09-28 17:38:45 +0000
+++ lib/lp/code/mail/codehandler.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -14,6 +14,7 @@
14from zope.interface import implementer14from zope.interface import implementer
15from zope.security.interfaces import Unauthorized15from zope.security.interfaces import Unauthorized
1616
17from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
17from lp.code.enums import CodeReviewVote18from lp.code.enums import CodeReviewVote
18from lp.code.errors import UserNotBranchReviewer19from lp.code.errors import UserNotBranchReviewer
19from lp.code.interfaces.branchmergeproposal import IBranchMergeProposalGetter20from lp.code.interfaces.branchmergeproposal import IBranchMergeProposalGetter
@@ -258,12 +259,13 @@
258 def processCommands(self, context, commands):259 def processCommands(self, context, commands):
259 """Process the various merge proposal commands against the context."""260 """Process the various merge proposal commands against the context."""
260 processing_errors = []261 processing_errors = []
261262 with BranchMergeProposalNoPreviewDiffDelta.monitor(
262 for command in commands:263 context.merge_proposal):
263 try:264 for command in commands:
264 command.execute(context)265 try:
265 except EmailProcessingError as error:266 command.execute(context)
266 processing_errors.append((error, command))267 except EmailProcessingError as error:
268 processing_errors.append((error, command))
267269
268 if len(processing_errors) > 0:270 if len(processing_errors) > 0:
269 errors, commands = zip(*processing_errors)271 errors, commands = zip(*processing_errors)
270272
=== modified file 'lib/lp/code/mail/tests/test_codehandler.py'
--- lib/lp/code/mail/tests/test_codehandler.py 2015-07-21 09:04:01 +0000
+++ lib/lp/code/mail/tests/test_codehandler.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Testing the CodeHandler."""4"""Testing the CodeHandler."""
@@ -7,6 +7,7 @@
77
8from textwrap import dedent8from textwrap import dedent
99
10from lazr.lifecycle.event import ObjectModifiedEvent
10from storm.store import Store11from storm.store import Store
11import transaction12import transaction
12from zope.security.management import setSecurityPolicy13from zope.security.management import setSecurityPolicy
@@ -17,6 +18,7 @@
17 CodeReviewNotificationLevel,18 CodeReviewNotificationLevel,
18 CodeReviewVote,19 CodeReviewVote,
19 )20 )
21from lp.code.event.branchmergeproposal import NewCodeReviewCommentEvent
20from lp.code.mail.codehandler import (22from lp.code.mail.codehandler import (
21 AddReviewerEmailCommand,23 AddReviewerEmailCommand,
22 CodeEmailCommands,24 CodeEmailCommands,
@@ -425,6 +427,24 @@
425 mail['from'])427 mail['from'])
426 self.assertEqual(0, bmp.all_comments.count())428 self.assertEqual(0, bmp.all_comments.count())
427429
430 def test_notifies_modification(self):
431 """Changes to the merge proposal itself trigger events."""
432 mail = self.factory.makeSignedMessage(body=' merge approved')
433 bmp = self.factory.makeBranchMergeProposal()
434 email_addr = bmp.address
435 switch_dbuser(config.processmail.dbuser)
436 login_person(bmp.merge_target.owner)
437 _, events = self.assertNotifies(
438 [ObjectModifiedEvent, NewCodeReviewCommentEvent], False,
439 self.code_handler.process, mail, email_addr, None)
440 self.assertEqual(bmp, events[0].object)
441 self.assertEqual(
442 BranchMergeProposalStatus.WORK_IN_PROGRESS,
443 events[0].object_before_modification.queue_status)
444 self.assertEqual(
445 BranchMergeProposalStatus.CODE_APPROVED,
446 events[0].object.queue_status)
447
428448
429class TestVoteEmailCommand(TestCase):449class TestVoteEmailCommand(TestCase):
430 """Test the vote and tag processing of the VoteEmailCommand."""450 """Test the vote and tag processing of the VoteEmailCommand."""
431451
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -58,7 +58,6 @@
58 )58 )
59from lp.code.event.branchmergeproposal import (59from lp.code.event.branchmergeproposal import (
60 BranchMergeProposalNeedsReviewEvent,60 BranchMergeProposalNeedsReviewEvent,
61 BranchMergeProposalStatusChangeEvent,
62 NewCodeReviewCommentEvent,61 NewCodeReviewCommentEvent,
63 ReviewerNominatedEvent,62 ReviewerNominatedEvent,
64 )63 )
@@ -577,7 +576,6 @@
577 _date_reviewed=None):576 _date_reviewed=None):
578 """Set the proposal to next_state."""577 """Set the proposal to next_state."""
579 # Check the reviewer can review the code for the target branch.578 # Check the reviewer can review the code for the target branch.
580 old_state = self.queue_status
581 if not self.merge_target.isPersonTrustedReviewer(reviewer):579 if not self.merge_target.isPersonTrustedReviewer(reviewer):
582 raise UserNotBranchReviewer580 raise UserNotBranchReviewer
583 # Check the current state of the proposal.581 # Check the current state of the proposal.
@@ -589,8 +587,6 @@
589 self.date_reviewed = _date_reviewed587 self.date_reviewed = _date_reviewed
590 # Record the reviewed revision id588 # Record the reviewed revision id
591 self.reviewed_revision_id = revision_id589 self.reviewed_revision_id = revision_id
592 notify(BranchMergeProposalStatusChangeEvent(
593 self, reviewer, old_state, next_state))
594590
595 def approveBranch(self, reviewer, revision_id, _date_reviewed=None):591 def approveBranch(self, reviewer, revision_id, _date_reviewed=None):
596 """See `IBranchMergeProposal`."""592 """See `IBranchMergeProposal`."""
597593
=== modified file 'lib/lp/code/model/branchmergeproposaljob.py'
--- lib/lp/code/model/branchmergeproposaljob.py 2015-08-03 09:52:29 +0000
+++ lib/lp/code/model/branchmergeproposaljob.py 2015-10-06 15:19:46 +0000
@@ -284,6 +284,7 @@
284 Or(Branch.next_mirror_time == None,284 Or(Branch.next_mirror_time == None,
285 Branch.branch_type != BranchType.HOSTED)),285 Branch.branch_type != BranchType.HOSTED)),
286 BranchMergeProposal.source_git_repository != None)))286 BranchMergeProposal.source_git_repository != None)))
287 jobs = jobs.config(distinct=True)
287 return (klass(job) for job in jobs)288 return (klass(job) for job in jobs)
288289
289 def getOopsVars(self):290 def getOopsVars(self):
290291
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py 2015-09-28 17:38:45 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for BranchMergeProposals."""4"""Tests for BranchMergeProposals."""
@@ -514,7 +514,7 @@
514 commenter = self.factory.makePerson()514 commenter = self.factory.makePerson()
515 login_person(commenter)515 login_person(commenter)
516 result, events = self.assertNotifies(516 result, events = self.assertNotifies(
517 NewCodeReviewCommentEvent,517 NewCodeReviewCommentEvent, False,
518 merge_proposal.createComment,518 merge_proposal.createComment,
519 owner=commenter,519 owner=commenter,
520 subject='A review.')520 subject='A review.')
@@ -661,7 +661,7 @@
661 registrant = self.factory.makePerson()661 registrant = self.factory.makePerson()
662 result, events = self.assertNotifies(662 result, events = self.assertNotifies(
663 [NewBranchMergeProposalEvent,663 [NewBranchMergeProposalEvent,
664 BranchMergeProposalNeedsReviewEvent],664 BranchMergeProposalNeedsReviewEvent], False,
665 source_branch.addLandingTarget, registrant, target_branch,665 source_branch.addLandingTarget, registrant, target_branch,
666 needs_review=True)666 needs_review=True)
667 self.assertEqual(result, events[0].object)667 self.assertEqual(result, events[0].object)
@@ -674,7 +674,7 @@
674 product=source_branch.product)674 product=source_branch.product)
675 registrant = self.factory.makePerson()675 registrant = self.factory.makePerson()
676 result, events = self.assertNotifies(676 result, events = self.assertNotifies(
677 [NewBranchMergeProposalEvent],677 [NewBranchMergeProposalEvent], False,
678 source_branch.addLandingTarget, registrant, target_branch)678 source_branch.addLandingTarget, registrant, target_branch)
679 self.assertEqual(result, events[0].object)679 self.assertEqual(result, events[0].object)
680680
@@ -685,7 +685,7 @@
685 set_state=BranchMergeProposalStatus.WORK_IN_PROGRESS)685 set_state=BranchMergeProposalStatus.WORK_IN_PROGRESS)
686 with person_logged_in(bmp.registrant):686 with person_logged_in(bmp.registrant):
687 self.assertNotifies(687 self.assertNotifies(
688 [BranchMergeProposalNeedsReviewEvent],688 [BranchMergeProposalNeedsReviewEvent], False,
689 bmp.setStatus, BranchMergeProposalStatus.NEEDS_REVIEW)689 bmp.setStatus, BranchMergeProposalStatus.NEEDS_REVIEW)
690690
691 def test_needs_review_no_op(self):691 def test_needs_review_no_op(self):
@@ -1285,7 +1285,7 @@
1285 bmp = self.factory.makeBranchMergeProposal()1285 bmp = self.factory.makeBranchMergeProposal()
1286 login_person(bmp.target_branch.owner)1286 login_person(bmp.target_branch.owner)
1287 self.assertNotifies(1287 self.assertNotifies(
1288 ObjectModifiedEvent, notify_modified, bmp, bmp.markAsMerged,1288 ObjectModifiedEvent, False, notify_modified, bmp, bmp.markAsMerged,
1289 merge_reporter=bmp.target_branch.owner)1289 merge_reporter=bmp.target_branch.owner)
1290 self.assertEqual(BranchMergeProposalStatus.MERGED, bmp.queue_status)1290 self.assertEqual(BranchMergeProposalStatus.MERGED, bmp.queue_status)
1291 self.assertEqual(bmp.target_branch.owner, bmp.merge_reporter)1291 self.assertEqual(bmp.target_branch.owner, bmp.merge_reporter)
@@ -1305,7 +1305,7 @@
1305 login_person(merge_proposal.source_branch.owner)1305 login_person(merge_proposal.source_branch.owner)
1306 reviewer = self.factory.makePerson()1306 reviewer = self.factory.makePerson()
1307 result, events = self.assertNotifies(1307 result, events = self.assertNotifies(
1308 ReviewerNominatedEvent,1308 ReviewerNominatedEvent, False,
1309 merge_proposal.nominateReviewer,1309 merge_proposal.nominateReviewer,
1310 reviewer=reviewer,1310 reviewer=reviewer,
1311 registrant=merge_proposal.source_branch.owner)1311 registrant=merge_proposal.source_branch.owner)
13121312
=== modified file 'lib/lp/code/model/tests/test_codereviewcomment.py'
--- lib/lp/code/model/tests/test_codereviewcomment.py 2012-01-15 10:43:27 +0000
+++ lib/lp/code/model/tests/test_codereviewcomment.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for CodeReviewComment"""4"""Unit tests for CodeReviewComment"""
@@ -107,7 +107,8 @@
107 """Creating a CodeReviewComment should trigger a notification."""107 """Creating a CodeReviewComment should trigger a notification."""
108 message = self.factory.makeMessage()108 message = self.factory.makeMessage()
109 self.assertNotifies(109 self.assertNotifies(
110 NewCodeReviewCommentEvent, self.bmp.createCommentFromMessage,110 NewCodeReviewCommentEvent, False,
111 self.bmp.createCommentFromMessage,
111 message, None, None, original_email=None, _validate=False)112 message, None, None, original_email=None, _validate=False)
112113
113114
114115
=== modified file 'lib/lp/code/model/tests/test_codereviewkarma.py'
--- lib/lp/code/model/tests/test_codereviewkarma.py 2011-12-30 07:38:46 +0000
+++ lib/lp/code/model/tests/test_codereviewkarma.py 2015-10-06 15:19:46 +0000
@@ -1,10 +1,11 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for karma allocated for code reviews."""4"""Tests for karma allocated for code reviews."""
55
6__metaclass__ = type6__metaclass__ = type
77
8from lp.code.interfaces.branchmergeproposal import notify_modified
8from lp.registry.interfaces.karma import IKarmaAssignedEvent9from lp.registry.interfaces.karma import IKarmaAssignedEvent
9from lp.registry.interfaces.person import IPerson10from lp.registry.interfaces.person import IPerson
10from lp.testing import (11from lp.testing import (
@@ -113,7 +114,9 @@
113 proposal = self.factory.makeBranchMergeProposal()114 proposal = self.factory.makeBranchMergeProposal()
114 reviewer = proposal.target_branch.owner115 reviewer = proposal.target_branch.owner
115 self.karma_events = []116 self.karma_events = []
116 proposal.approveBranch(reviewer, "A rev id.")117 login_person(reviewer)
118 notify_modified(
119 proposal, proposal.approveBranch, reviewer, "A rev id.")
117 self.assertOneKarmaEvent(reviewer, 'branchmergeapproved')120 self.assertOneKarmaEvent(reviewer, 'branchmergeapproved')
118121
119 def test_approvingOwnCodeReview(self):122 def test_approvingOwnCodeReview(self):
@@ -123,7 +126,9 @@
123 proposal = self.factory.makeBranchMergeProposal(126 proposal = self.factory.makeBranchMergeProposal(
124 target_branch=target_branch, registrant=reviewer)127 target_branch=target_branch, registrant=reviewer)
125 self.karma_events = []128 self.karma_events = []
126 proposal.approveBranch(reviewer, "A rev id.")129 login_person(reviewer)
130 notify_modified(
131 proposal, proposal.approveBranch, reviewer, "A rev id.")
127 self.assertOneKarmaEvent(reviewer, 'branchmergeapprovedown')132 self.assertOneKarmaEvent(reviewer, 'branchmergeapprovedown')
128133
129 def test_rejectedCodeReview(self):134 def test_rejectedCodeReview(self):
@@ -132,7 +137,8 @@
132 proposal = self.factory.makeBranchMergeProposal()137 proposal = self.factory.makeBranchMergeProposal()
133 reviewer = proposal.target_branch.owner138 reviewer = proposal.target_branch.owner
134 self.karma_events = []139 self.karma_events = []
135 proposal.rejectBranch(reviewer, "A rev id.")140 login_person(reviewer)
141 notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.")
136 self.assertOneKarmaEvent(reviewer, 'branchmergerejected')142 self.assertOneKarmaEvent(reviewer, 'branchmergerejected')
137143
138 def test_rejectedOwnCodeReview(self):144 def test_rejectedOwnCodeReview(self):
@@ -144,5 +150,6 @@
144 proposal = self.factory.makeBranchMergeProposal(150 proposal = self.factory.makeBranchMergeProposal(
145 target_branch=target_branch, registrant=reviewer)151 target_branch=target_branch, registrant=reviewer)
146 self.karma_events = []152 self.karma_events = []
147 proposal.rejectBranch(reviewer, "A rev id.")153 login_person(reviewer)
154 notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.")
148 self.assertOneKarmaEvent(reviewer, 'branchmergerejectedown')155 self.assertOneKarmaEvent(reviewer, 'branchmergerejectedown')
149156
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-09-02 16:54:24 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-10-06 15:19:46 +0000
@@ -51,6 +51,7 @@
51 GitRepositoryExists,51 GitRepositoryExists,
52 GitTargetError,52 GitTargetError,
53 )53 )
54from lp.code.event.git import GitRefsUpdatedEvent
54from lp.code.interfaces.branchmergeproposal import (55from lp.code.interfaces.branchmergeproposal import (
55 BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,56 BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,
56 )57 )
@@ -1864,7 +1865,7 @@
1864 hosting_client.detectMerges = FakeMethod(1865 hosting_client.detectMerges = FakeMethod(
1865 result={source_1.commit_sha1: u"0" * 40})1866 result={source_1.commit_sha1: u"0" * 40})
1866 self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))1867 self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
1867 repository.createOrUpdateRefs({1868 refs_info = {
1868 u"refs/heads/target-1": {1869 u"refs/heads/target-1": {
1869 u"sha1": u"0" * 40,1870 u"sha1": u"0" * 40,
1870 u"type": GitObjectType.COMMIT,1871 u"type": GitObjectType.COMMIT,
@@ -1872,8 +1873,12 @@
1872 u"refs/heads/target-2": {1873 u"refs/heads/target-2": {
1873 u"sha1": u"1" * 40,1874 u"sha1": u"1" * 40,
1874 u"type": GitObjectType.COMMIT,1875 u"type": GitObjectType.COMMIT,
1875 }1876 },
1876 })1877 }
1878 expected_events = [
1879 ObjectModifiedEvent, ObjectModifiedEvent, GitRefsUpdatedEvent]
1880 _, events = self.assertNotifies(
1881 expected_events, True, repository.createOrUpdateRefs, refs_info)
1877 expected_args = [1882 expected_args = [
1878 (repository.getInternalPath(), target_1.commit_sha1,1883 (repository.getInternalPath(), target_1.commit_sha1,
1879 set([source_1.commit_sha1, source_2.commit_sha1])),1884 set([source_1.commit_sha1, source_2.commit_sha1])),
@@ -1888,6 +1893,15 @@
1888 BranchMergeProposalStatus.WORK_IN_PROGRESS, bmp2.queue_status)1893 BranchMergeProposalStatus.WORK_IN_PROGRESS, bmp2.queue_status)
1889 self.assertEqual(BranchMergeProposalStatus.MERGED, bmp3.queue_status)1894 self.assertEqual(BranchMergeProposalStatus.MERGED, bmp3.queue_status)
1890 self.assertEqual(u"0" * 40, bmp3.merged_revision_id)1895 self.assertEqual(u"0" * 40, bmp3.merged_revision_id)
1896 # The two ObjectModifiedEvents indicate the queue_status changes.
1897 self.assertContentEqual(
1898 [bmp1, bmp3], [event.object for event in events[:2]])
1899 self.assertContentEqual(
1900 [(BranchMergeProposalStatus.WORK_IN_PROGRESS,
1901 BranchMergeProposalStatus.MERGED)],
1902 set((event.object_before_modification.queue_status,
1903 event.object.queue_status)
1904 for event in events[:2]))
18911905
18921906
1893class TestGitRepositorySet(TestCaseWithFactory):1907class TestGitRepositorySet(TestCaseWithFactory):
18941908
=== modified file 'lib/lp/code/subscribers/branchmergeproposal.py'
--- lib/lp/code/subscribers/branchmergeproposal.py 2013-04-09 09:47:58 +0000
+++ lib/lp/code/subscribers/branchmergeproposal.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the1# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Event subscribers for branch merge proposals."""4"""Event subscribers for branch merge proposals."""
@@ -49,14 +49,19 @@
49 from_person = None49 from_person = None
50 else:50 else:
51 from_person = IPerson(event.user)51 from_person = IPerson(event.user)
52 # If the merge proposal was work in progress, then we don't want to send52 # If the merge proposal was work in progress and is now needs review,
53 # out an email as the needs review email will cover that.53 # then we don't want to send out an email as the needs review email will
54 # cover that.
54 old_status = event.object_before_modification.queue_status55 old_status = event.object_before_modification.queue_status
55 if old_status == BranchMergeProposalStatus.WORK_IN_PROGRESS:56 new_status = merge_proposal.queue_status
56 # Unless the new status is merged. If this occurs we really should57
57 # send out an email.58 in_progress_states = (
58 if merge_proposal.queue_status != BranchMergeProposalStatus.MERGED:59 BranchMergeProposalStatus.WORK_IN_PROGRESS,
59 return60 BranchMergeProposalStatus.NEEDS_REVIEW)
61
62 if (old_status == BranchMergeProposalStatus.WORK_IN_PROGRESS and
63 new_status in in_progress_states):
64 return
60 # Create a delta of the changes. If there are no changes to report, then65 # Create a delta of the changes. If there are no changes to report, then
61 # we're done.66 # we're done.
62 delta = BranchMergeProposalNoPreviewDiffDelta.construct(67 delta = BranchMergeProposalNoPreviewDiffDelta.construct(
6368
=== modified file 'lib/lp/code/subscribers/karma.py'
--- lib/lp/code/subscribers/karma.py 2015-04-22 16:11:40 +0000
+++ lib/lp/code/subscribers/karma.py 2015-10-06 15:19:46 +0000
@@ -3,6 +3,8 @@
33
4"""Assign karma for code domain activity."""4"""Assign karma for code domain activity."""
55
6from zope.principalregistry.principalregistry import UnauthenticatedPrincipal
7
6from lp.code.enums import BranchMergeProposalStatus8from lp.code.enums import BranchMergeProposalStatus
7from lp.registry.interfaces.person import IPerson9from lp.registry.interfaces.person import IPerson
8from lp.services.database.sqlbase import block_implicit_flushes10from lp.services.database.sqlbase import block_implicit_flushes
@@ -43,26 +45,33 @@
4345
4446
45@block_implicit_flushes47@block_implicit_flushes
46def branch_merge_status_changed(proposal, event):48def branch_merge_modified(proposal, event):
47 """Assign karma to the user who approved the merge."""49 """Assign karma to the user who approved or rejected the merge."""
50 if event.user is None or isinstance(event.user, UnauthenticatedPrincipal):
51 # Some modification events have no associated user context. In
52 # these cases there's no karma to assign.
53 return
54
48 if proposal.source_git_repository is not None:55 if proposal.source_git_repository is not None:
49 target = proposal.source_git_repository.namespace56 target = proposal.source_git_repository.namespace
50 else:57 else:
51 target = proposal.source_branch.target58 target = proposal.source_branch.target
52 user = IPerson(event.user)59 user = IPerson(event.user)
60 old_status = event.object_before_modification.queue_status
61 new_status = proposal.queue_status
5362
54 in_progress_states = (63 in_progress_states = (
55 BranchMergeProposalStatus.WORK_IN_PROGRESS,64 BranchMergeProposalStatus.WORK_IN_PROGRESS,
56 BranchMergeProposalStatus.NEEDS_REVIEW)65 BranchMergeProposalStatus.NEEDS_REVIEW)
5766
58 if ((event.to_state == BranchMergeProposalStatus.CODE_APPROVED) and67 if ((new_status == BranchMergeProposalStatus.CODE_APPROVED) and
59 (event.from_state in (in_progress_states))):68 (old_status in (in_progress_states))):
60 if user == proposal.registrant:69 if user == proposal.registrant:
61 target.assignKarma(user, 'branchmergeapprovedown')70 target.assignKarma(user, 'branchmergeapprovedown')
62 else:71 else:
63 target.assignKarma(user, 'branchmergeapproved')72 target.assignKarma(user, 'branchmergeapproved')
64 elif ((event.to_state == BranchMergeProposalStatus.REJECTED) and73 elif ((new_status == BranchMergeProposalStatus.REJECTED) and
65 (event.from_state in (in_progress_states))):74 (old_status in (in_progress_states))):
66 if user == proposal.registrant:75 if user == proposal.registrant:
67 target.assignKarma(user, 'branchmergerejectedown')76 target.assignKarma(user, 'branchmergerejectedown')
68 else:77 else:
6978
=== modified file 'lib/lp/codehosting/scanner/tests/test_mergedetection.py'
--- lib/lp/codehosting/scanner/tests/test_mergedetection.py 2013-06-20 05:50:00 +0000
+++ lib/lp/codehosting/scanner/tests/test_mergedetection.py 2015-10-06 15:19:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the scanner's merge detection."""4"""Tests for the scanner's merge detection."""
@@ -8,6 +8,7 @@
8import logging8import logging
99
10from bzrlib.revision import NULL_REVISION10from bzrlib.revision import NULL_REVISION
11from lazr.lifecycle.event import ObjectModifiedEvent
11import transaction12import transaction
12from zope.component import getUtility13from zope.component import getUtility
13from zope.event import notify14from zope.event import notify
@@ -271,7 +272,8 @@
271 self.assertNotEqual(272 self.assertNotEqual(
272 BranchLifecycleStatus.MERGED,273 BranchLifecycleStatus.MERGED,
273 proposal.source_branch.lifecycle_status)274 proposal.source_branch.lifecycle_status)
274 mergedetection.merge_detected(275 _, [event] = self.assertNotifies(
276 [ObjectModifiedEvent], True, mergedetection.merge_detected,
275 logging.getLogger(),277 logging.getLogger(),
276 proposal.source_branch, proposal.target_branch, proposal)278 proposal.source_branch, proposal.target_branch, proposal)
277 self.assertEqual(279 self.assertEqual(
@@ -279,6 +281,12 @@
279 self.assertEqual(281 self.assertEqual(
280 BranchLifecycleStatus.MERGED,282 BranchLifecycleStatus.MERGED,
281 proposal.source_branch.lifecycle_status)283 proposal.source_branch.lifecycle_status)
284 self.assertEqual(proposal, event.object)
285 self.assertEqual(
286 BranchMergeProposalStatus.WORK_IN_PROGRESS,
287 event.object_before_modification.queue_status)
288 self.assertEqual(
289 BranchMergeProposalStatus.MERGED, event.object.queue_status)
282 job = IStore(proposal).find(290 job = IStore(proposal).find(
283 BranchMergeProposalJob,291 BranchMergeProposalJob,
284 BranchMergeProposalJob.branch_merge_proposal == proposal,292 BranchMergeProposalJob.branch_merge_proposal == proposal,
285293
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2015-09-30 00:38:30 +0000
+++ lib/lp/testing/__init__.py 2015-10-06 15:19:46 +0000
@@ -530,11 +530,14 @@
530 from lp.testing.matchers import Provides530 from lp.testing.matchers import Provides
531 self.assertThat(obj, Provides(interface))531 self.assertThat(obj, Provides(interface))
532532
533 def assertNotifies(self, event_types, callable_obj, *args, **kwargs):533 def assertNotifies(self, event_types, propagate, callable_obj,
534 *args, **kwargs):
534 """Assert that a callable performs a given notification.535 """Assert that a callable performs a given notification.
535536
536 :param event_type: One or more event types that notification is537 :param event_type: One or more event types that notification is
537 expected for.538 expected for.
539 :param propagate: If True, propagate events to their normal
540 subscribers.
538 :param callable_obj: The callable to call.541 :param callable_obj: The callable to call.
539 :param *args: The arguments to pass to the callable.542 :param *args: The arguments to pass to the callable.
540 :param **kwargs: The keyword arguments to pass to the callable.543 :param **kwargs: The keyword arguments to pass to the callable.
@@ -543,7 +546,7 @@
543 """546 """
544 if not isinstance(event_types, (list, tuple)):547 if not isinstance(event_types, (list, tuple)):
545 event_types = [event_types]548 event_types = [event_types]
546 with EventRecorder() as recorder:549 with EventRecorder(propagate=propagate) as recorder:
547 result = callable_obj(*args, **kwargs)550 result = callable_obj(*args, **kwargs)
548 if len(recorder.events) == 0:551 if len(recorder.events) == 0:
549 raise AssertionError('No notification was performed.')552 raise AssertionError('No notification was performed.')
@@ -1241,21 +1244,27 @@
1241class EventRecorder:1244class EventRecorder:
1242 """Intercept and record Zope events.1245 """Intercept and record Zope events.
12431246
1244 This prevents the events from propagating to their normal subscribers.1247 This prevents the events from propagating to their normal subscribers,
1245 The recorded events can be accessed via the 'events' list.1248 unless `propagate=True` is passed to the constructor. The recorded
1249 events can be accessed via the 'events' list.
1246 """1250 """
12471251
1248 def __init__(self):1252 def __init__(self, propagate=False):
1253 self.propagate = propagate
1249 self.events = []1254 self.events = []
1250 self.old_subscribers = None1255 self.old_subscribers = None
1256 self.new_subscribers = None
12511257
1252 def __enter__(self):1258 def __enter__(self):
1253 self.old_subscribers = zope.event.subscribers[:]1259 self.old_subscribers = zope.event.subscribers[:]
1254 zope.event.subscribers[:] = [self.events.append]1260 if not self.propagate:
1261 zope.event.subscribers[:] = []
1262 zope.event.subscribers.append(self.events.append)
1263 self.new_subscribers = zope.event.subscribers[:]
1255 return self1264 return self
12561265
1257 def __exit__(self, exc_type, exc_value, traceback):1266 def __exit__(self, exc_type, exc_value, traceback):
1258 assert zope.event.subscribers == [self.events.append], (1267 assert zope.event.subscribers == self.new_subscribers, (
1259 'Subscriber list has been changed while running!')1268 'Subscriber list has been changed while running!')
1260 zope.event.subscribers[:] = self.old_subscribers1269 zope.event.subscribers[:] = self.old_subscribers
12611270