Merge lp:~wgrant/launchpad/recipe-cancel into lp:launchpad
- recipe-cancel
- Merge into devel
Proposed by
William Grant
Status: | Merged |
---|---|
Merged at revision: | 17733 |
Proposed branch: | lp:~wgrant/launchpad/recipe-cancel |
Merge into: | lp:launchpad |
Diff against target: |
438 lines (+122/-77) 8 files modified
lib/lp/code/browser/configure.zcml (+1/-1) lib/lp/code/browser/sourcepackagerecipebuild.py (+8/-19) lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py (+18/-30) lib/lp/code/configure.zcml (+6/-1) lib/lp/code/interfaces/sourcepackagerecipebuild.py (+44/-9) lib/lp/code/model/sourcepackagerecipebuild.py (+25/-5) lib/lp/code/model/tests/test_sourcepackagerecipe.py (+2/-1) lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+18/-11) |
To merge this branch: | bzr merge lp:~wgrant/launchpad/recipe-cancel |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+270810@code.launchpad.net |
Commit message
Allow archive owners to cancel their recipe builds, and make recipes use the normal cancellation infrastructure.
Description of the change
Allow archive owners to cancel their recipe builds, and make recipes use the normal cancellation infrastructure rather than just setting themselves to SUPERSEDED.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/browser/configure.zcml' |
2 | --- lib/lp/code/browser/configure.zcml 2015-09-10 11:20:58 +0000 |
3 | +++ lib/lp/code/browser/configure.zcml 2015-09-11 14:23:48 +0000 |
4 | @@ -1176,7 +1176,7 @@ |
5 | class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildCancelView" |
6 | name="+cancel" |
7 | template="../../app/templates/generic-edit.pt" |
8 | - permission="launchpad.Admin"/> |
9 | + permission="launchpad.Edit"/> |
10 | <browser:page |
11 | for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild" |
12 | class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildRescoreView" |
13 | |
14 | === modified file 'lib/lp/code/browser/sourcepackagerecipebuild.py' |
15 | --- lib/lp/code/browser/sourcepackagerecipebuild.py 2013-11-18 07:51:18 +0000 |
16 | +++ lib/lp/code/browser/sourcepackagerecipebuild.py 2015-09-11 14:23:48 +0000 |
17 | @@ -39,13 +39,6 @@ |
18 | ) |
19 | |
20 | |
21 | -UNEDITABLE_BUILD_STATES = ( |
22 | - BuildStatus.FULLYBUILT, |
23 | - BuildStatus.FAILEDTOBUILD, |
24 | - BuildStatus.SUPERSEDED, |
25 | - BuildStatus.FAILEDTOUPLOAD,) |
26 | - |
27 | - |
28 | class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin): |
29 | |
30 | usedfor = ISourcePackageRecipeBuild |
31 | @@ -60,21 +53,17 @@ |
32 | |
33 | links = ('cancel', 'rescore') |
34 | |
35 | - @enabled_with_permission('launchpad.Admin') |
36 | + @enabled_with_permission('launchpad.Edit') |
37 | def cancel(self): |
38 | - if self.context.status in UNEDITABLE_BUILD_STATES: |
39 | - enabled = False |
40 | - else: |
41 | - enabled = True |
42 | - return Link('+cancel', 'Cancel build', icon='remove', enabled=enabled) |
43 | + return Link( |
44 | + '+cancel', 'Cancel build', icon='remove', |
45 | + enabled=self.context.can_be_cancelled) |
46 | |
47 | @enabled_with_permission('launchpad.Admin') |
48 | def rescore(self): |
49 | - if self.context.status in UNEDITABLE_BUILD_STATES: |
50 | - enabled = False |
51 | - else: |
52 | - enabled = True |
53 | - return Link('+rescore', 'Rescore build', icon='edit', enabled=enabled) |
54 | + return Link( |
55 | + '+rescore', 'Rescore build', icon='edit', |
56 | + enabled=self.context.can_be_rescored) |
57 | |
58 | |
59 | class SourcePackageRecipeBuildView(LaunchpadView): |
60 | @@ -152,7 +141,7 @@ |
61 | @action('Cancel build', name='cancel') |
62 | def request_action(self, action, data): |
63 | """Cancel the build.""" |
64 | - self.context.cancelBuild() |
65 | + self.context.cancel() |
66 | |
67 | |
68 | class SourcePackageRecipeBuildRescoreView(LaunchpadFormView): |
69 | |
70 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py' |
71 | --- lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2015-08-03 12:59:18 +0000 |
72 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2015-09-11 14:23:48 +0000 |
73 | @@ -22,7 +22,6 @@ |
74 | BrowserTestCase, |
75 | login, |
76 | logout, |
77 | - person_logged_in, |
78 | TestCaseWithFactory, |
79 | ) |
80 | from lp.testing.layers import DatabaseFunctionalLayer |
81 | @@ -83,17 +82,19 @@ |
82 | daily_build_archive=self.ppa) |
83 | build = self.factory.makeSourcePackageRecipeBuild( |
84 | recipe=recipe) |
85 | + build.queueBuild() |
86 | return build |
87 | |
88 | def test_cancel_build(self): |
89 | - """An admin can cancel a build.""" |
90 | + """The archive owner can cancel a build.""" |
91 | queue = self.factory.makeSourcePackageRecipeBuild().queueBuild() |
92 | build = queue.specific_build |
93 | transaction.commit() |
94 | build_url = canonical_url(build) |
95 | + owner = build.archive.owner |
96 | logout() |
97 | |
98 | - browser = self.getUserBrowser(build_url, user=self.admin) |
99 | + browser = self.getUserBrowser(build_url, user=owner) |
100 | browser.getLink('Cancel build').click() |
101 | |
102 | self.assertEqual( |
103 | @@ -107,12 +108,10 @@ |
104 | build_url) |
105 | |
106 | login(ANONYMOUS) |
107 | - self.assertEqual( |
108 | - BuildStatus.SUPERSEDED, |
109 | - build.status) |
110 | + self.assertEqual(BuildStatus.CANCELLED, build.status) |
111 | |
112 | - def test_cancel_build_not_admin(self): |
113 | - """No one but an admin can cancel a build.""" |
114 | + def test_cancel_build_not_owner(self): |
115 | + """A normal user can't cancel a build.""" |
116 | queue = self.factory.makeSourcePackageRecipeBuild().queueBuild() |
117 | build = queue.specific_build |
118 | transaction.commit() |
119 | @@ -131,12 +130,14 @@ |
120 | def test_cancel_build_wrong_state(self): |
121 | """If the build isn't queued, you can't cancel it.""" |
122 | build = self.makeRecipeBuild() |
123 | - build.cancelBuild() |
124 | + with admin_logged_in(): |
125 | + build.cancel() |
126 | transaction.commit() |
127 | build_url = canonical_url(build) |
128 | + owner = build.archive.owner |
129 | logout() |
130 | |
131 | - browser = self.getUserBrowser(build_url, user=self.admin) |
132 | + browser = self.getUserBrowser(build_url, user=owner) |
133 | self.assertRaises( |
134 | LinkNotFoundError, |
135 | browser.getLink, 'Cancel build') |
136 | @@ -212,7 +213,8 @@ |
137 | def test_rescore_build_wrong_state(self): |
138 | """If the build isn't queued, you can't rescore it.""" |
139 | build = self.makeRecipeBuild() |
140 | - build.cancelBuild() |
141 | + with admin_logged_in(): |
142 | + build.cancel() |
143 | transaction.commit() |
144 | build_url = canonical_url(build) |
145 | logout() |
146 | @@ -228,25 +230,11 @@ |
147 | This is the case where the user has a stale link that they click on. |
148 | """ |
149 | build = self.factory.makeSourcePackageRecipeBuild() |
150 | - build.cancelBuild() |
151 | - index_url = canonical_url(build) |
152 | - browser = self.getViewBrowser(build, '+rescore', user=self.admin) |
153 | - self.assertEqual(index_url, browser.url) |
154 | - self.assertIn( |
155 | - 'Cannot rescore this build because it is not queued.', |
156 | - browser.contents) |
157 | - |
158 | - def test_rescore_build_wrong_state_stale_page(self): |
159 | - """Show sane error if you attempt to rescore a non-queued build. |
160 | - |
161 | - This is the case where the user is on the rescore page and submits. |
162 | - """ |
163 | - build = self.factory.makeSourcePackageRecipeBuild() |
164 | - index_url = canonical_url(build) |
165 | - browser = self.getViewBrowser(build, '+rescore', user=self.admin) |
166 | - with person_logged_in(self.admin): |
167 | - build.cancelBuild() |
168 | - browser.getLink('Rescore build').click() |
169 | + build.queueBuild() |
170 | + with admin_logged_in(): |
171 | + build.cancel() |
172 | + index_url = canonical_url(build) |
173 | + browser = self.getViewBrowser(build, '+rescore', user=self.admin) |
174 | self.assertEqual(index_url, browser.url) |
175 | self.assertIn( |
176 | 'Cannot rescore this build because it is not queued.', |
177 | |
178 | === modified file 'lib/lp/code/configure.zcml' |
179 | --- lib/lp/code/configure.zcml 2015-09-01 17:10:46 +0000 |
180 | +++ lib/lp/code/configure.zcml 2015-09-11 14:23:48 +0000 |
181 | @@ -1015,7 +1015,12 @@ |
182 | |
183 | <class |
184 | class="lp.code.model.sourcepackagerecipebuild.SourcePackageRecipeBuild"> |
185 | - <require permission="launchpad.View" interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"/> |
186 | + <require |
187 | + permission="launchpad.View" |
188 | + interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildView"/> |
189 | + <require |
190 | + permission="launchpad.Edit" |
191 | + interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildEdit"/> |
192 | </class> |
193 | |
194 | <securedutility |
195 | |
196 | === modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py' |
197 | --- lib/lp/code/interfaces/sourcepackagerecipebuild.py 2015-05-14 08:50:41 +0000 |
198 | +++ lib/lp/code/interfaces/sourcepackagerecipebuild.py 2015-09-11 14:23:48 +0000 |
199 | @@ -9,11 +9,17 @@ |
200 | 'ISourcePackageRecipeBuildSource', |
201 | ] |
202 | |
203 | -from lazr.restful.declarations import export_as_webservice_entry |
204 | +from lazr.restful.declarations import ( |
205 | + export_as_webservice_entry, |
206 | + export_write_operation, |
207 | + exported, |
208 | + operation_for_version, |
209 | + ) |
210 | from lazr.restful.fields import ( |
211 | CollectionField, |
212 | Reference, |
213 | ) |
214 | +from zope.interface import Interface |
215 | from zope.schema import ( |
216 | Bool, |
217 | Int, |
218 | @@ -21,9 +27,7 @@ |
219 | ) |
220 | |
221 | from lp import _ |
222 | -from lp.buildmaster.interfaces.buildfarmjob import ( |
223 | - ISpecificBuildFarmJobSource, |
224 | - ) |
225 | +from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource |
226 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
227 | from lp.code.interfaces.sourcepackagerecipe import ( |
228 | ISourcePackageRecipe, |
229 | @@ -35,9 +39,7 @@ |
230 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease |
231 | |
232 | |
233 | -class ISourcePackageRecipeBuild(IPackageBuild): |
234 | - """A build of a source package.""" |
235 | - export_as_webservice_entry() |
236 | +class ISourcePackageRecipeBuildView(IPackageBuild): |
237 | |
238 | id = Int(title=_("Identifier for this build.")) |
239 | |
240 | @@ -56,6 +58,16 @@ |
241 | recipe = Object( |
242 | schema=ISourcePackageRecipe, title=_("The recipe being built.")) |
243 | |
244 | + can_be_rescored = exported(Bool( |
245 | + title=_("Can be rescored"), |
246 | + required=True, readonly=True, |
247 | + description=_("Whether this build record can be rescored manually."))) |
248 | + |
249 | + can_be_cancelled = exported(Bool( |
250 | + title=_("Can be cancelled"), |
251 | + required=True, readonly=True, |
252 | + description=_("Whether this build record can be cancelled."))) |
253 | + |
254 | manifest = Object( |
255 | schema=ISourcePackageRecipeData, title=_( |
256 | 'A snapshot of the recipe for this build.')) |
257 | @@ -70,13 +82,36 @@ |
258 | def getFileByName(filename): |
259 | """Return the file under +files with specified name.""" |
260 | |
261 | - def cancelBuild(): |
262 | - """Cancel the build.""" |
263 | + |
264 | +class ISourcePackageRecipeBuildEdit(Interface): |
265 | + |
266 | + @export_write_operation() |
267 | + @operation_for_version("devel") |
268 | + def cancel(): |
269 | + """Cancel the build if it is either pending or in progress. |
270 | + |
271 | + Check the can_be_cancelled property prior to calling this method to |
272 | + find out if cancelling the build is possible. |
273 | + |
274 | + If the build is in progress, it is marked as CANCELLING until the |
275 | + buildd manager terminates the build and marks it CANCELLED. If the |
276 | + build is not in progress, it is marked CANCELLED immediately and is |
277 | + removed from the build queue. |
278 | + |
279 | + If the build is not in a cancellable state, this method is a no-op. |
280 | + """ |
281 | |
282 | def destroySelf(): |
283 | """Delete the build itself.""" |
284 | |
285 | |
286 | +class ISourcePackageRecipeBuild(ISourcePackageRecipeBuildView, |
287 | + ISourcePackageRecipeBuildEdit): |
288 | + """A build of a source package.""" |
289 | + |
290 | + export_as_webservice_entry() |
291 | + |
292 | + |
293 | class ISourcePackageRecipeBuildSource(ISpecificBuildFarmJobSource): |
294 | """A utility of this interface be used to create source package builds.""" |
295 | |
296 | |
297 | === modified file 'lib/lp/code/model/sourcepackagerecipebuild.py' |
298 | --- lib/lp/code/model/sourcepackagerecipebuild.py 2015-09-11 13:28:31 +0000 |
299 | +++ lib/lp/code/model/sourcepackagerecipebuild.py 2015-09-11 14:23:48 +0000 |
300 | @@ -259,11 +259,31 @@ |
301 | builds.append(build) |
302 | return builds |
303 | |
304 | - def cancelBuild(self): |
305 | - """See `ISourcePackageRecipeBuild.`""" |
306 | - self.updateStatus(BuildStatus.SUPERSEDED) |
307 | - if self.buildqueue_record is not None: |
308 | - self.buildqueue_record.destroySelf() |
309 | + @property |
310 | + def can_be_rescored(self): |
311 | + """See `IBuild`.""" |
312 | + return self.status is BuildStatus.NEEDSBUILD |
313 | + |
314 | + @property |
315 | + def can_be_cancelled(self): |
316 | + """See `ISourcePackageRecipeBuild`.""" |
317 | + if not self.buildqueue_record: |
318 | + return False |
319 | + |
320 | + cancellable_statuses = [ |
321 | + BuildStatus.BUILDING, |
322 | + BuildStatus.NEEDSBUILD, |
323 | + ] |
324 | + return self.status in cancellable_statuses |
325 | + |
326 | + def cancel(self): |
327 | + """See `ISourcePackageRecipeBuild`.""" |
328 | + if not self.can_be_cancelled: |
329 | + return |
330 | + # BuildQueue.cancel() will decide whether to go straight to |
331 | + # CANCELLED, or go through CANCELLING to let buildd-manager |
332 | + # clean up the slave. |
333 | + self.buildqueue_record.cancel() |
334 | |
335 | def destroySelf(self): |
336 | if self.buildqueue_record is not None: |
337 | |
338 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py' |
339 | --- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2015-04-30 01:45:30 +0000 |
340 | +++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2015-09-11 14:23:48 +0000 |
341 | @@ -666,7 +666,8 @@ |
342 | # Cancelled builds are not considered pending. |
343 | recipe = self.factory.makeSourcePackageRecipe() |
344 | build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe) |
345 | - build.cancelBuild() |
346 | + build.queueBuild() |
347 | + build.cancel() |
348 | self.assertEqual([build], list(recipe.builds)) |
349 | self.assertEqual([build], list(recipe.completed_builds)) |
350 | self.assertEqual([], list(recipe.pending_builds)) |
351 | |
352 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py' |
353 | --- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2015-09-11 06:04:36 +0000 |
354 | +++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2015-09-11 14:23:48 +0000 |
355 | @@ -38,6 +38,7 @@ |
356 | from lp.services.mail.sendmail import format_address |
357 | from lp.services.webapp.authorization import check_permission |
358 | from lp.testing import ( |
359 | + admin_logged_in, |
360 | ANONYMOUS, |
361 | login, |
362 | person_logged_in, |
363 | @@ -78,7 +79,8 @@ |
364 | # SourcePackageRecipeBuild provides IPackageBuild and |
365 | # ISourcePackageRecipeBuild. |
366 | spb = self.makeSourcePackageRecipeBuild() |
367 | - self.assertProvides(spb, ISourcePackageRecipeBuild) |
368 | + with admin_logged_in(): |
369 | + self.assertProvides(spb, ISourcePackageRecipeBuild) |
370 | |
371 | def test_implements_interface(self): |
372 | build = self.makeSourcePackageRecipeBuild() |
373 | @@ -88,7 +90,8 @@ |
374 | # A source package recipe build can be stored in the database |
375 | spb = self.makeSourcePackageRecipeBuild() |
376 | transaction.commit() |
377 | - self.assertProvides(spb, ISourcePackageRecipeBuild) |
378 | + with admin_logged_in(): |
379 | + self.assertProvides(spb, ISourcePackageRecipeBuild) |
380 | |
381 | def test_queueBuild(self): |
382 | spb = self.makeSourcePackageRecipeBuild() |
383 | @@ -411,7 +414,8 @@ |
384 | # ISourcePackageRecipeBuild should make sure to remove jobs and build |
385 | # queue entries and then invalidate itself. |
386 | build = self.factory.makeSourcePackageRecipeBuild() |
387 | - build.destroySelf() |
388 | + with admin_logged_in(): |
389 | + build.destroySelf() |
390 | |
391 | def test_destroySelf_clears_release(self): |
392 | # Destroying a sourcepackagerecipebuild removes references to it from |
393 | @@ -420,7 +424,8 @@ |
394 | release = self.factory.makeSourcePackageRelease( |
395 | source_package_recipe_build=build) |
396 | self.assertEqual(build, release.source_package_recipe_build) |
397 | - build.destroySelf() |
398 | + with admin_logged_in(): |
399 | + build.destroySelf() |
400 | self.assertIs(None, release.source_package_recipe_build) |
401 | transaction.commit() |
402 | |
403 | @@ -433,18 +438,19 @@ |
404 | # Ensure database ids are set. |
405 | store.flush() |
406 | build_farm_job_id = naked_build.build_farm_job_id |
407 | - build.destroySelf() |
408 | + with admin_logged_in(): |
409 | + build.destroySelf() |
410 | self.assertIs(None, store.get(BuildFarmJob, build_farm_job_id)) |
411 | |
412 | - def test_cancelBuild(self): |
413 | + def test_cancel(self): |
414 | # ISourcePackageRecipeBuild should make sure to remove jobs and build |
415 | # queue entries and then invalidate itself. |
416 | build = self.factory.makeSourcePackageRecipeBuild() |
417 | - build.cancelBuild() |
418 | + build.queueBuild() |
419 | + with admin_logged_in(): |
420 | + build.cancel() |
421 | |
422 | - self.assertEqual( |
423 | - BuildStatus.SUPERSEDED, |
424 | - build.status) |
425 | + self.assertEqual(BuildStatus.CANCELLED, build.status) |
426 | |
427 | def test_getUploader(self): |
428 | # For ACL purposes the uploader is the build requester. |
429 | @@ -524,7 +530,8 @@ |
430 | build = self.factory.makeSourcePackageRecipeBuild( |
431 | recipe=cake, distroseries=secret, archive=pantry) |
432 | build.updateStatus(BuildStatus.FULLYBUILT) |
433 | - cake.destroySelf() |
434 | + with admin_logged_in(): |
435 | + cake.destroySelf() |
436 | IStore(build).flush() |
437 | build.notify() |
438 | notifications = pop_notifications() |
Very nice, thanks.