Merge lp:~thumper/launchpad/auto-approve into lp:launchpad/db-devel

Proposed by Tim Penhey
Status: Rejected
Rejected by: Stuart Bishop
Proposed branch: lp:~thumper/launchpad/auto-approve
Merge into: lp:launchpad/db-devel
Diff against target: 773 lines (has conflicts)
Text conflict in lib/lp/code/model/branch.py
To merge this branch: bzr merge lp:~thumper/launchpad/auto-approve
Reviewer Review Type Date Requested Status
Jonathan Lange (community) code Approve
Stuart Bishop (community) db Needs Resubmitting
Review via email: mp+7087@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Here is a summary of the changes:

 database/schema/comments.sql | 1
 database/schema/patch-2109-99-0.sql | 7

The database patch to add a column to products for recording the auto approval policy.

 lib/lp/registry/configure.zcml | 2
 lib/lp/registry/interfaces/product.py | 7
 lib/lp/registry/model/product.py | 7

Adding the review_approval_policy to the Product.

 lib/lp/code/interfaces/branchtarget.py | 4
 lib/lp/code/model/branchtarget.py | 17 +

Adding the review_approval_policy to IBranchTarget.

 lib/lp/code/interfaces/codereviewvote.py | 10
 lib/lp/code/model/codereviewvote.py | 10

Add a helper attribute to get the vote from the code review comment.

 lib/lp/code/model/branchmergeproposal.py | 22 +
 lib/lp/code/model/tests/test_branchmergeproposals.py | 53 +++

Add in the auto approval check.

 lib/lp/code/interfaces/reviewapprovalpolicies.py | 67 ++++
 lib/lp/code/model/reviewapprovalpolicies.py | 124 ++++++++
 lib/lp/code/model/tests/test_reviewapprovalpolicies.py | 244 +++++++++++++++++

The policies themselves and their tests.

 14 files changed, 572 insertions(+), 3 deletions(-)

Tests:
  TestAutomaticProposalAppoval
  test_reviewapprovalpolicies

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (32.6 KiB)

Hi Tim,

Thanks for looking into this feature. People have been bugging us about it
forever, and it'll be great to give them what they want.

I notice that there's no UI for this yet. That's OK -- it's a big enough
branch and the features as implemented make sense as an atomic whole. However,
we really should get a UI asap, since we don't want this code lying dormant.

I've got quite a few comments and questions, some of them trivial, some of
them not. Overall, the code looks really good. I'm particularly pleased to see
the thorough policy tests & the clean design.

cheers,
jml

> === modified file 'database/schema/comments.sql'
> --- database/schema/comments.sql 2009-05-14 09:57:46 +0000
> +++ database/schema/comments.sql 2009-06-04 23:29:41 +0000
> @@ -593,6 +593,7 @@
> COMMENT ON COLUMN Product.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.';
> COMMENT ON COLUMN Product.license_approved IS 'The Other/Open Source license has been approved by an administrator.';
> COMMENT ON COLUMN Product.remote_product IS 'The ID of this product on its remote bug tracker.';
> +COMMENT ON COLUMN Product.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy to define the requirements for automatically approving or rejecting proposals based on the code reviews';
>

Please change "to define" to "that defines".

> -- ProductLicense
> COMMENT ON TABLE ProductLicense IS 'The licenses that cover the software for a product.';
>
> === added file 'database/schema/patch-2109-99-0.sql'
> --- database/schema/patch-2109-99-0.sql 1970-01-01 00:00:00 +0000
> +++ database/schema/patch-2109-99-0.sql 2009-06-04 23:29:41 +0000
> @@ -0,0 +1,7 @@
> +SET client_min_messages = ERROR;
> +
> +ALTER TABLE Product
> + ADD COLUMN review_approval_policy INT DEFAULT 1 NOT NULL;
> +
> +INSERT INTO LaunchpadDatabaseRevision VALUES (2109, 99, 0);
> +
>
> === modified file 'lib/lp/code/interfaces/branchtarget.py'
> --- lib/lp/code/interfaces/branchtarget.py 2009-05-19 06:12:59 +0000
> +++ lib/lp/code/interfaces/branchtarget.py 2009-06-04 23:23:15 +0000
> @@ -87,6 +87,10 @@
> supports_merge_proposals = Attribute(
> "Does this target support merge proposals at all?")
>
> + review_approval_policy = Attribute(
> + "The policy object relating to the approval policy set for the "
> + "target.")
> +
> def areBranchesMergeable(other_target):
> """Are branches from other_target mergeable into this target."""
>
>
> === modified file 'lib/lp/code/interfaces/codereviewvote.py'
> --- lib/lp/code/interfaces/codereviewvote.py 2009-04-17 10:32:16 +0000
> +++ lib/lp/code/interfaces/codereviewvote.py 2009-06-05 00:31:22 +0000
> @@ -8,14 +8,14 @@
> ]
>
> from zope.interface import Interface
> -from zope.schema import Datetime, Int, TextLine
> +from zope.schema import Choice, Datetime, Int, TextLine
>
> from canonical.launchpad import _
> from canonical.launchpad.fields import PublicPersonChoice
> from lp.code.interfaces.branchmergeproposal import (
> IBranchMergeProposal)
> from lp.code.interf...

review: Needs Fixing (code)
Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (24.9 KiB)

On Fri, 05 Jun 2009 17:18:42 Jonathan Lange wrote:
> Review: Needs Fixing code
> Hi Tim,
>
> Thanks for looking into this feature. People have been bugging us about it
> forever, and it'll be great to give them what they want.
>
> I notice that there's no UI for this yet. That's OK -- it's a big enough
> branch and the features as implemented make sense as an atomic whole.
> However, we really should get a UI asap, since we don't want this code
> lying dormant.

Yes there is no web UI for this yet. However I made sure that the method to
set the review policy is exposed through the API. Beuno assures me that an
enum picker has or is about to be landed, so I suggest that we use this to set
the review policy. Since the method is exposed through the API already it
*should* be simple.

> I've got quite a few comments and questions, some of them trivial, some of
> them not. Overall, the code looks really good. I'm particularly pleased to
> see the thorough policy tests & the clean design.

Thanks.

> > === modified file 'database/schema/comments.sql'
> > --- database/schema/comments.sql 2009-05-14 09:57:46 +0000
> > +++ database/schema/comments.sql 2009-06-04 23:29:41 +0000
> > @@ -593,6 +593,7 @@
> > COMMENT ON COLUMN Product.reviewer_whiteboard IS 'A whiteboard for
> > Launchpad admins, registry experts and the project owners to capture the
> > state of current issues with the project.'; COMMENT ON COLUMN
> > Product.license_approved IS 'The Other/Open Source license has been
> > approved by an administrator.'; COMMENT ON COLUMN Product.remote_product
> > IS 'The ID of this product on its remote bug tracker.'; +COMMENT ON
> > COLUMN Product.review_approval_policy IS 'An enumerated value referring
> > to the CodeReviewApprovalPolicy to define the requirements for
> > automatically approving or rejecting proposals based on the code
> > reviews';
>
> Please change "to define" to "that defines".

Done

> > === modified file 'lib/lp/code/interfaces/codereviewvote.py'
> > --- lib/lp/code/interfaces/codereviewvote.py 2009-04-17 10:32:16 +0000
> > +++ lib/lp/code/interfaces/codereviewvote.py 2009-06-05 00:31:22 +0000
> > @@ -8,14 +8,14 @@
> > ]
> >
> > from zope.interface import Interface
> > -from zope.schema import Datetime, Int, TextLine
> > +from zope.schema import Choice, Datetime, Int, TextLine
> >
> > from canonical.launchpad import _
> > from canonical.launchpad.fields import PublicPersonChoice
> > from lp.code.interfaces.branchmergeproposal import (
> > IBranchMergeProposal)
> > from lp.code.interfaces.codereviewcomment import (
> > - ICodeReviewComment)
> > + CodeReviewVote, ICodeReviewComment)
> > from lazr.restful.fields import Reference
> > from lazr.restful.declarations import (
> > export_as_webservice_entry, exported)
> > @@ -66,3 +66,9 @@
> > "The code review comment that contains the most recent
> > vote." ),
> > required=True, schema=ICodeReviewComment))
> > +
> > + vote = exported(
> > + Choice(
> > + description=_('The last review by the reviewer.'),
> > + required=False, readonly=True,
> > + vocabulary=CodeReviewVote))
>
> Th...

1=== modified file 'database/schema/comments.sql'
2--- database/schema/comments.sql 2009-06-04 23:29:41 +0000
3+++ database/schema/comments.sql 2009-06-07 21:55:53 +0000
4@@ -593,7 +593,7 @@
5 COMMENT ON COLUMN Product.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.';
6 COMMENT ON COLUMN Product.license_approved IS 'The Other/Open Source license has been approved by an administrator.';
7 COMMENT ON COLUMN Product.remote_product IS 'The ID of this product on its remote bug tracker.';
8-COMMENT ON COLUMN Product.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy to define the requirements for automatically approving or rejecting proposals based on the code reviews';
9+COMMENT ON COLUMN Product.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy that defines the requirements for automatically approving or rejecting proposals based on the code reviews';
10
11 -- ProductLicense
12 COMMENT ON TABLE ProductLicense IS 'The licenses that cover the software for a product.';
13
14=== modified file 'lib/lp/code/interfaces/reviewapprovalpolicies.py'
15--- lib/lp/code/interfaces/reviewapprovalpolicies.py 2009-06-04 04:37:54 +0000
16+++ lib/lp/code/interfaces/reviewapprovalpolicies.py 2009-06-07 22:05:57 +0000
17@@ -1,9 +1,12 @@
18 # Copyright 2009 Canonical Ltd. All rights reserved.
19
20-"""Implementation classes for code review approval policies."""
21+"""Interfaces and enums for code review approval policies."""
22
23 __metaclass__ = type
24-__all__ = []
25+__all__ = [
26+ 'CodeReviewApprovalPolicy',
27+ 'ICodeReviewApprovalPolicy',
28+ ]
29
30
31 from zope.interface import Interface
32@@ -15,8 +18,9 @@
33 """Policies for automatically approving code reviews.
34
35 In all the situations mentioned below, reviewer refers to a member of the
36- review team for the target branch. There has to be reviews by people who
37- are in a position to say yay or nay for the merge.
38+ review team for the target branch. Only reviews by people in a position
39+ to approve the proposal are consulted to automatically change the proposal
40+ state.
41 """
42
43 NO_AUTO_CHANGE = DBItem(1, """
44
45=== modified file 'lib/lp/code/model/branchmergeproposal.py'
46--- lib/lp/code/model/branchmergeproposal.py 2009-06-05 01:39:33 +0000
47+++ lib/lp/code/model/branchmergeproposal.py 2009-06-07 22:08:53 +0000
48@@ -659,9 +659,10 @@
49
50 def _autoApproveCheck(self, reviewer):
51 """Check the approval policy for this proposal."""
52+ # Only look to change the status if the proposal needs review.
53+ if self.queue_status != BranchMergeProposalStatus.NEEDS_REVIEW:
54+ return None
55 # Get all the reviews pending and done by members of the review team.
56- if self.queue_status != BranchMergeProposalStatus.NEEDS_REVIEW:
57- return None
58 reviews = [
59 (reference.reviewer, reference.vote) for reference in self.votes
60 if self.isPersonValidReviewer(reference.reviewer)]
61@@ -673,9 +674,11 @@
62 self.approveBranch(reviewer, tip_rev_id)
63 elif next_status == BranchMergeProposalStatus.REJECTED:
64 self.rejectBranch(reviewer, tip_rev_id)
65- else:
66+ elif next_status in None:
67 # Nothing to do.
68 pass
69+ else:
70+ raise AssertionError('Unexpected result from approval policy.')
71
72 def updatePreviewDiff(self, diff_content, diff_stat,
73 source_revision_id, target_revision_id,
74
75=== modified file 'lib/lp/code/model/reviewapprovalpolicies.py'
76--- lib/lp/code/model/reviewapprovalpolicies.py 2009-06-04 04:37:54 +0000
77+++ lib/lp/code/model/reviewapprovalpolicies.py 2009-06-07 22:24:18 +0000
78@@ -19,11 +19,31 @@
79 CodeReviewApprovalPolicy, ICodeReviewApprovalPolicy)
80
81
82+APPROVAL_POLICIES = {}
83+
84+
85+class PolicyMetaClass(type):
86+ """A meta-class to automatically register the policies."""
87+
88+ def __new__(cls, classname, bases, classdict):
89+ """Create the policy and add it to the global dict."""
90+ instance = type.__new__(cls, classname, bases, classdict)
91+ # If the class has a policy attribute, register with the global dict.
92+ if 'policy' in classdict:
93+ global APPROVAL_POLICIES
94+ APPROVAL_POLICIES[instance.policy] = instance
95+ return instance
96+
97+
98 class NoChangePolicy:
99- """A policy where no changes are made."""
100+ """A policy where no changes are made automatically."""
101+
102+ __metaclass__ = PolicyMetaClass
103
104 implements(ICodeReviewApprovalPolicy)
105
106+ policy = CodeReviewApprovalPolicy.NO_AUTO_CHANGE
107+
108 def checkReviews(self, reviews):
109 """No change, return None."""
110 return None
111@@ -32,6 +52,8 @@
112 class BasePolicy:
113 """A base policy class to sum review types."""
114
115+ __metaclass__ = PolicyMetaClass
116+
117 def countReviews(self, reviews):
118 """Return a dict of review counts."""
119 result = {
120@@ -56,6 +78,8 @@
121
122 implements(ICodeReviewApprovalPolicy)
123
124+ policy = CodeReviewApprovalPolicy.ONE_REVIEWER
125+
126 def checkReviews(self, reviews):
127 """See `ICodeReviewApprovalPolicy`."""
128 counts = self.countReviews(reviews)
129@@ -74,6 +98,8 @@
130
131 implements(ICodeReviewApprovalPolicy)
132
133+ policy = CodeReviewApprovalPolicy.TWO_REVIEWERS
134+
135 def checkReviews(self, reviews):
136 """See `ICodeReviewApprovalPolicy`."""
137 counts = self.countReviews(reviews)
138@@ -95,6 +121,8 @@
139
140 implements(ICodeReviewApprovalPolicy)
141
142+ policy = CodeReviewApprovalPolicy.ALL_REQUESTED_COMPLETE
143+
144 def checkReviews(self, reviews):
145 """See `ICodeReviewApprovalPolicy`."""
146 counts = self.countReviews(reviews)
147@@ -113,12 +141,8 @@
148
149 def get_approval_policy(policy):
150 """Return a policy class based on the enumerated value."""
151- if policy == CodeReviewApprovalPolicy.NO_AUTO_CHANGE:
152- return NoChangePolicy()
153- if policy == CodeReviewApprovalPolicy.ONE_REVIEWER:
154- return OneReviewerPolicy()
155- if policy == CodeReviewApprovalPolicy.TWO_REVIEWERS:
156- return TwoReviewersPolicy()
157- if policy == CodeReviewApprovalPolicy.ALL_REQUESTED_COMPLETE:
158- return ReviewsCompletePolicy()
159- raise AssertionError("Invalid policy value: %s" % policy)
160+ try:
161+ policy_class = APPROVAL_POLICIES[policy]
162+ return policy_class()
163+ except KeyError:
164+ raise AssertionError("Invalid policy value: %s" % policy)
165
166=== modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py'
167--- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-05 01:18:29 +0000
168+++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-07 22:33:19 +0000
169@@ -1418,30 +1418,30 @@
170 set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
171 product=self.product)
172
173- def assertReviewState(self, reviewer, vote, expected):
174- """Have `reviewer` comment on the proposal and check the status."""
175+ def addReview(self, reviewer, vote):
176+ """Add a review by the reviewer with the specified vote."""
177 login_person(reviewer)
178 self.bmp.createComment(
179 reviewer, subject=None, content="comment", vote=vote)
180- self.assertEqual(expected, self.bmp.queue_status)
181+
182+ def assertReviewState(self, reviewer, vote, expected):
183+ """Have `reviewer` comment on the proposal and check the status."""
184
185 def test_auto_approval(self):
186 # If the product of the target branch has a policy to auto approve the
187 # proposal with a single review, then an approve review will cause the
188 # state to move from NEEDS_REVIEW to CODE_APPROVED.
189- self.assertReviewState(
190- reviewer=self.bmp.target_branch.owner,
191- vote=CodeReviewVote.APPROVE,
192- expected=BranchMergeProposalStatus.CODE_APPROVED)
193+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.APPROVE)
194+ self.assertEqual(
195+ BranchMergeProposalStatus.CODE_APPROVED, self.bmp.queue_status)
196
197 def test_auto_rejection(self):
198 # If the product of the target branch has a policy to auto reject the
199 # proposal with a single review, then a disapprove review will cause
200 # the state to move from NEEDS_REVIEW to REJECTED.
201- self.assertReviewState(
202- reviewer=self.bmp.target_branch.owner,
203- vote=CodeReviewVote.DISAPPROVE,
204- expected=BranchMergeProposalStatus.REJECTED)
205+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.DISAPPROVE)
206+ self.assertEqual(
207+ BranchMergeProposalStatus.REJECTED, self.bmp.queue_status)
208
209 def test_not_needs_review(self):
210 # Auto approval only happens if the proposal is in NEEDS_REVIEW state.
211@@ -1449,10 +1449,9 @@
212 # status of the proposal.
213 login_person(self.bmp.registrant)
214 self.bmp.setAsWorkInProgress()
215- self.assertReviewState(
216- reviewer=self.bmp.target_branch.owner,
217- vote=CodeReviewVote.APPROVE,
218- expected=BranchMergeProposalStatus.WORK_IN_PROGRESS)
219+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.APPROVE)
220+ self.assertEqual(
221+ BranchMergeProposalStatus.WORK_IN_PROGRESS, self.bmp.queue_status)
222
223
224 def test_suite():
225
226=== modified file 'lib/lp/code/model/tests/test_reviewapprovalpolicies.py'
227--- lib/lp/code/model/tests/test_reviewapprovalpolicies.py 2009-06-04 04:37:54 +0000
228+++ lib/lp/code/model/tests/test_reviewapprovalpolicies.py 2009-06-07 22:37:57 +0000
229@@ -15,7 +15,7 @@
230 from lp.code.model.reviewapprovalpolicies import (
231 get_approval_policy, NoChangePolicy, OneReviewerPolicy,
232 ReviewsCompletePolicy, TwoReviewersPolicy)
233-from lp.testing import login_person, TestCase, TestCaseWithFactory
234+from lp.testing import TestCase, TestCaseWithFactory
235
236
237 class TestGetApprovalPolicy(TestCase):
238@@ -24,27 +24,27 @@
239 def test_no_change_policy(self):
240 # NO_AUTO_CHANGE creates a NoChangePolicy.
241 policy = get_approval_policy(CodeReviewApprovalPolicy.NO_AUTO_CHANGE)
242- self.assertTrue(isinstance(policy, NoChangePolicy))
243- self.assertTrue(ICodeReviewApprovalPolicy.providedBy(policy))
244+ self.assertIsInstance(policy, NoChangePolicy)
245+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
246
247 def test_one_reviewer_policy(self):
248 # ONE_REVIEWER creates a OneReviewerPolicy.
249 policy = get_approval_policy(CodeReviewApprovalPolicy.ONE_REVIEWER)
250- self.assertTrue(isinstance(policy, OneReviewerPolicy))
251- self.assertTrue(ICodeReviewApprovalPolicy.providedBy(policy))
252+ self.assertIsInstance(policy, OneReviewerPolicy)
253+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
254
255 def test_two_reviewers_policy(self):
256 # TWO_REVIEWERS creates a TwoReviewersPolicy.
257 policy = get_approval_policy(CodeReviewApprovalPolicy.TWO_REVIEWERS)
258- self.assertTrue(isinstance(policy, TwoReviewersPolicy))
259- self.assertTrue(ICodeReviewApprovalPolicy.providedBy(policy))
260+ self.assertIsInstance(policy, TwoReviewersPolicy)
261+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
262
263 def test_all_reviews_completed_policy(self):
264 # ALL_REQUESTED_COMPLETE creates a ReviewsCompletePolicy.
265 policy = get_approval_policy(
266 CodeReviewApprovalPolicy.ALL_REQUESTED_COMPLETE)
267- self.assertTrue(isinstance(policy, ReviewsCompletePolicy))
268- self.assertTrue(ICodeReviewApprovalPolicy.providedBy(policy))
269+ self.assertIsInstance(policy, ReviewsCompletePolicy)
270+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
271
272
273 class TestNoChangePolicy(TestCaseWithFactory):
lp:~thumper/launchpad/auto-approve updated
8127. By Tim Penhey

Added simple metaclass for policy discovery.

8128. By Tim Penhey

Grammar improvement.

8129. By Tim Penhey

Small comment fixes and extra assert.

8130. By Tim Penhey

Test improvements.

Revision history for this message
Stuart Bishop (stub) wrote :
Download full text (7.5 KiB)

As discussed, the flag needs to move from the Product table to the Branch table to support future use cases.

(13:23:44) stub: thumper: You sure an enum for the code review approval policy is enough? I thought that might end up as a new table containing several booleans or enums to twiddle with.
(13:25:31) stub: The column name is going to need to change though ('review' is generic, and in in particular confusing with Product.reviewed and Product.reviewer_whiteboard already existing. Need to be explicit with code_revew_approval_policy I think)
13:30
14:45
(14:48:01) Tim Penhey (thumper): stub: there is no way I'm going to suggest that we support complexly defined review policies
(14:48:03) Tim Penhey (thumper): stub: I've defined the four most common (as I see it)
(14:48:14) Tim Penhey (thumper): stub: and they are selectable through an enum
(14:48:20) stub: ok
(14:48:23) Tim Penhey (thumper): stub: right now we only support products
(14:48:46) Tim Penhey (thumper): stub: but I'd like to support package sets when they exist
(14:49:29) Tim Penhey (thumper): stub: the only other idea is perhaps we may want to be able to define policies on a branch basis
(14:49:35) Tim Penhey (thumper): stub: which would allow us to set it for certain package branches
(14:49:47) Tim Penhey (thumper): stub: or to define different policies for different branches on a given product
(14:49:53) Tim Penhey (thumper): stub: what are your thoughts on this?
(14:50:16) Tim Penhey (thumper): stub: the reason I've not done it yet is that 99% of branches won't need to set one
(14:50:24) Tim Penhey (thumper): stub: so I'm loathed to put it on to the branch table
(14:50:47) stub: Would we enable it for non-trunk branches in the UI?
(14:51:01) Tim Penhey (thumper): we could
(14:51:09) Tim Penhey (thumper): right now I don't suggest that we do
(14:51:12) Tim Penhey (thumper): but...
(14:51:16) ***Tim Penhey (thumper) shrugs
(14:51:47) stub: It seems silly to even display if for development branches, which are sources not sinks in general.
(14:51:58) Tim Penhey (thumper): exactly
(14:52:13) Tim Penhey (thumper): another reason why I don't really want it on branches
(14:52:25) stub: Where will the flag go for package branches (assuming we don't shuffle it to the Branch table)?
(14:52:39) Tim Penhey (thumper): on the yet to be defined package sets
(14:53:52) stub: A package can be in multiple packagesets
(14:54:27) Tim Penhey (thumper): ah, poos
(14:54:29) stub: So I don't see how it fits there
(14:54:38) Tim Penhey (thumper): I'm not sure where to set it for package branches
(14:54:51) stub: Moving the flag to Branch might be the best approach.
(14:54:57) Tim Penhey (thumper): or even how often it'll be requested
(14:55:06) stub: Even if it is NULL for most Branches.
(14:55:31) Tim Penhey (thumper): hmm...
(14:56:02) stub: I suspect the code review process for packages will be YAGNI myself - are there big teams that coordinate to create packages?
(14:56:21) stub: Maybe kernel, and their processes are probably more complex than we can support in code review already.
(14:56:32) Tim Penhey (thumper): we could enable the UI for series branches
(14:57:20) Tim Penhey (thumper): or offi...

Read more...

review: Needs Resubmitting (db)
lp:~thumper/launchpad/auto-approve updated
8131. By Tim Penhey

Move the enum from Product -> Branch and fix everything.

Revision history for this message
Tim Penhey (thumper) wrote :

Moved column from product to branch.

Fixed everything else.

Should be good now.

1=== modified file 'database/schema/comments.sql'
2--- database/schema/comments.sql 2009-06-07 22:27:26 +0000
3+++ database/schema/comments.sql 2009-06-09 01:25:39 +0000
4@@ -45,6 +45,7 @@
5 COMMENT ON COLUMN Branch.stacked_on IS 'The Launchpad branch that this branch is stacked on (if any).';
6 COMMENT ON COLUMN Branch.distroseries IS 'The distribution series that the branch belongs to.';
7 COMMENT ON COLUMN Branch.sourcepackagename IS 'The source package this is a branch of.';
8+COMMENT ON COLUMN Branch.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy that defines the requirements for automatically approving or rejecting proposals based on the code reviews';
9
10 -- BranchJob
11
12@@ -593,7 +594,6 @@
13 COMMENT ON COLUMN Product.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.';
14 COMMENT ON COLUMN Product.license_approved IS 'The Other/Open Source license has been approved by an administrator.';
15 COMMENT ON COLUMN Product.remote_product IS 'The ID of this product on its remote bug tracker.';
16-COMMENT ON COLUMN Product.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy that defines the requirements for automatically approving or rejecting proposals based on the code reviews';
17
18 -- ProductLicense
19 COMMENT ON TABLE ProductLicense IS 'The licenses that cover the software for a product.';
20
21=== modified file 'database/schema/patch-2109-99-0.sql'
22--- database/schema/patch-2109-99-0.sql 2009-06-04 23:29:41 +0000
23+++ database/schema/patch-2109-99-0.sql 2009-06-09 01:25:39 +0000
24@@ -1,6 +1,6 @@
25 SET client_min_messages = ERROR;
26
27-ALTER TABLE Product
28+ALTER TABLE Branch
29 ADD COLUMN review_approval_policy INT DEFAULT 1 NOT NULL;
30
31 INSERT INTO LaunchpadDatabaseRevision VALUES (2109, 99, 0);
32
33=== modified file 'lib/lp/code/configure.zcml'
34--- lib/lp/code/configure.zcml 2009-05-20 21:39:25 +0000
35+++ lib/lp/code/configure.zcml 2009-06-09 01:25:39 +0000
36@@ -401,6 +401,7 @@
37 commitsForDays
38 needs_upgrading
39 isBranchMergeable
40+ review_approval_policy
41 "/>
42 <require
43 permission="launchpad.Edit"
44@@ -412,7 +413,7 @@
45 last_scanned last_scanned_id revision_count branch_type
46 reviewer branch_format repository_format
47 control_format stacked_on merge_queue
48- merge_control_status"/>
49+ merge_control_status review_approval_policy"/>
50 <require
51 permission="launchpad.AnyPerson"
52 set_attributes="whiteboard"/>
53
54=== modified file 'lib/lp/code/interfaces/branch.py'
55--- lib/lp/code/interfaces/branch.py 2009-05-20 21:39:25 +0000
56+++ lib/lp/code/interfaces/branch.py 2009-06-09 01:25:39 +0000
57@@ -87,6 +87,7 @@
58 from canonical.launchpad.validators import LaunchpadValidationError
59 from lp.code.interfaces.branchlookup import IBranchLookup
60 from lp.code.interfaces.branchtarget import IHasBranchTarget
61+from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
62 from canonical.launchpad.interfaces.launchpad import (
63 IHasOwner, ILaunchpadCelebrities)
64 from lp.registry.interfaces.person import IPerson
65@@ -895,6 +896,12 @@
66 readonly=True,
67 value_type=Reference(IPerson)))
68
69+ review_approval_policy = exported(
70+ Choice(
71+ title=_("The policy defining the automatical approval of code "
72+ " reviews"),
73+ vocabulary=CodeReviewApprovalPolicy))
74+
75 date_created = exported(
76 Datetime(
77 title=_('Date Created'),
78
79=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
80--- lib/lp/code/interfaces/branchmergeproposal.py 2009-05-13 20:03:39 +0000
81+++ lib/lp/code/interfaces/branchmergeproposal.py 2009-06-09 01:25:39 +0000
82@@ -303,6 +303,10 @@
83 description=_('Any emails sent to this address will result'
84 'in comments being added.')))
85
86+ review_approval_policy = Attribute(
87+ "The policy object relating to the approval policy set for the "
88+ "target branch.")
89+
90 @operation_parameters(
91 id=Int(
92 title=_("A CodeReviewComment ID.")))
93
94=== modified file 'lib/lp/code/interfaces/branchtarget.py'
95--- lib/lp/code/interfaces/branchtarget.py 2009-06-04 23:23:15 +0000
96+++ lib/lp/code/interfaces/branchtarget.py 2009-06-09 01:25:39 +0000
97@@ -87,10 +87,6 @@
98 supports_merge_proposals = Attribute(
99 "Does this target support merge proposals at all?")
100
101- review_approval_policy = Attribute(
102- "The policy object relating to the approval policy set for the "
103- "target.")
104-
105 def areBranchesMergeable(other_target):
106 """Are branches from other_target mergeable into this target."""
107
108
109=== modified file 'lib/lp/code/model/branch.py'
110--- lib/lp/code/model/branch.py 2009-05-20 21:39:25 +0000
111+++ lib/lp/code/model/branch.py 2009-06-09 01:25:39 +0000
112@@ -41,6 +41,7 @@
113
114 from lp.code.interfaces.branch import (BranchFormat, RepositoryFormat,
115 BRANCH_FORMAT_UPGRADE_PATH, REPOSITORY_FORMAT_UPGRADE_PATH)
116+from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
117 from lp.code.mail.branch import send_branch_modified_notifications
118 from lp.code.model.branchmergeproposal import (
119 BranchMergeProposal)
120@@ -196,6 +197,11 @@
121 joinColumn='branch',
122 orderBy='id')
123
124+ review_approval_policy = EnumCol(
125+ dbName='review_approval_policy', notNull=True,
126+ schema=CodeReviewApprovalPolicy,
127+ default=CodeReviewApprovalPolicy.NO_AUTO_CHANGE)
128+
129 date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
130 date_last_modified = UtcDateTimeCol(notNull=True, default=DEFAULT)
131
132
133=== modified file 'lib/lp/code/model/branchmergeproposal.py'
134--- lib/lp/code/model/branchmergeproposal.py 2009-06-07 22:27:57 +0000
135+++ lib/lp/code/model/branchmergeproposal.py 2009-06-09 01:25:39 +0000
136@@ -42,6 +42,7 @@
137 from lp.code.model.codereviewcomment import CodeReviewComment
138 from lp.code.model.codereviewvote import (
139 CodeReviewVoteReference)
140+from lp.code.model.reviewapprovalpolicies import get_approval_policy
141 from canonical.launchpad.database.diff import Diff, PreviewDiff, StaticDiff
142 from lp.services.job.model.job import Job
143 from canonical.launchpad.database.message import (
144@@ -657,6 +658,11 @@
145 self._autoApproveCheck(message.owner)
146 return code_review_message
147
148+ @property
149+ def review_approval_policy(self):
150+ """See `IBranchMergeProposal`."""
151+ return get_approval_policy(self.target_branch.review_approval_policy)
152+
153 def _autoApproveCheck(self, reviewer):
154 """Check the approval policy for this proposal."""
155 # Only look to change the status if the proposal needs review.
156@@ -666,7 +672,7 @@
157 reviews = [
158 (reference.reviewer, reference.vote) for reference in self.votes
159 if self.isPersonValidReviewer(reference.reviewer)]
160- policy = self.target_branch.target.review_approval_policy
161+ policy = self.review_approval_policy
162 next_status = policy.checkReviews(reviews)
163 # Auto approval approves or rejects the tip.
164 tip_rev_id = self.target_branch.last_scanned_id
165
166=== modified file 'lib/lp/code/model/branchtarget.py'
167--- lib/lp/code/model/branchtarget.py 2009-06-04 23:44:00 +0000
168+++ lib/lp/code/model/branchtarget.py 2009-06-09 01:25:39 +0000
169@@ -17,8 +17,6 @@
170 from lp.code.interfaces.branchcollection import IAllBranches
171 from lp.code.interfaces.branchtarget import (
172 check_default_stacked_on, IBranchTarget)
173-from lp.code.model.reviewapprovalpolicies import (
174- get_approval_policy, NoChangePolicy)
175 from lp.soyuz.interfaces.publishing import PackagePublishingPocket
176 from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
177
178@@ -95,11 +93,6 @@
179 """See `IBranchTarget`."""
180 return True
181
182- @property
183- def review_approval_policy(self):
184- """See `IBranchTarget`."""
185- return NoChangePolicy()
186-
187 def areBranchesMergeable(self, other_target):
188 """See `IBranchTarget`."""
189 # Branches are mergable into a PackageTarget if the source package
190@@ -169,11 +162,6 @@
191 """See `IBranchTarget`."""
192 return False
193
194- @property
195- def review_approval_policy(self):
196- """See `IBranchTarget`."""
197- return NoChangePolicy()
198-
199 def areBranchesMergeable(self, other_target):
200 """See `IBranchTarget`."""
201 return False
202@@ -235,11 +223,6 @@
203 """See `IBranchTarget`."""
204 return True
205
206- @property
207- def review_approval_policy(self):
208- """See `IBranchTarget`."""
209- return get_approval_policy(self.product.review_approval_policy)
210-
211 def areBranchesMergeable(self, other_target):
212 """See `IBranchTarget`."""
213 # Branches are mergable into a PackageTarget if the source package
214
215=== modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py'
216--- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-07 22:38:26 +0000
217+++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-09 01:25:39 +0000
218@@ -1410,13 +1410,11 @@
219
220 def setUp(self):
221 TestCaseWithFactory.setUp(self)
222- self.product = self.factory.makeProduct()
223- login_person(self.product.owner)
224- self.product.review_approval_policy = (
225- CodeReviewApprovalPolicy.ONE_REVIEWER)
226 self.bmp = self.factory.makeBranchMergeProposal(
227- set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
228- product=self.product)
229+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
230+ branch = self.bmp.target_branch
231+ login_person(branch.owner)
232+ branch.review_approval_policy = CodeReviewApprovalPolicy.ONE_REVIEWER
233
234 def addReview(self, reviewer, vote):
235 """Add a review by the reviewer with the specified vote."""
236@@ -1424,9 +1422,6 @@
237 self.bmp.createComment(
238 reviewer, subject=None, content="comment", vote=vote)
239
240- def assertReviewState(self, reviewer, vote, expected):
241- """Have `reviewer` comment on the proposal and check the status."""
242-
243 def test_auto_approval(self):
244 # If the product of the target branch has a policy to auto approve the
245 # proposal with a single review, then an approve review will cause the
246
247=== modified file 'lib/lp/registry/configure.zcml'
248--- lib/lp/registry/configure.zcml 2009-06-04 23:41:44 +0000
249+++ lib/lp/registry/configure.zcml 2009-06-09 01:25:39 +0000
250@@ -1069,7 +1069,7 @@
251 official_codehosting official_malone owner
252 programminglang project
253 redeemSubscriptionVoucher releaseroot
254- remote_product review_approval_policy screenshotsurl
255+ remote_product screenshotsurl
256 security_contact sourceforgeproject
257 summary title translationgroup
258 translationpermission wikiurl"/>
259
260=== modified file 'lib/lp/registry/interfaces/product.py'
261--- lib/lp/registry/interfaces/product.py 2009-06-04 23:41:44 +0000
262+++ lib/lp/registry/interfaces/product.py 2009-06-09 01:25:39 +0000
263@@ -42,7 +42,6 @@
264 IBranchMergeProposal, BranchMergeProposalStatus)
265 from lp.code.interfaces.branchvisibilitypolicy import (
266 IHasBranchVisibilityPolicy)
267-from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
268 from canonical.launchpad.interfaces.bugtarget import (
269 IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted)
270 from lp.registry.interfaces.karma import IKarmaContext
271@@ -528,12 +527,6 @@
272 readonly=True,
273 value_type=Reference(schema=IBranch)))
274
275- review_approval_policy = exported(
276- Choice(
277- title=_("The policy defining the automatical approval of code "
278- " reviews"),
279- vocabulary=CodeReviewApprovalPolicy))
280-
281 bounties = Attribute(_("The bounties that are related to this product."))
282
283 translatable_packages = Attribute(
284
285=== modified file 'lib/lp/registry/model/product.py'
286--- lib/lp/registry/model/product.py 2009-06-04 23:41:44 +0000
287+++ lib/lp/registry/model/product.py 2009-06-09 01:25:39 +0000
288@@ -32,7 +32,6 @@
289 from canonical.database.datetimecol import UtcDateTimeCol
290 from canonical.database.enumcol import EnumCol
291 from canonical.database.sqlbase import quote, SQLBase, sqlvalues
292-from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
293 from lp.code.model.branch import BranchSet
294 from lp.code.model.branchvisibilitypolicy import (
295 BranchVisibilityPolicyMixin)
296@@ -519,11 +518,6 @@
297 branches = SQLMultipleJoin('Branch', joinColumn='product',
298 orderBy='id')
299
300- review_approval_policy = EnumCol(
301- dbName='review_approval_policy', notNull=True,
302- schema=CodeReviewApprovalPolicy,
303- default=CodeReviewApprovalPolicy.NO_AUTO_CHANGE)
304-
305 serieses = SQLMultipleJoin('ProductSeries', joinColumn='product',
306 orderBy='name')
307
308
309=== modified file 'scripts/rosetta/message-sharing-merge.py' (properties changed: -x to +x)
Revision history for this message
Jonathan Lange (jml) wrote :

> === modified file 'lib/lp/code/model/branchmergeproposal.py'
> --- lib/lp/code/model/branchmergeproposal.py 2009-06-05 01:39:33 +0000
> +++ lib/lp/code/model/branchmergeproposal.py 2009-06-07 22:08:53 +0000
> @@ -659,9 +659,10 @@
>
> def _autoApproveCheck(self, reviewer):
> """Check the approval policy for this proposal."""
> + # Only look to change the status if the proposal needs review.
> + if self.queue_status != BranchMergeProposalStatus.NEEDS_REVIEW:
> + return None
> # Get all the reviews pending and done by members of the review team.
> - if self.queue_status != BranchMergeProposalStatus.NEEDS_REVIEW:
> - return None
> reviews = [
> (reference.reviewer, reference.vote) for reference in self.votes
> if self.isPersonValidReviewer(reference.reviewer)]
> @@ -673,9 +674,11 @@
> self.approveBranch(reviewer, tip_rev_id)
> elif next_status == BranchMergeProposalStatus.REJECTED:
> self.rejectBranch(reviewer, tip_rev_id)
> - else:
> + elif next_status in None:
> # Nothing to do.
> pass
> + else:
> + raise AssertionError('Unexpected result from approval policy.')
>

Can you please tweak this to include the repr() of the unexpected result in
the assertion error?

> === modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py'
> --- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-05 01:18:29 +0000
> +++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-07 22:33:19 +0000
> @@ -1418,30 +1418,30 @@
> set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
> product=self.product)
>
> - def assertReviewState(self, reviewer, vote, expected):
> - """Have `reviewer` comment on the proposal and check the status."""
> + def addReview(self, reviewer, vote):
> + """Add a review by the reviewer with the specified vote."""
> login_person(reviewer)
> self.bmp.createComment(
> reviewer, subject=None, content="comment", vote=vote)
> - self.assertEqual(expected, self.bmp.queue_status)
> +
> + def assertReviewState(self, reviewer, vote, expected):
> + """Have `reviewer` comment on the proposal and check the status."""
>

I think you need to delete this.

Revision history for this message
Jonathan Lange (jml) wrote :

Approved conditional on tweaking the assertion error to include the unexpected value from the policy.

review: Approve (code)

Unmerged revisions

8131. By Tim Penhey

Move the enum from Product -> Branch and fix everything.

8130. By Tim Penhey

Test improvements.

8129. By Tim Penhey

Small comment fixes and extra assert.

8128. By Tim Penhey

Grammar improvement.

8127. By Tim Penhey

Added simple metaclass for policy discovery.

8126. By Tim Penhey

Missed the id.

8125. By Tim Penhey

Add implementation tests.

8124. By Tim Penhey

Implement the auto approve.

8123. By Tim Penhey

Add a helper vote attribute to the CodeReviewVoteReference.

8122. By Tim Penhey

Hook up the attribute with the target.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/comments.sql'
2--- database/schema/comments.sql 2009-06-08 07:26:58 +0000
3+++ database/schema/comments.sql 2009-06-09 10:45:18 +0000
4@@ -45,6 +45,7 @@
5 COMMENT ON COLUMN Branch.stacked_on IS 'The Launchpad branch that this branch is stacked on (if any).';
6 COMMENT ON COLUMN Branch.distroseries IS 'The distribution series that the branch belongs to.';
7 COMMENT ON COLUMN Branch.sourcepackagename IS 'The source package this is a branch of.';
8+COMMENT ON COLUMN Branch.review_approval_policy IS 'An enumerated value referring to the CodeReviewApprovalPolicy that defines the requirements for automatically approving or rejecting proposals based on the code reviews';
9
10 -- BranchJob
11
12
13=== added file 'database/schema/patch-2109-99-0.sql'
14--- database/schema/patch-2109-99-0.sql 1970-01-01 00:00:00 +0000
15+++ database/schema/patch-2109-99-0.sql 2009-06-09 10:45:18 +0000
16@@ -0,0 +1,7 @@
17+SET client_min_messages = ERROR;
18+
19+ALTER TABLE Branch
20+ ADD COLUMN review_approval_policy INT DEFAULT 1 NOT NULL;
21+
22+INSERT INTO LaunchpadDatabaseRevision VALUES (2109, 99, 0);
23+
24
25=== modified file 'lib/lp/code/configure.zcml'
26--- lib/lp/code/configure.zcml 2009-06-07 01:53:39 +0000
27+++ lib/lp/code/configure.zcml 2009-06-09 10:45:18 +0000
28@@ -393,6 +393,7 @@
29 commitsForDays
30 needs_upgrading
31 isBranchMergeable
32+ review_approval_policy
33 "/>
34 <require
35 permission="launchpad.Edit"
36@@ -404,7 +405,7 @@
37 last_scanned last_scanned_id revision_count branch_type
38 reviewer branch_format repository_format
39 control_format stacked_on merge_queue
40- merge_control_status"/>
41+ merge_control_status review_approval_policy"/>
42 <require
43 permission="launchpad.AnyPerson"
44 set_attributes="whiteboard"/>
45
46=== modified file 'lib/lp/code/interfaces/branch.py'
47--- lib/lp/code/interfaces/branch.py 2009-06-07 01:53:39 +0000
48+++ lib/lp/code/interfaces/branch.py 2009-06-09 10:45:18 +0000
49@@ -92,6 +92,7 @@
50 from canonical.launchpad.validators import LaunchpadValidationError
51 from lp.code.interfaces.branchlookup import IBranchLookup
52 from lp.code.interfaces.branchtarget import IHasBranchTarget
53+from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
54 from canonical.launchpad.interfaces.launchpad import (
55 IHasOwner, ILaunchpadCelebrities)
56 from lp.registry.interfaces.person import IPerson
57@@ -918,6 +919,12 @@
58 readonly=True,
59 value_type=Reference(IPerson)))
60
61+ review_approval_policy = exported(
62+ Choice(
63+ title=_("The policy defining the automatical approval of code "
64+ " reviews"),
65+ vocabulary=CodeReviewApprovalPolicy))
66+
67 date_created = exported(
68 Datetime(
69 title=_('Date Created'),
70
71=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
72--- lib/lp/code/interfaces/branchmergeproposal.py 2009-06-02 08:20:49 +0000
73+++ lib/lp/code/interfaces/branchmergeproposal.py 2009-06-09 10:45:18 +0000
74@@ -303,6 +303,10 @@
75 description=_('Any emails sent to this address will result'
76 'in comments being added.')))
77
78+ review_approval_policy = Attribute(
79+ "The policy object relating to the approval policy set for the "
80+ "target branch.")
81+
82 @operation_parameters(
83 id=Int(
84 title=_("A CodeReviewComment ID.")))
85
86=== modified file 'lib/lp/code/interfaces/codereviewvote.py'
87--- lib/lp/code/interfaces/codereviewvote.py 2009-04-17 10:32:16 +0000
88+++ lib/lp/code/interfaces/codereviewvote.py 2009-06-09 10:45:18 +0000
89@@ -8,14 +8,14 @@
90 ]
91
92 from zope.interface import Interface
93-from zope.schema import Datetime, Int, TextLine
94+from zope.schema import Choice, Datetime, Int, TextLine
95
96 from canonical.launchpad import _
97 from canonical.launchpad.fields import PublicPersonChoice
98 from lp.code.interfaces.branchmergeproposal import (
99 IBranchMergeProposal)
100 from lp.code.interfaces.codereviewcomment import (
101- ICodeReviewComment)
102+ CodeReviewVote, ICodeReviewComment)
103 from lazr.restful.fields import Reference
104 from lazr.restful.declarations import (
105 export_as_webservice_entry, exported)
106@@ -66,3 +66,9 @@
107 "The code review comment that contains the most recent vote."
108 ),
109 required=True, schema=ICodeReviewComment))
110+
111+ vote = exported(
112+ Choice(
113+ description=_('The last review by the reviewer.'),
114+ required=False, readonly=True,
115+ vocabulary=CodeReviewVote))
116
117=== added file 'lib/lp/code/interfaces/reviewapprovalpolicies.py'
118--- lib/lp/code/interfaces/reviewapprovalpolicies.py 1970-01-01 00:00:00 +0000
119+++ lib/lp/code/interfaces/reviewapprovalpolicies.py 2009-06-09 10:45:18 +0000
120@@ -0,0 +1,71 @@
121+# Copyright 2009 Canonical Ltd. All rights reserved.
122+
123+"""Interfaces and enums for code review approval policies."""
124+
125+__metaclass__ = type
126+__all__ = [
127+ 'CodeReviewApprovalPolicy',
128+ 'ICodeReviewApprovalPolicy',
129+ ]
130+
131+
132+from zope.interface import Interface
133+
134+from lazr.enum import DBEnumeratedType, DBItem
135+
136+
137+class CodeReviewApprovalPolicy(DBEnumeratedType):
138+ """Policies for automatically approving code reviews.
139+
140+ In all the situations mentioned below, reviewer refers to a member of the
141+ review team for the target branch. Only reviews by people in a position
142+ to approve the proposal are consulted to automatically change the proposal
143+ state.
144+ """
145+
146+ NO_AUTO_CHANGE = DBItem(1, """
147+ No automatic approval
148+
149+ All approvals or rejections are done manually.
150+ """)
151+
152+ ONE_REVIEWER = DBItem(2, """
153+ One review is enough
154+
155+ The approval or rejection of one reviewer is enough to accept or
156+ reject the proposal as a whole. If the reviewer approves the review,
157+ the proposal will be set to the approved state. If the reviewer
158+ disapproves in the review, the proposal is rejected. Any other review
159+ does not change the state of the proposal.
160+ """)
161+
162+ TWO_REVIEWERS = DBItem(3, """
163+ Two reviews needed
164+
165+ The proposal needs to be approved by two reviewers in order for the
166+ proposal to be approved. If there are two approvals the proposal is
167+ approved. If there are two rejections then the proposal is rejected.
168+ If the reviewers disagree, then the proposal status is not updated.
169+ """)
170+
171+ ALL_REQUESTED_COMPLETE = DBItem(4, """
172+ All requested reviews complete
173+
174+ All requested reviews must be complete and agree. If all reviewers
175+ agree the status is updated. Reviewers who abstain are ignored.
176+ """)
177+
178+
179+class ICodeReviewApprovalPolicy(Interface):
180+ """A code review approval policy."""
181+
182+ def checkReviews(reviews):
183+ """Check the status of the reviews to see if it should be approved.
184+
185+ :param reviews: An iterable of (person, review), where the
186+ person is considered a valid reviewer of the proposal, and the
187+ review is a CodeReviewVote value or None if pending.
188+ :return: BranchMergeProposalStatus.CODE_APPROVED if the proposal state
189+ should be approved, BranchMergeProposalStatus.REJECTED if the
190+ proposal should be rejected, or None if no change.
191+ """
192
193=== modified file 'lib/lp/code/model/branch.py'
194--- lib/lp/code/model/branch.py 2009-06-04 18:30:55 +0000
195+++ lib/lp/code/model/branch.py 2009-06-09 10:45:18 +0000
196@@ -39,6 +39,12 @@
197 from canonical.launchpad.webapp.interfaces import (
198 IStoreSelector, MAIN_STORE, SLAVE_FLAVOR)
199
200+<<<<<<< TREE
201+=======
202+from lp.code.interfaces.branch import (BranchFormat, RepositoryFormat,
203+ BRANCH_FORMAT_UPGRADE_PATH, REPOSITORY_FORMAT_UPGRADE_PATH)
204+from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
205+>>>>>>> MERGE-SOURCE
206 from lp.code.mail.branch import send_branch_modified_notifications
207 from lp.code.model.branchmergeproposal import (
208 BranchMergeProposal, BranchMergeProposalGetter)
209@@ -195,6 +201,11 @@
210 joinColumn='branch',
211 orderBy='id')
212
213+ review_approval_policy = EnumCol(
214+ dbName='review_approval_policy', notNull=True,
215+ schema=CodeReviewApprovalPolicy,
216+ default=CodeReviewApprovalPolicy.NO_AUTO_CHANGE)
217+
218 date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
219 date_last_modified = UtcDateTimeCol(notNull=True, default=DEFAULT)
220
221
222=== modified file 'lib/lp/code/model/branchmergeproposal.py'
223--- lib/lp/code/model/branchmergeproposal.py 2009-06-05 15:04:29 +0000
224+++ lib/lp/code/model/branchmergeproposal.py 2009-06-09 10:45:18 +0000
225@@ -42,6 +42,7 @@
226 from lp.code.model.codereviewcomment import CodeReviewComment
227 from lp.code.model.codereviewvote import (
228 CodeReviewVoteReference)
229+from lp.code.model.reviewapprovalpolicies import get_approval_policy
230 from canonical.launchpad.database.diff import Diff, PreviewDiff, StaticDiff
231 from lp.services.job.model.job import Job
232 from canonical.launchpad.database.message import (
233@@ -636,8 +637,38 @@
234 if _notify_listeners:
235 notify(NewCodeReviewCommentEvent(
236 code_review_message, original_email))
237+ # Check for automatic approval or rejection.
238+ self._autoApproveCheck(message.owner)
239 return code_review_message
240
241+ @property
242+ def review_approval_policy(self):
243+ """See `IBranchMergeProposal`."""
244+ return get_approval_policy(self.target_branch.review_approval_policy)
245+
246+ def _autoApproveCheck(self, reviewer):
247+ """Check the approval policy for this proposal."""
248+ # Only look to change the status if the proposal needs review.
249+ if self.queue_status != BranchMergeProposalStatus.NEEDS_REVIEW:
250+ return None
251+ # Get all the reviews pending and done by members of the review team.
252+ reviews = [
253+ (reference.reviewer, reference.vote) for reference in self.votes
254+ if self.isPersonValidReviewer(reference.reviewer)]
255+ policy = self.review_approval_policy
256+ next_status = policy.checkReviews(reviews)
257+ # Auto approval approves or rejects the tip.
258+ tip_rev_id = self.target_branch.last_scanned_id
259+ if next_status == BranchMergeProposalStatus.CODE_APPROVED:
260+ self.approveBranch(reviewer, tip_rev_id)
261+ elif next_status == BranchMergeProposalStatus.REJECTED:
262+ self.rejectBranch(reviewer, tip_rev_id)
263+ elif next_status in None:
264+ # Nothing to do.
265+ pass
266+ else:
267+ raise AssertionError('Unexpected result from approval policy.')
268+
269 def updatePreviewDiff(self, diff_content, diff_stat,
270 source_revision_id, target_revision_id,
271 dependent_revision_id=None, conflicts=None):
272
273=== modified file 'lib/lp/code/model/codereviewvote.py'
274--- lib/lp/code/model/codereviewvote.py 2009-04-17 10:32:16 +0000
275+++ lib/lp/code/model/codereviewvote.py 2009-06-09 10:45:18 +0000
276@@ -15,6 +15,8 @@
277 from canonical.database.datetimecol import UtcDateTimeCol
278 from canonical.database.sqlbase import SQLBase
279 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
280+
281+
282 class CodeReviewVoteReference(SQLBase):
283 """See `ICodeReviewVote`"""
284
285@@ -33,3 +35,11 @@
286 review_type = StringCol(default=None)
287 comment = ForeignKey(
288 dbName='vote_message', foreignKey='CodeReviewComment', default=None)
289+
290+ @property
291+ def vote(self):
292+ """Return the vote from the comment if it is there."""
293+ if self.comment is not None:
294+ return self.comment.vote
295+ else:
296+ return None
297
298=== added file 'lib/lp/code/model/reviewapprovalpolicies.py'
299--- lib/lp/code/model/reviewapprovalpolicies.py 1970-01-01 00:00:00 +0000
300+++ lib/lp/code/model/reviewapprovalpolicies.py 2009-06-09 10:45:18 +0000
301@@ -0,0 +1,148 @@
302+# Copyright 2009 Canonical Ltd. All rights reserved.
303+
304+"""Implementation classes for code review approval policies."""
305+
306+__metaclass__ = type
307+__all__ = [
308+ 'get_approval_policy',
309+ 'NoChangePolicy',
310+ 'OneReviewerPolicy',
311+ 'ReviewsCompletePolicy',
312+ 'TwoReviewersPolicy',
313+ ]
314+
315+from zope.interface import implements
316+
317+from lp.code.interfaces.branchmergeproposal import BranchMergeProposalStatus
318+from lp.code.interfaces.codereviewcomment import CodeReviewVote
319+from lp.code.interfaces.reviewapprovalpolicies import (
320+ CodeReviewApprovalPolicy, ICodeReviewApprovalPolicy)
321+
322+
323+APPROVAL_POLICIES = {}
324+
325+
326+class PolicyMetaClass(type):
327+ """A meta-class to automatically register the policies."""
328+
329+ def __new__(cls, classname, bases, classdict):
330+ """Create the policy and add it to the global dict."""
331+ instance = type.__new__(cls, classname, bases, classdict)
332+ # If the class has a policy attribute, register with the global dict.
333+ if 'policy' in classdict:
334+ global APPROVAL_POLICIES
335+ APPROVAL_POLICIES[instance.policy] = instance
336+ return instance
337+
338+
339+class NoChangePolicy:
340+ """A policy where no changes are made automatically."""
341+
342+ __metaclass__ = PolicyMetaClass
343+
344+ implements(ICodeReviewApprovalPolicy)
345+
346+ policy = CodeReviewApprovalPolicy.NO_AUTO_CHANGE
347+
348+ def checkReviews(self, reviews):
349+ """No change, return None."""
350+ return None
351+
352+
353+class BasePolicy:
354+ """A base policy class to sum review types."""
355+
356+ __metaclass__ = PolicyMetaClass
357+
358+ def countReviews(self, reviews):
359+ """Return a dict of review counts."""
360+ result = {
361+ CodeReviewVote.APPROVE: 0,
362+ CodeReviewVote.DISAPPROVE: 0,
363+ CodeReviewVote.ABSTAIN: 0,
364+ 'pending': 0,
365+ 'other': 0,
366+ }
367+ for reviewer, review in reviews:
368+ if review in result:
369+ result[review] += 1
370+ elif review is None:
371+ result['pending'] += 1
372+ else:
373+ result['other'] += 1
374+ return result
375+
376+
377+class OneReviewerPolicy(BasePolicy):
378+ """A policy where one review is enough."""
379+
380+ implements(ICodeReviewApprovalPolicy)
381+
382+ policy = CodeReviewApprovalPolicy.ONE_REVIEWER
383+
384+ def checkReviews(self, reviews):
385+ """See `ICodeReviewApprovalPolicy`."""
386+ counts = self.countReviews(reviews)
387+ approvals = counts[CodeReviewVote.APPROVE]
388+ disapprovals = counts[CodeReviewVote.DISAPPROVE]
389+ if approvals > 0 and disapprovals == 0:
390+ return BranchMergeProposalStatus.CODE_APPROVED
391+ elif disapprovals > 0 and approvals == 0:
392+ return BranchMergeProposalStatus.REJECTED
393+ else:
394+ return None
395+
396+
397+class TwoReviewersPolicy(BasePolicy):
398+ """A policy where two non-conflicting reviews are needed."""
399+
400+ implements(ICodeReviewApprovalPolicy)
401+
402+ policy = CodeReviewApprovalPolicy.TWO_REVIEWERS
403+
404+ def checkReviews(self, reviews):
405+ """See `ICodeReviewApprovalPolicy`."""
406+ counts = self.countReviews(reviews)
407+ approvals = counts[CodeReviewVote.APPROVE]
408+ disapprovals = counts[CodeReviewVote.DISAPPROVE]
409+ if approvals > 1 and disapprovals == 0:
410+ return BranchMergeProposalStatus.CODE_APPROVED
411+ elif disapprovals > 1 and approvals == 0:
412+ return BranchMergeProposalStatus.REJECTED
413+ else:
414+ return None
415+
416+
417+class ReviewsCompletePolicy(BasePolicy):
418+ """A policy where all requested reviews need to be complete.
419+
420+ A complete review is approved, disapproved or abstained.
421+ """
422+
423+ implements(ICodeReviewApprovalPolicy)
424+
425+ policy = CodeReviewApprovalPolicy.ALL_REQUESTED_COMPLETE
426+
427+ def checkReviews(self, reviews):
428+ """See `ICodeReviewApprovalPolicy`."""
429+ counts = self.countReviews(reviews)
430+ # If there are pending or in-progress reviews, no change.
431+ if counts['pending'] > 0 or counts['other'] > 0:
432+ return None
433+ approvals = counts[CodeReviewVote.APPROVE]
434+ disapprovals = counts[CodeReviewVote.DISAPPROVE]
435+ if approvals > 0 and disapprovals == 0:
436+ return BranchMergeProposalStatus.CODE_APPROVED
437+ elif disapprovals > 0 and approvals == 0:
438+ return BranchMergeProposalStatus.REJECTED
439+ else:
440+ return None
441+
442+
443+def get_approval_policy(policy):
444+ """Return a policy class based on the enumerated value."""
445+ try:
446+ policy_class = APPROVAL_POLICIES[policy]
447+ return policy_class()
448+ except KeyError:
449+ raise AssertionError("Invalid policy value: %s" % policy)
450
451=== modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py'
452--- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-04 20:50:32 +0000
453+++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-06-09 10:45:18 +0000
454@@ -39,6 +39,7 @@
455 IMergeProposalCreatedJob, WrongBranchMergeProposal)
456 from lp.code.interfaces.branchsubscription import (
457 BranchSubscriptionNotificationLevel, CodeReviewNotificationLevel)
458+from lp.code.interfaces.reviewapprovalpolicies import CodeReviewApprovalPolicy
459 from canonical.launchpad.interfaces.message import IMessageJob
460 from lp.registry.interfaces.person import IPersonSet
461 from lp.registry.interfaces.product import IProductSet
462@@ -1422,5 +1423,51 @@
463 removeSecurityProxy(merge_proposal.preview_diff).diff_id)
464
465
466+class TestAutomaticProposalAppoval(TestCaseWithFactory):
467+ """Test that the approval policy is called if set."""
468+
469+ layer = DatabaseFunctionalLayer
470+
471+ def setUp(self):
472+ TestCaseWithFactory.setUp(self)
473+ self.bmp = self.factory.makeBranchMergeProposal(
474+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
475+ branch = self.bmp.target_branch
476+ login_person(branch.owner)
477+ branch.review_approval_policy = CodeReviewApprovalPolicy.ONE_REVIEWER
478+
479+ def addReview(self, reviewer, vote):
480+ """Add a review by the reviewer with the specified vote."""
481+ login_person(reviewer)
482+ self.bmp.createComment(
483+ reviewer, subject=None, content="comment", vote=vote)
484+
485+ def test_auto_approval(self):
486+ # If the product of the target branch has a policy to auto approve the
487+ # proposal with a single review, then an approve review will cause the
488+ # state to move from NEEDS_REVIEW to CODE_APPROVED.
489+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.APPROVE)
490+ self.assertEqual(
491+ BranchMergeProposalStatus.CODE_APPROVED, self.bmp.queue_status)
492+
493+ def test_auto_rejection(self):
494+ # If the product of the target branch has a policy to auto reject the
495+ # proposal with a single review, then a disapprove review will cause
496+ # the state to move from NEEDS_REVIEW to REJECTED.
497+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.DISAPPROVE)
498+ self.assertEqual(
499+ BranchMergeProposalStatus.REJECTED, self.bmp.queue_status)
500+
501+ def test_not_needs_review(self):
502+ # Auto approval only happens if the proposal is in NEEDS_REVIEW state.
503+ # So a normally approving vote when in a different state change the
504+ # status of the proposal.
505+ login_person(self.bmp.registrant)
506+ self.bmp.setAsWorkInProgress()
507+ self.addReview(self.bmp.target_branch.owner, CodeReviewVote.APPROVE)
508+ self.assertEqual(
509+ BranchMergeProposalStatus.WORK_IN_PROGRESS, self.bmp.queue_status)
510+
511+
512 def test_suite():
513 return TestLoader().loadTestsFromName(__name__)
514
515=== added file 'lib/lp/code/model/tests/test_reviewapprovalpolicies.py'
516--- lib/lp/code/model/tests/test_reviewapprovalpolicies.py 1970-01-01 00:00:00 +0000
517+++ lib/lp/code/model/tests/test_reviewapprovalpolicies.py 2009-06-09 10:45:18 +0000
518@@ -0,0 +1,244 @@
519+# Copyright 2009 Canonical Ltd. All rights reserved.
520+
521+"""Tests for code review approval policies."""
522+
523+__metaclass__ = type
524+
525+import unittest
526+
527+from canonical.testing.layers import DatabaseFunctionalLayer
528+
529+from lp.code.interfaces.branchmergeproposal import BranchMergeProposalStatus
530+from lp.code.interfaces.codereviewcomment import CodeReviewVote
531+from lp.code.interfaces.reviewapprovalpolicies import (
532+ CodeReviewApprovalPolicy, ICodeReviewApprovalPolicy)
533+from lp.code.model.reviewapprovalpolicies import (
534+ get_approval_policy, NoChangePolicy, OneReviewerPolicy,
535+ ReviewsCompletePolicy, TwoReviewersPolicy)
536+from lp.testing import TestCase, TestCaseWithFactory
537+
538+
539+class TestGetApprovalPolicy(TestCase):
540+ """Test the policy factory generates the correct types."""
541+
542+ def test_no_change_policy(self):
543+ # NO_AUTO_CHANGE creates a NoChangePolicy.
544+ policy = get_approval_policy(CodeReviewApprovalPolicy.NO_AUTO_CHANGE)
545+ self.assertIsInstance(policy, NoChangePolicy)
546+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
547+
548+ def test_one_reviewer_policy(self):
549+ # ONE_REVIEWER creates a OneReviewerPolicy.
550+ policy = get_approval_policy(CodeReviewApprovalPolicy.ONE_REVIEWER)
551+ self.assertIsInstance(policy, OneReviewerPolicy)
552+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
553+
554+ def test_two_reviewers_policy(self):
555+ # TWO_REVIEWERS creates a TwoReviewersPolicy.
556+ policy = get_approval_policy(CodeReviewApprovalPolicy.TWO_REVIEWERS)
557+ self.assertIsInstance(policy, TwoReviewersPolicy)
558+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
559+
560+ def test_all_reviews_completed_policy(self):
561+ # ALL_REQUESTED_COMPLETE creates a ReviewsCompletePolicy.
562+ policy = get_approval_policy(
563+ CodeReviewApprovalPolicy.ALL_REQUESTED_COMPLETE)
564+ self.assertIsInstance(policy, ReviewsCompletePolicy)
565+ self.assertProvides(policy, ICodeReviewApprovalPolicy)
566+
567+
568+class TestNoChangePolicy(TestCaseWithFactory):
569+ """Test that a no change policy doesn't change the status."""
570+
571+ layer = DatabaseFunctionalLayer
572+
573+ def setUp(self):
574+ TestCaseWithFactory.setUp(self)
575+ self.policy = NoChangePolicy()
576+
577+ def test_review_approval(self):
578+ # An review approval doesn't change the status.
579+ reviews = [(self.factory.makePerson(), CodeReviewVote.APPROVE)]
580+ self.assertIs(None, self.policy.checkReviews(reviews))
581+
582+ def test_review_disapproval(self):
583+ # An review disapproval doesn't change the status.
584+ reviews = [(self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
585+ self.assertIs(None, self.policy.checkReviews(reviews))
586+
587+
588+class TestOneReviewerPolicy(TestCaseWithFactory):
589+ """Test that one review is enough to change the status."""
590+
591+ layer = DatabaseFunctionalLayer
592+
593+ def setUp(self):
594+ TestCaseWithFactory.setUp(self)
595+ self.policy = OneReviewerPolicy()
596+
597+ def test_review_approval(self):
598+ # An review approval doesn't change the status.
599+ reviews = [(self.factory.makePerson(), CodeReviewVote.APPROVE)]
600+ self.assertEqual(
601+ BranchMergeProposalStatus.CODE_APPROVED,
602+ self.policy.checkReviews(reviews))
603+
604+ def test_review_disapproval(self):
605+ # An review disapproval doesn't change the status.
606+ reviews = [(self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
607+ self.assertEqual(
608+ BranchMergeProposalStatus.REJECTED,
609+ self.policy.checkReviews(reviews))
610+
611+ def test_other_review(self):
612+ # An review disapproval doesn't change the status.
613+ reviews = [(self.factory.makePerson(), CodeReviewVote.ABSTAIN)]
614+ self.assertIs(None, self.policy.checkReviews(reviews))
615+
616+ def test_conflicting_reviews(self):
617+ # If there is an approval and a disapproval, an update isn't
618+ # suggested.
619+ reviews = [
620+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
621+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
622+ self.assertIs(None, self.policy.checkReviews(reviews))
623+
624+
625+class TestTwoReviewersPolicy(TestCaseWithFactory):
626+ """Test that two reviews are needed to change the status."""
627+
628+ layer = DatabaseFunctionalLayer
629+
630+ def setUp(self):
631+ TestCaseWithFactory.setUp(self)
632+ self.policy = TwoReviewersPolicy()
633+
634+ def test_review_approval(self):
635+ # An single approval doesn't change the status.
636+ reviews = [(self.factory.makePerson(), CodeReviewVote.APPROVE)]
637+ self.assertIs(None, self.policy.checkReviews(reviews))
638+
639+ def test_two_approvals(self):
640+ # Two approvals do change the status.
641+ reviews = [
642+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
643+ (self.factory.makePerson(), CodeReviewVote.APPROVE)]
644+ self.assertEqual(
645+ BranchMergeProposalStatus.CODE_APPROVED,
646+ self.policy.checkReviews(reviews))
647+
648+ def test_review_disapproval(self):
649+ # An single disapproval doesn't change the status.
650+ reviews = [(self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
651+ self.assertIs(None, self.policy.checkReviews(reviews))
652+
653+ def test_two_disapprovals(self):
654+ # Two disapprovals do change the status.
655+ reviews = [
656+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE),
657+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
658+ self.assertEqual(
659+ BranchMergeProposalStatus.REJECTED,
660+ self.policy.checkReviews(reviews))
661+
662+ def test_conflicting_reviews(self):
663+ # If there is an approval and a disapproval, an update isn't
664+ # suggested.
665+ reviews = [
666+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
667+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
668+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
669+ self.assertIs(None, self.policy.checkReviews(reviews))
670+
671+ reviews = [
672+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
673+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE),
674+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
675+ self.assertIs(None, self.policy.checkReviews(reviews))
676+
677+
678+class TestReviewsCompletePolicy(TestCaseWithFactory):
679+ """Test that all requested reviews are complete."""
680+
681+ layer = DatabaseFunctionalLayer
682+
683+ def setUp(self):
684+ TestCaseWithFactory.setUp(self)
685+ self.policy = ReviewsCompletePolicy()
686+
687+ def test_review_approval(self):
688+ # An single approval is enough to approve the proposal.
689+ reviews = [(self.factory.makePerson(), CodeReviewVote.APPROVE)]
690+ self.assertEqual(
691+ BranchMergeProposalStatus.CODE_APPROVED,
692+ self.policy.checkReviews(reviews))
693+
694+ def test_review_approval_and_pending(self):
695+ # An single approval is not good enough if there is another pending.
696+ reviews = [
697+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
698+ (self.factory.makePerson(), None)]
699+ self.assertIs(None, self.policy.checkReviews(reviews))
700+
701+ def test_review_approval_and_other(self):
702+ # An single approval is not good enough if there is another incomplete
703+ # review.
704+ reviews = [
705+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
706+ (self.factory.makePerson(), CodeReviewVote.NEEDS_FIXING)]
707+ self.assertIs(None, self.policy.checkReviews(reviews))
708+
709+ def test_review_approval_and_abstain(self):
710+ # An single approval is good enough if the other requested reviewer
711+ # abstained.
712+ reviews = [
713+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
714+ (self.factory.makePerson(), CodeReviewVote.ABSTAIN)]
715+ self.assertEqual(
716+ BranchMergeProposalStatus.CODE_APPROVED,
717+ self.policy.checkReviews(reviews))
718+
719+ def test_review_disapproval(self):
720+ # An single disapproval is enough to reject the proposal.
721+ reviews = [(self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
722+ self.assertEqual(
723+ BranchMergeProposalStatus.REJECTED,
724+ self.policy.checkReviews(reviews))
725+
726+ def test_review_disapproval_and_pending(self):
727+ # An single disapproval is not good enough if there is another
728+ # pending.
729+ reviews = [
730+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE),
731+ (self.factory.makePerson(), None)]
732+ self.assertIs(None, self.policy.checkReviews(reviews))
733+
734+ def test_review_disapproval_and_other(self):
735+ # An single disapproval is not good enough if there is another
736+ # incomplete review.
737+ reviews = [
738+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE),
739+ (self.factory.makePerson(), CodeReviewVote.NEEDS_FIXING)]
740+ self.assertIs(None, self.policy.checkReviews(reviews))
741+
742+ def test_review_disapproval_and_abstain(self):
743+ # An single disapproval is good enough if the other requested reviewer
744+ # abstained.
745+ reviews = [
746+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE),
747+ (self.factory.makePerson(), CodeReviewVote.ABSTAIN)]
748+ self.assertEqual(
749+ BranchMergeProposalStatus.REJECTED,
750+ self.policy.checkReviews(reviews))
751+
752+ def test_conflicting_reviews(self):
753+ # If there is an approval and a disapproval, an update isn't
754+ # suggested.
755+ reviews = [
756+ (self.factory.makePerson(), CodeReviewVote.APPROVE),
757+ (self.factory.makePerson(), CodeReviewVote.DISAPPROVE)]
758+ self.assertIs(None, self.policy.checkReviews(reviews))
759+
760+
761+def test_suite():
762+ return unittest.TestLoader().loadTestsFromName(__name__)
763
764=== modified file 'lib/lp/registry/model/product.py'
765--- lib/lp/registry/model/product.py 2009-06-02 08:20:49 +0000
766+++ lib/lp/registry/model/product.py 2009-06-09 10:45:18 +0000
767@@ -517,6 +517,7 @@
768
769 branches = SQLMultipleJoin('Branch', joinColumn='product',
770 orderBy='id')
771+
772 serieses = SQLMultipleJoin('ProductSeries', joinColumn='product',
773 orderBy='name')
774

Subscribers

People subscribed via source and target branches

to status/vote changes: