Merge lp:~thumper/launchpad/auto-approve into lp:launchpad/db-devel
- auto-approve
- Merge into db-devel
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 |
Related bugs: | |
Related blueprints: |
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 |
Commit message
Description of the change
Tim Penhey (thumper) wrote : | # |
Jonathan Lange (jml) wrote : | # |
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/
> --- database/
> +++ database/
> @@ -593,6 +593,7 @@
> COMMENT ON COLUMN Product.
> COMMENT ON COLUMN Product.
> COMMENT ON COLUMN Product.
> +COMMENT ON COLUMN Product.
>
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/
> --- database/
> +++ database/
> @@ -0,0 +1,7 @@
> +SET client_min_messages = ERROR;
> +
> +ALTER TABLE Product
> + ADD COLUMN review_
> +
> +INSERT INTO LaunchpadDataba
> +
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -87,6 +87,10 @@
> supports_
> "Does this target support merge proposals at all?")
>
> + review_
> + "The policy object relating to the approval policy set for the "
> + "target.")
> +
> def areBranchesMerg
> """Are branches from other_target mergeable into this target."""
>
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -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.
> from lp.code.
> IBranchMergePro
> from lp.code.interf...
Tim Penhey (thumper) wrote : | # |
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/
> > --- database/
> > +++ database/
> > @@ -593,6 +593,7 @@
> > COMMENT ON COLUMN Product.
> > Launchpad admins, registry experts and the project owners to capture the
> > state of current issues with the project.'; COMMENT ON COLUMN
> > Product.
> > approved by an administrator.'; COMMENT ON COLUMN Product.
> > IS 'The ID of this product on its remote bug tracker.'; +COMMENT ON
> > COLUMN Product.
> > to the CodeReviewAppro
> > automatically approving or rejecting proposals based on the code
> > reviews';
>
> Please change "to define" to "that defines".
Done
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -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.
> > from lp.code.
> > IBranchMergePro
> > from lp.code.
> > - ICodeReviewComment)
> > + CodeReviewVote, ICodeReviewComment)
> > from lazr.restful.fields import Reference
> > from lazr.restful.
> > export_
> > @@ -66,3 +66,9 @@
> > "The code review comment that contains the most recent
> > vote." ),
> > required=True, schema=
> > +
> > + vote = exported(
> > + Choice(
> > + description=_('The last review by the reviewer.'),
> > + required=False, readonly=True,
> > + vocabulary=
>
> 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): |
- 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.
Stuart Bishop (stub) wrote : | # |
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.
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...
- 8131. By Tim Penhey
-
Move the enum from Product -> Branch and fix everything.
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) |
Jonathan Lange (jml) wrote : | # |
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -659,9 +659,10 @@
>
> def _autoApproveChe
> """Check the approval policy for this proposal."""
> + # Only look to change the status if the proposal needs review.
> + if self.queue_status != BranchMergeProp
> + return None
> # Get all the reviews pending and done by members of the review team.
> - if self.queue_status != BranchMergeProp
> - return None
> reviews = [
> (reference.
> if self.isPersonVa
> @@ -673,9 +674,11 @@
> self.approveBra
> elif next_status == BranchMergeProp
> self.rejectBran
> - else:
> + elif next_status in None:
> # Nothing to do.
> pass
> + else:
> + raise AssertionError(
>
Can you please tweak this to include the repr() of the unexpected result in
the assertion error?
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -1418,30 +1418,30 @@
> set_state=
> product=
>
> - def assertReviewSta
> - """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(
> self.bmp.
> reviewer, subject=None, content="comment", vote=vote)
> - self.assertEqua
> +
> + def assertReviewSta
> + """Have `reviewer` comment on the proposal and check the status."""
>
I think you need to delete this.
Jonathan Lange (jml) wrote : | # |
Approved conditional on tweaking the assertion error to include the unexpected value from the policy.
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 CodeReviewVoteR
eference. - 8122. By Tim Penhey
-
Hook up the attribute with the target.
Preview Diff
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 |
Here is a summary of the changes:
database/ schema/ comments. sql | 1 schema/ patch-2109- 99-0.sql | 7
database/
The database patch to add a column to products for recording the auto approval policy.
lib/lp/ registry/ configure. zcml | 2 registry/ interfaces/ product. py | 7 registry/ model/product. py | 7
lib/lp/
lib/lp/
Adding the review_ approval_ policy to the Product.
lib/lp/ code/interfaces /branchtarget. py | 4 code/model/ branchtarget. py | 17 +
lib/lp/
Adding the review_ approval_ policy to IBranchTarget.
lib/lp/ code/interfaces /codereviewvote .py | 10 code/model/ codereviewvote. py | 10
lib/lp/
Add a helper attribute to get the vote from the code review comment.
lib/lp/ code/model/ branchmergeprop osal.py | 22 + code/model/ tests/test_ branchmergeprop osals.py | 53 +++
lib/lp/
Add in the auto approval check.
lib/lp/ code/interfaces /reviewapproval policies. py | 67 ++++ code/model/ reviewapprovalp olicies. py | 124 ++++++++ code/model/ tests/test_ reviewapprovalp olicies. py | 244 +++++++++++++++++
lib/lp/
lib/lp/
The policies themselves and their tests.
14 files changed, 572 insertions(+), 3 deletions(-)
Tests: ProposalAppoval reviewapprovalp olicies
TestAutomatic
test_